一、前言
在学习API9的时候就写了一个DragView
,用于展示某个页面的悬浮可拖动的入口,特意丰富了许多的功能,今天分享给大家~。Demo基于API11。
二、思路
因为API本身就带有拖拽的手势,所以直接使用:PanGesture
,根据拖拽返回的坐标,动态的更新DragView
的position
坐标。即可实现拖拽的功能。
除了拖拽,还需要的是从停留位置,吸附到某个位置。我们使用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)
})
)
分别处理三个事件,onActionStart
和onActionUpdate
事件是独立的,但是逻辑一致所以全部使用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
}
}
最后、如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏