3

一、前言

在学习API9的时候就写了一个DragView,用于展示某个页面的悬浮可拖动的入口,特意丰富了许多的功能,今天分享给大家~。Demo基于API11。

二、思路

因为API本身就带有拖拽的手势,所以直接使用:PanGesture,根据拖拽返回的坐标,动态的更新DragViewposition坐标。即可实现拖拽的功能。

除了拖拽,还需要的是从停留位置,吸附到某个位置。我们使用animateTo,结合坐标值即可完成很好的吸附效果。

三、准备容器

使用.position(this.curPosition)来控制拖拽的UI位置。dragContentBuilder方便自定义内容,组件的复用。

 @State private curPosition: Position = { x: 0, y: 0 };
 build() {
     Stack() {
         if (this.dragContentBuilder) {
             this.dragContentBuilder()
         } else {
             this.defDragView()
         }
     }
     )
     .position(this.curPosition)
     .onClick(this.onClickListener)
 }

四、边界

一般而言,拖拽的边界肯定是当前屏幕中的,但是如果需求需要限制在某个区域,或者需要规避一些位置。所以我们准备一个边界对象,来更好的管理拖拽的边界。

 boundArea: BoundArea = new BoundArea(0, 0, px2vp(display.getDefaultDisplaySync()
         .width), px2vp(display.getDefaultDisplaySync().height))
 export class BoundArea {
     readonly start: number = 0
     readonly end: number = 0
     readonly top: number = 0
     readonly bottom: number = 0
     readonly width: number = 0
     readonly height: number = 0
     readonly centerX: number = 0
     readonly centerY: number = 0
 ​
     constructor(start: number, top: number, end: number, bottom: number) {
         this.start = start
         this.top = top
         this.end = end
         this.bottom = bottom
         this.width = this.end - this.start
         this.height = this.bottom - this.top
         this.centerX = this.width / 2 + this.start
         this.centerY = this.height / 2 + this.top
     }
 }

boundArea默认使用了整个屏幕的坐标。

五、容器大小

因为具体的UI是从外部传入的,所以宽高不确定,需要计算。我们这里使用onAreaChange,绑定到容器上:

 .onAreaChange((oldValue: Area, newValue: Area) => {
     let height = newValue.height as number
     let width = newValue.width as number
     if ((this.dragHeight != height || this.dragWidth != width) && (height != 0 && width != 0)) {
         this.dragHeight = height
         this.dragWidth = width
     }
 })

可以看到,在容器发生改变的时候,我们保存它的宽高。

六、拖拽

拖拽手势使用起来还是很简单的:

 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });

direction决定了可以在哪个方向拖,我们显然需要所有方向。当然如果后续需要限制拖动方向,修改即可。

将拖动事件绑定到容器上:

 .gesture( // 绑定PanGesture事件,监听拖拽动作
     PanGesture(this.panOption)
         .onActionStart((event: GestureEvent) => {
             this.changePosition(event.offsetX, event.offsetY)
         })
         .onActionUpdate((event: GestureEvent) => {
             this.changePosition(event.offsetX, event.offsetY)
         })
         .onActionEnd((event: GestureEvent) => {
             this.endPosition = this.curPosition
             this.adsorbToEnd(this.endPosition.x, this.endPosition.y)
         })
 )

分别处理三个事件,onActionStartonActionUpdate事件是独立的,但是逻辑一致所以全部使用this.changePosition(event.offsetX, event.offsetY)处理。

 private changePosition(offsetX: number, offsetY: number) {
     let targetX = this.endPosition.x + offsetX;
     let targetY = this.endPosition.y + offsetY;
     targetX = Math.max(this.boundArea.start, Math.min(targetX, this.boundArea.end - this.dragHeight));
     targetY = Math.max(this.boundArea.top, Math.min(targetY, this.boundArea.bottom - this.dragWidth));
     this.curPosition = { x: targetX, y: targetY };
 }

因为存在边界,所以我们需要限制curPosition的变化,在当前拖动的坐标和边界值之间取合理的值。因为容器存在宽高,所以我们需要考虑到其宽高。

当手指抬起的时候,需要做动画吸附:

 private adsorbToEnd(startX: number, startY: number) {
     let targetX = 0
     let targetY = 0
     if (startX <= (this.boundArea.centerX)) {
         targetX = this.boundArea.start + ((this.dragMargin.left ?? 0) as number)
     } else {
         targetX = this.boundArea.end - ((this.dragMargin.right ?? 0) as number) - this.dragWidth
     }
     let newTopBound = this.boundArea.top + ((this.dragMargin.top ?? 0) as number)
     let newBottomBound = this.boundArea.bottom - ((this.dragMargin.bottom ?? 0) as number) - this.dragWidth
     if (startY <= newTopBound) {
         targetY = newTopBound
     } else if (startY >= newBottomBound) {
         targetY = newBottomBound
     } else {
         targetY = startY
     }
     this.startMoveAnimateTo(targetX, targetY)
 }
 ​
 private startMoveAnimateTo(x: number, y: number) {
     animateTo({
         duration: 300,
         curve: Curve.Smooth, 
         iterations: 1, 
         playMode: PlayMode.Normal, 
         onFinish: () => {
             this.endPosition = this.curPosition
         }
     }, () => {
         this.curPosition = { x: x, y: y }
     })
 }

startX <= (this.boundArea.centerX)用于判断在边界的位置,根据位置来决定吸附到左边还是右边。计算出吸附的位置之后,只需要使用animateTo来触发this.curPosition的更新即可。

七、初始位置

如果不能控制一开始的显示位置,对于使用者的体验非常不好,所以我们可以新增一个参数Alignment来更改初始位置:

 dragAlign: Alignment = Alignment.BottomStart

可能还要微调位置,所以再加一个margin:

 dragMargin: Margin = {}

在onAreaChange的时候进行更新:

 .onAreaChange((oldValue: Area, newValue: Area) => {
     //.....
     if (this.isNotInit) {
         this.initAlign()
     }
 })
 ​
 private initAlign() {
     this.isNotInit = false
     let x = 0
     let y = 0
     let topMargin: number = (this.dragMargin.top ?? 0) as number
     let bottomMargin: number = (this.dragMargin.bottom ?? 0) as number
     let startMargin: number = (this.dragMargin.left ?? 0) as number
     let endMargin: number = (this.dragMargin.right ?? 0) as number
     switch (this.dragAlign) {
         case Alignment.Start:
             x = this.boundArea.start + startMargin
             break;
         case Alignment.Top:
             y = this.boundArea.top + topMargin
             break;
         case Alignment.End:
             x = this.boundArea.end - this.dragWidth - endMargin
             break;
         case Alignment.Bottom:
             y = this.boundArea.bottom - this.dragHeight - bottomMargin
             break;
         case Alignment.TopStart:
             x = this.boundArea.start + startMargin
             y = this.boundArea.top + topMargin
             break;
         case Alignment.BottomStart:
             x = this.boundArea.start + startMargin
             y = this.boundArea.bottom - this.dragHeight - bottomMargin
             break;
         case Alignment.BottomEnd:
             x = this.boundArea.end - this.dragWidth - endMargin
             y = this.boundArea.bottom - this.dragHeight - bottomMargin
             break;
         case Alignment.Center:
             x = this.boundArea.centerX - this.dragWidth / 2 + startMargin - endMargin
             y = this.boundArea.centerY - this.dragHeight / 2 + topMargin - bottomMargin
             break;
     }
     this.curPosition = { x: x, y: y }
     this.endPosition = this.curPosition
 }

只要稍微考虑容器宽高并计算下就好了。

八、使用

非常简单

 DragView({
     dragAlign: Alignment.Center,
     dragMargin: bothway(10),
     dragContentBuilder:this.defDragView()
 })
 ​
 @Builder
 defDragView() {
     Stack() {
         Text("拖我")
             .width(50)
             .height(50)
             .fontSize(15)
     }
     .shadow({
         radius: 1.5,
         color: "#80000000",
         offsetX: 0,
         offsetY: 1
     })
     .padding(18)
     .borderRadius(30)
     .backgroundColor(Color.White)
     .animation({ duration: 200, curve: Curve.Smooth })
 }

当然你想往里面塞任何东西都行~

九、总结

当然还有很多人需要跨页面的悬浮窗,这可以参考应用内消息通知,活用subWindow.moveWindowTo(0, 0);

因为我使用的是Navigation路由方案,所以放在顶层直接是跨页面的。

完整的代码:(懒得上传了,只有一个import,复制即用)

 import { display, Position } from '@kit.ArkUI';
 ​
 @Preview
 @Component
 export struct DragView {
     private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
     private endPosition: Position = { x: 0, y: 0 }
     private dragHeight: number = 0
     private dragWidth: number = 0
     private dragMargin: Margin = {}
     boundArea: BoundArea = new BoundArea(0, 0, px2vp(display.getDefaultDisplaySync()
         .width), px2vp(display.getDefaultDisplaySync().height))
     private isNotInit: boolean = true
     @State private curPosition: Position = { x: 0, y: 0 };
     dragAlign: Alignment = Alignment.BottomStart
     onClickListener?: (event: ClickEvent) => void
     @BuilderParam dragContentBuilder: CustomBuilder
 ​
     build() {
         Stack() {
             if (this.dragContentBuilder) {
                 this.dragContentBuilder()
             } else {
                 this.defDragView()
             }
         }
         .onAreaChange((oldValue: Area, newValue: Area) => {
             let height = newValue.height as number
             let width = newValue.width as number
             if ((this.dragHeight != height || this.dragWidth != width) && (height != 0 && width != 0)) {
                 this.dragHeight = height
                 this.dragWidth = width
             }
             if (this.isNotInit) {
                 this.initAlign()
             }
         })
         .gesture( // 绑定PanGesture事件,监听拖拽动作
             PanGesture(this.panOption)
                 .onActionStart((event: GestureEvent) => {
                     this.changePosition(event.offsetX, event.offsetY)
                 })
                 .onActionUpdate((event: GestureEvent) => {
                     this.changePosition(event.offsetX, event.offsetY)
                 })
                 .onActionEnd((event: GestureEvent) => {
                     this.endPosition = this.curPosition
                     this.adsorbToEnd(this.endPosition.x, this.endPosition.y)
                 })
         )
         .position(this.curPosition)
         .onClick(this.onClickListener)
     }
 ​
     private adsorbToEnd(startX: number, startY: number) {
         let targetX = 0
         let targetY = 0
         if (startX <= (this.boundArea.centerX)) {
             targetX = this.boundArea.start + ((this.dragMargin.left ?? 0) as number)
         } else {
             targetX = this.boundArea.end - ((this.dragMargin.right ?? 0) as number) - this.dragWidth
         }
         let newTopBound = this.boundArea.top + ((this.dragMargin.top ?? 0) as number)
         let newBottomBound = this.boundArea.bottom - ((this.dragMargin.bottom ?? 0) as number) - this.dragWidth
         if (startY <= newTopBound) {
             targetY = newTopBound
         } else if (startY >= newBottomBound) {
             targetY = newBottomBound
         } else {
             targetY = startY
         }
         this.startMoveAnimateTo(targetX, targetY)
     }
 ​
     private changePosition(offsetX: number, offsetY: number) {
         let targetX = this.endPosition.x + offsetX;
         let targetY = this.endPosition.y + offsetY;
 ​
         targetX = Math.max(this.boundArea.start, Math.min(targetX, this.boundArea.end - this.dragHeight));
         targetY = Math.max(this.boundArea.top, Math.min(targetY, this.boundArea.bottom - this.dragWidth));
 ​
         this.curPosition = { x: targetX, y: targetY };
     }
 ​
     private startMoveAnimateTo(x: number, y: number) {
         animateTo({
             duration: 300, // 动画时长
             curve: Curve.Smooth, // 动画曲线
             iterations: 1, // 播放次数
             playMode: PlayMode.Normal, // 动画模式
             onFinish: () => {
                 this.endPosition = this.curPosition
             }
         }, () => {
             this.curPosition = { x: x, y: y }
         })
     }
 ​
     private initAlign() {
         this.isNotInit = false
         let x = 0
         let y = 0
         let topMargin: number = (this.dragMargin.top ?? 0) as number
         let bottomMargin: number = (this.dragMargin.bottom ?? 0) as number
         let startMargin: number = (this.dragMargin.left ?? 0) as number
         let endMargin: number = (this.dragMargin.right ?? 0) as number
         switch (this.dragAlign) {
             case Alignment.Start:
                 x = this.boundArea.start + startMargin
                 break;
             case Alignment.Top:
                 y = this.boundArea.top + topMargin
                 break;
             case Alignment.End:
                 x = this.boundArea.end - this.dragWidth - endMargin
                 break;
             case Alignment.Bottom:
                 y = this.boundArea.bottom - this.dragHeight - bottomMargin
                 break;
             case Alignment.TopStart:
                 x = this.boundArea.start + startMargin
                 y = this.boundArea.top + topMargin
                 break;
             case Alignment.BottomStart:
                 x = this.boundArea.start + startMargin
                 y = this.boundArea.bottom - this.dragHeight - bottomMargin
                 break;
             case Alignment.BottomEnd:
                 x = this.boundArea.end - this.dragWidth - endMargin
                 y = this.boundArea.bottom - this.dragHeight - bottomMargin
                 break;
             case Alignment.Center:
                 x = this.boundArea.centerX - this.dragWidth / 2 + startMargin - endMargin
                 y = this.boundArea.centerY - this.dragHeight / 2 + topMargin - bottomMargin
                 break;
         }
 ​
         this.curPosition = { x: x, y: y }
         this.endPosition = this.curPosition
     }
 ​
     @Builder
     defDragView() {
         Stack()
             .width(100)
             .height(100)
             .backgroundColor(Color.Orange)
     }
 }
 ​
 export class BoundArea {
     readonly start: number = 0
     readonly end: number = 0
     readonly top: number = 0
     readonly bottom: number = 0
     readonly width: number = 0
     readonly height: number = 0
     readonly centerX: number = 0
     readonly centerY: number = 0
 ​
     constructor(start: number, top: number, end: number, bottom: number) {
         this.start = start
         this.top = top
         this.end = end
         this.bottom = bottom
         this.width = this.end - this.start
         this.height = this.bottom - this.top
         this.centerX = this.width / 2 + this.start
         this.centerY = this.height / 2 + this.top
     }
 }

最后、如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏


猫猫头啊
1.4k 声望689 粉丝

猫猫头,用狗子头像。只是想说,别给自己画圈。我可以、我都行、我什么都是。