HarmonyOS(40) 悬浮框实现

悬浮框

实现效果和样式

如下图:按住电话悬浮框,随着手指的拖动会滚动,同时当松开手指时如果在屏幕左半边,则自动移动到左边;反之会自动移动停靠到右边。

构建电话悬浮框的代码:就是一个Column>>Image + Text,然后设置圆角和通过shadow方法设置阴影即可。

java 复制代码
@Component
export default struct FloatingWindowComponent {
  private res: Resource = $r('app.media.ic_call_green');
  private tips?: Resource = $r('app.string.Tips_call');

  build() {
    Column() {
      Image(this.res)
        .objectFit(ImageFit.Contain)
        .width('40%')
        .height('40%')

      Text(this.tips)
        .fontSize(12)
        .fontColor($r('app.color.background_green'))
        .fontWeight(FontWeight.Regular)
        .fontFamily($r('app.string.Font_family_regular'))
    }
    .width(80)
    .height(80)
    .backgroundColor($r('app.color.white'))
    .borderRadius(16)
    .shadow({ radius: 15, color: $r('app.color.btn_border_color') })
    .justifyContent(FlexAlign.SpaceAround)
  }
}

相关概念

实现随着手指的移动而移动,就需要获取手指当前的(x,y)坐标值。在onTouch事件TouchEvent来获取对应的位置,TouchEvent对象提供了(windowX,windowY)、(displayX,displayY)、(screenX,screenY)(已废弃)三对属性。比如我们想获取手指按下时对应的坐标位置,可以用如下代码:

dart 复制代码
 if (event.type === TouchType.Down) {
    this.moveStartX = event.touches[0].windowX; // 按下时获取X坐标初始值
     this.moveStartY = event.touches[0].windowY; // 按下时获取Y坐标初始值
 }

具体的代表的意思如下:

实现思路

  1. 获取屏幕的宽度和高度,通过display获取屏幕的宽和高,然后通过px2vp将px转换成vp
java 复制代码
  aboutToAppear() {
    display.getAllDisplays((err, data) => {
      // 拿到屏幕宽高的一半,作为判断基准值
      this.displayHalfWidth = data[0].width / 2;
      this.displayHalfHeight = data[0].height / 2;
      // 将拿到的px转为vp
      this.displayHalfWidth = px2vp(this.displayHalfWidth);
      this.displayHalfHeight = px2vp(this.displayHalfHeight);
    })
  }
  1. 使用@state方法修饰当前(x,y)的坐标位置,当二者的值发生变化时,会更新组件的位置,实现悬浮框跟着手指移动的效果。
dart 复制代码
 @State positionX: number = 50; // 组件位置X
 @State positionY: number = 500; // 组件位置Y
 /下面的系列属性,官方给的demo中都加了@State修饰,其实没必要
 moveStartX: number = 0; // X方向起始点
 moveStartY: number = 0; // Y方向起始点
 moveEndX: number = 0; // X方向终点
 moveEndY: number = 0; // Y方向终点
 moveSumLengthX: number = 0; // X方向移动距离总和
 moveSumLengthY: number = 0; // Y方向移动距离总和
 moveStartTime: number = 0; // 触摸开始时间
 moveEndTime: number = 0; // 触摸结束时间
  1. 监听组件的onTouch方法,监听TouchType.Down,TouchType.Move, TouchType.Up事件,计算手指移动的位置,同时更新positionX和positionY,因为两个变量通过@State修饰,会刷新页面,实现悬浮框跟随者手指移动的效果,核心代码如下:
java 复制代码
if (event.type === TouchType.Move) {
  //省略部分代码

  // 跟手过程,使用responsiveSpringMotion曲线
  animateTo({ curve: curves.responsiveSpringMotion() }, () => {
    // 减去半径,以使球的中心运动到手指位置
    this.positionX = event.touches[0].windowX - this.diameter / 2;
    this.positionY = event.touches[0].windowY - this.diameter / 2 - 120;
    Logger.info(TAG, `move end, animateTo x:${this.positionX}, y:${this.positionY}`);
  })
}
  1. 将positionX和positionY设置给组件的position(x,y)方法:

全部源码

dart 复制代码
import { curves, display } from '@kit.ArkUI';
import { TitleBar } from '../../../../common/TitleBar'
import FloatingWindowComponent from './FloatingWindowComponent';
import Logger from '../../../../util/Logger';

const TAG = '[FloatingWindowPage]';

@Entry
@Component
struct FloatingWindowSample {
  private diameter: number = 120; // 触摸点相对偏移量
  @State positionX: number = 50; // 组件位置X
  @State positionY: number = 500; // 组件位置Y
  @State displayHalfWidth: number = 0; // 屏幕一半的宽
  @State displayHalfHeight: number = 0; // 屏幕一半的高
  @State moveStartX: number = 0; // X方向起始点
  @State moveStartY: number = 0; // Y方向起始点
  @State moveEndX: number = 0; // X方向终点
  @State moveEndY: number = 0; // Y方向终点
  @State moveSumLengthX: number = 0; // X方向移动距离总和
  @State moveSumLengthY: number = 0; // Y方向移动距离总和
  @State moveStartTime: number = 0; // 触摸开始时间
  @State moveEndTime: number = 0; // 触摸结束时间

  aboutToAppear() {
    display.getAllDisplays((err, data) => {
      // 拿到屏幕宽高的一半,作为判断基准值
      this.displayHalfWidth = data[0].width / 2;
      this.displayHalfHeight = data[0].height / 2;
      // 将拿到的px转为vp
      Logger.info(TAG, `aboutToAppear getAllDisplays data 1 width:${this.displayHalfWidth}, height:${this.displayHalfHeight}`);
      this.displayHalfWidth = px2vp(this.displayHalfWidth);
      this.displayHalfHeight = px2vp(this.displayHalfHeight);
      Logger.info(TAG, `aboutToAppear getAllDisplays data 2 width:${this.displayHalfWidth}, height:${this.displayHalfHeight}`);
    })
  }

  build() {
    Row() {
      Column() {
        TitleBar({ title: $r('app.string.Floating_window') })
          .id('target')
        Row() {
          Row() {
            FloatingWindowComponent()
          }
          .id('floatingWindowComponent')
          .width(80)
          .height(80)
          .position({ x: this.positionX, y: this.positionY })
          .onTouch((event: TouchEvent) => {
            if (event.type === TouchType.Down) {
              this.moveStartX = event.touches[0].windowX; // 按下时获取X坐标初始值
              this.moveStartY = event.touches[0].windowY; // 按下时获取Y坐标初始值
              this.moveStartTime = Date.now(); // 按下时开始时间
              this.moveSumLengthX = 0; // 按下时初始化x方向移动距离
              this.moveSumLengthY = 0; // 按下时初始化y方向移动距离
            }
            if (event.type === TouchType.Move) {
              this.moveEndX = event.touches[0].windowX; // X方向移动的当前位置
              this.moveEndY = event.touches[0].windowY; // Y方向移动的当前位置
              this.moveSumLengthX += Math.abs(this.moveEndX - this.moveStartX); // 每一次移动计算相对于上一次X方向位置的距离
              this.moveSumLengthY += Math.abs(this.moveEndY - this.moveStartY); // 每一次移动计算相对于上一次Y方向位置的距离
              this.moveStartX = this.moveEndX;
              this.moveStartY = this.moveEndY;
              Logger.info(TAG, `move ing, moveSumLengthX:${this.moveSumLengthX}, moveSumLengthY:${this.moveSumLengthY}`);

              // 跟手过程,使用responsiveSpringMotion曲线
              animateTo({ curve: curves.responsiveSpringMotion() }, () => {
                // 减去半径,以使球的中心运动到手指位置
                this.positionX = event.touches[0].windowX - this.diameter / 2;
                this.positionY = event.touches[0].windowY - this.diameter / 2 - 120;
                Logger.info(TAG, `move end, animateTo x:${this.positionX}, y:${this.positionY}`);
              })
            } else if (event.type === TouchType.Up) {//手指抬起时自动靠边处理
              this.moveEndTime = Date.now();
              let moveDiffTime = this.moveEndTime - this.moveStartTime; // 最后一秒移动的距离
              // 距离
              let s = Math.sqrt((this.moveSumLengthX * this.moveSumLengthX) + (this.moveSumLengthY * this.moveSumLengthY));
              // 时间
              let t = moveDiffTime;
              // 速度
              let v = s / t;
              Logger.info(TAG, `moveEnd, moveSumLengthX:${this.moveSumLengthX}, moveSumLengthY:${this.moveSumLengthY}, moveDiffTime:${moveDiffTime}`);
              Logger.info(TAG, `moveEnd, s:${s}, t:${t}, v:${v}`);

              // 离手时,使用springMotion曲线,且将移动时速度赋值给离手时速度
              animateTo({ curve: curves.springMotion(), tempo: v }, () => {
                if (this.positionX >= this.displayHalfWidth) {
                  // 如果划到右边,则定位至屏幕右边减去自身宽度80,再减去10留出间隙
                  this.positionX = this.displayHalfWidth * 2 - 90;
                } else {
                  this.positionX = 10;
                }
                if (this.positionY >= this.displayHalfHeight * 2 - 300) {
                  this.positionY = this.displayHalfHeight * 2 - 300;
                } else if (this.positionY <= 0) {
                  this.positionY = 10;
                }
                Logger.info(TAG, `touchUp, animateTo x:${this.displayHalfWidth}, y:100`);
              })
            }
          })
        }
        .width('100%')
        .height('92%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor($r('app.color.background_shallow_grey'))
    }
    .width('100%')
    .height('100%')
  }
}

参考资料

源码传送门

下载"语言-语言基础类库",运行后进入:动画>专场动画>悬浮窗。即可看到运行效果。

HarmonyOS鸿蒙学习笔记(5)@State作用说明和简单案例
HarmonyOS鸿蒙学习笔记(17)获取屏幕宽高等属性
触摸事件

相关推荐
似水流年QC2 小时前
初探鸿蒙:从概念到实践
华为·harmonyos
HMS Core3 小时前
融合虚拟与现实,AR Engine为用户提供沉浸式交互体验
华为·ar·harmonyos
dawn12 小时前
鸿蒙ArkTS中的获取网络数据
华为·harmonyos
桃花键神12 小时前
鸿蒙5.0时代:原生鸿蒙应用市场引领开发者服务新篇章
华为·harmonyos
鸿蒙自习室12 小时前
鸿蒙多线程开发——并发模型对比(Actor与内存共享)
华为·harmonyos
JavaPub-rodert13 小时前
鸿蒙生态崛起:开发者的机遇与挑战
华为·harmonyos
帅比九日15 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos
yilylong17 小时前
鸿蒙(Harmony)实现滑块验证码
华为·harmonyos·鸿蒙
baby_hua17 小时前
HarmonyOS第一课——DevEco Studio的使用
华为·harmonyos
HarmonyOS_SDK17 小时前
融合虚拟与现实,AR Engine为用户提供沉浸式交互体验
harmonyos