
回复
实现一个全局可随意拖动的悬浮工具球,可控制是否吸边,根据悬浮球的位置,向四个方向弹出工具栏。全局监听工具按钮的点击事件回调。
效果演示:
实现思路:
1.使用新建窗口实现悬浮状态,收起状态时系统窗口和控制按钮大小一样,当展开时,计算需要的窗口大小,重新设置悬浮窗口大小
2.控制是否吸边,只需要在拖动手势结束时,判断当前手指位置在屏幕的左侧还是右侧,然后将窗口的X坐标赋值成0或屏幕像素-窗口大小,避免遮挡
3.将屏幕划分为四个区域,上左上右下左下右,操作按钮在不同的区域内,向不同的方向弹出工具栏
4.窗口大小发生变化,操作按钮相对于窗口的位置也会发生变化,因此需要在展开时重新计算操作按钮相当于窗口的位置
5.X轴使用Stack布局存放所有按键,Alignment设置的是底部居左,当在左半屏展开时,不需要修改X偏移,在右半屏展开时,需要将窗口向左移动,操作按键相对于窗口向右偏移。这样就可以保证展开时操作按钮在X轴方向没有移动
6.Y轴,当在上半屏展开时,想要工具栏向下展开,因此操作按钮相对于Y轴要向上移动,当在下半屏展开时,想要工具栏向上展开,因此需要将窗口向上移动,这样就保证展开时操作按钮相对于Y轴没有移动。
7.拖动手势,只需要在拖动的过程中实时修改窗口位置就可以实现拖动效果
8.单击手势,点击展开时,需要计算每个弹出工具栏的位置,这里可以参考上一篇文章,使用三角函数计算每个按键的X,Y偏移量
9.点击回调,由于点击回调可能接收的位置是任意一个page页面,因此这里使用eventHub事件通信机制,只需要在想要接收回调的地方订阅即可。
10.具体实现步骤可以参考源码注释
悬浮窗源码:
import { WindowUtils } from '../utils/WindowUtils';
export const FLOAT_WINDOW_DEFAUT_SIZE=40
export const EVENT_CLICK_MESSAGE:string='EVENT_CLICK_MESSAGE'
const FLOAT_WINDOW_ICON_SIZE=40
@Entry
@ComponentV2
struct FloatToolBar{
private isEdgeNear:boolean = AppStorage.get('isEdgeNear')??false // 是否贴边
@Local normalIconSpace:number = 120 // 控制按钮和弹出按钮的间距
private toolCounts:number = 4 //默认弹出4个按钮
private spaceAngle:number = 5 //距离XY轴的夹角
@Local floatOffsetX:number=0;
@Local floatOffsetY:number=0; // 控制 操作按钮 相对于 窗口的位置
@Local fingerClickX:number=0; //点击时 手指相对于屏幕的位置
@Local fingerClickY:number=WindowUtils.getWindowHeight()*2/3;
@Local posX:number = 0
@Local posY:number = WindowUtils.getWindowHeight()*2/3
@Local spaceBetween: number = 0
@Local iconOpacity: number = 0
@Local isExpend:boolean = false
aboutToAppear(): void {
}
calculteOffset(index: number): Position {
if (this.isExpend) {
//每个按钮之间的夹角
let gapAngle = (90-2*this.spaceAngle)/(this.toolCounts-1)
let angleRadian: number =0
//右上展开
if (vp2px(this.fingerClickX)<WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)>WindowUtils.getWindowHeight()/2){
angleRadian =( 360-(90-this.spaceAngle-index*gapAngle)) * Math.PI / 180
}
//左上
if (vp2px(this.fingerClickX)>WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)>WindowUtils.getWindowHeight()/2){
angleRadian = ( 180+ (90-this.spaceAngle-index*gapAngle)) * Math.PI / 180
}
//右下
if (vp2px(this.fingerClickX)<WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)<WindowUtils.getWindowHeight()/2){
angleRadian = ((90-this.spaceAngle-index*gapAngle)) * Math.PI / 180
}
//左下
if (vp2px(this.fingerClickX)>WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)<WindowUtils.getWindowHeight()/2){
angleRadian = ( 90 + (90-this.spaceAngle-index*gapAngle)) * Math.PI / 180
}
return {
x: this.floatOffsetX+this.spaceBetween * Math.cos(angleRadian),
y: this.floatOffsetY+this.spaceBetween * Math.sin(angleRadian)
}
}else {
//右上
if (vp2px(this.fingerClickX)<WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)>WindowUtils.getWindowHeight()/2){
return {
x: 0,
y: 0
}
}
//左上
if (vp2px(this.fingerClickX)>WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)>WindowUtils.getWindowHeight()/2){
return {
x: this.normalIconSpace,
y: 0
}
}
//右下
if (vp2px(this.fingerClickX)<WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)<WindowUtils.getWindowHeight()/2){
return {
x: 0,
y: -this.normalIconSpace
}
}
//左下
if (vp2px(this.fingerClickX)>WindowUtils.getWindowWidth()/2&&vp2px(this.fingerClickY)<WindowUtils.getWindowHeight()/2){
return {
x: this.normalIconSpace,
y: -this.normalIconSpace
}
}
return {
x: 0,
y: 0
}
}
}
build() {
Stack({alignContent:Alignment.BottomStart}){
Circle()
.width(FLOAT_WINDOW_ICON_SIZE)
.height(FLOAT_WINDOW_ICON_SIZE)
.fill(Color.Green)
.fillOpacity(this.iconOpacity)
.offset(this.calculteOffset(0))
.animation({
duration: 300,
curve:'ease-out'
})
.onClick(()=>{
WindowUtils.getUIAbilityContext()?.eventHub.emit('EVENT_CLICK_MESSAGE',Color.Green );
})
Circle()
.width(FLOAT_WINDOW_ICON_SIZE)
.height(FLOAT_WINDOW_ICON_SIZE)
.fill(Color.Brown)
.fillOpacity(this.iconOpacity)
.offset(this.calculteOffset(1))
.animation({
duration: 300,
curve:'ease-out'
})
.onClick(()=>{
WindowUtils.getUIAbilityContext()?.eventHub.emit('EVENT_CLICK_MESSAGE',Color.Brown );
})
Circle()
.width(FLOAT_WINDOW_ICON_SIZE)
.height(FLOAT_WINDOW_ICON_SIZE)
.fill(Color.Red)
.fillOpacity(this.iconOpacity)
.offset(this.calculteOffset(2))
.animation({
duration: 300,
curve:'ease-out'
})
.onClick(()=>{
WindowUtils.getUIAbilityContext()?.eventHub.emit('EVENT_CLICK_MESSAGE',Color.Red );
})
Circle()
.width(FLOAT_WINDOW_ICON_SIZE)
.height(FLOAT_WINDOW_ICON_SIZE)
.fill(Color.Yellow)
.fillOpacity(this.iconOpacity)
.offset(this.calculteOffset(3))
.animation({
duration: 300,
curve:'ease-out'
})
.onClick(()=>{
WindowUtils.getUIAbilityContext()?.eventHub.emit('EVENT_CLICK_MESSAGE',Color.Yellow );
})
Circle()
.width(FLOAT_WINDOW_ICON_SIZE)
.height(FLOAT_WINDOW_ICON_SIZE)
.fill(Color.Blue)
.fillOpacity(1)
.zIndex(5)
.offset({
x:this.floatOffsetX,
y:this.floatOffsetY
})
.gesture(
GestureGroup(GestureMode.Exclusive,
TapGesture({ count: 1, fingers: 1 })
.onAction((event: GestureEvent) => {
this.fingerClickX=event.fingerList[0].displayX
this.fingerClickY=event.fingerList[0].displayY
if (this.isExpend) {
if (this.fingerClickX>px2vp(WindowUtils.getWindowWidth()/2)) {
this.posX = this.posX+vp2px(this.spaceBetween)
this.floatOffsetX=0
}
if (this.fingerClickY<px2vp(WindowUtils.getWindowHeight()/2)) {
this.floatOffsetY = 0
this.posY = this.posY - vp2px(this.spaceBetween)
}
WindowUtils.subWindow?.resize(vp2px(FLOAT_WINDOW_ICON_SIZE),vp2px(FLOAT_WINDOW_ICON_SIZE))
this.posY = this.posY + vp2px(this.spaceBetween)
WindowUtils.subWindow?.moveWindowTo(this.posX ,this.posY)
this.isExpend = !this.isExpend
this.spaceBetween=0
this.iconOpacity=0
}else {
this.spaceBetween=this.normalIconSpace
this.iconOpacity=0.8
//位于左半屏 向右展开,不需要额外操作 位于右半屏时,向左展开
if (this.fingerClickX>px2vp(WindowUtils.getWindowWidth()/2)) {
this.posX = this.posX-vp2px(this.spaceBetween)
this.floatOffsetX = this.spaceBetween
}
//位于上半屏,向下展开
if (this.fingerClickY<px2vp(WindowUtils.getWindowHeight()/2)) {
this.floatOffsetY = -this.spaceBetween
}else {
this.posY = this.posY - vp2px(this.spaceBetween)
}
WindowUtils.subWindow?.resize(vp2px(this.spaceBetween+FLOAT_WINDOW_ICON_SIZE),vp2px(this.spaceBetween+FLOAT_WINDOW_ICON_SIZE))
WindowUtils.subWindow?.moveWindowTo(this.posX ,this.posY)
this.isExpend = !this.isExpend
}
}),
PanGesture()
.onActionStart( (event: GestureEvent) => {
this.normalIconSpace=0
})
.onActionUpdate((event: GestureEvent) => {
console.info('Pan start'+event.offsetX);
if (this.isExpend) return
this.fingerClickX=event.fingerList[0].displayX
this.fingerClickY=event.fingerList[0].displayY
this.posX += event.offsetX
this.posY += event.offsetY
//顶部限制
if (this.posY<WindowUtils.getStatusHeight()) {
this.posY = WindowUtils.getStatusHeight()
}
//底部限制
if (this.posY>WindowUtils.getWindowHeight()-WindowUtils.getNavHeight()-vp2px(FLOAT_WINDOW_DEFAUT_SIZE)) {
this.posY = WindowUtils.getWindowHeight()-WindowUtils.getNavHeight()-vp2px(FLOAT_WINDOW_DEFAUT_SIZE)
}
//左边限制
if (this.posX<0) {
this.posX=0
}
if (this.posX>WindowUtils.getWindowWidth()-vp2px(FLOAT_WINDOW_DEFAUT_SIZE)) {
this.posX = WindowUtils.getWindowWidth()-vp2px(FLOAT_WINDOW_DEFAUT_SIZE)
}
WindowUtils.subWindow?.moveWindowTo(this.posX ,this.posY)
})
.onActionEnd(()=>{
this.normalIconSpace=120
//判断是否贴边,如果开启贴边的话,结束拖拽判断是在屏幕最右还是最左
if (this.isEdgeNear) {
if (this.posX>WindowUtils.getWindowWidth()/2) {
this.posX = WindowUtils.getWindowWidth()-vp2px(FLOAT_WINDOW_DEFAUT_SIZE)
}else {
this.posX=0
}
WindowUtils.subWindow?.moveWindowTo(this.posX ,this.posY)
}
})
)
)
}.width('100%').height('100%')
}
}
调用源码:
import { EVENT_CLICK_MESSAGE, FLOAT_WINDOW_DEFAUT_SIZE } from '../pages/FloatToolBar'
import { WindowUtils } from '../utils/WindowUtils'
@Entry
@ComponentV2
struct TestFloatToolBar{
@Local receiveMessage:string='#00000000'
aboutToAppear(): void {
WindowUtils.getUIAbilityContext()?.eventHub.on(EVENT_CLICK_MESSAGE,(callBack:string)=>{
this.receiveMessage = callBack
})
}
build() {
Column({space:10}){
Button('打开任意位置悬浮框').onClick(()=>{
AppStorage.setOrCreate('isEdgeNear',false)
WindowUtils.openSubWindow('pages/FloatToolBar',{
height:vp2px(FLOAT_WINDOW_DEFAUT_SIZE),
width:vp2px(FLOAT_WINDOW_DEFAUT_SIZE),
y:WindowUtils.getWindowHeight()*2/3
})
})
Button('打开贴边悬浮框').onClick(()=>{
AppStorage.setOrCreate('isEdgeNear',true)
WindowUtils.openSubWindow('pages/FloatToolBar',{
height:vp2px(FLOAT_WINDOW_DEFAUT_SIZE),
width:vp2px(FLOAT_WINDOW_DEFAUT_SIZE),
y:WindowUtils.getWindowHeight()*2/3
})
})
Button('关闭悬浮窗').onClick(()=>{
WindowUtils.closeSubWindow()
})
Row(){
Text('点击了')
Row().width('30%').height(30).backgroundColor(this.receiveMessage)
}
}
}
}