HarmonyOS 手势拦截实战

118 阅读5分钟

大家好啊!我是说的鸿蒙,今天来点大家想看的东西。

前言

最近在看官方文档,看到手势拦截这一块时突发奇想,觉得有必要整个有关于手势的效果出来看看,于是乎想到了模仿某类手机系统应用做出了那么一个简单的Demo。

等不及了,快端上来罢!

效果

手势拦截1.gif 整个布局非常的简单,看起来就是一个Scroll嵌套了一个List,在List上又有一个Title和一个深色可变高度区域,当下拉时整体不滑动,当深色区域高度为0后开始滑动。

实现过程

其实需要处理的点就在于拦截Scroll的向下滑动手势,让深色区域消费掉这次滑动,当深色区域高度为0后Scroll在响应滑动事件

处理重点

我们把整个过程分成以下几个部分

  • 拦截Scroll事件
  • 深色区域处理滑动事件
  • 深色区域高度改变中途结束滑动事件处理
  • 滑动结束后Scroll响应滑动

拦截事件

HarmonyOS中手势拦截分两种

  • 手势触发控制
  • 手势响应控制

这里我们通过手势响应处理的方式来进行拦截

手势响应控制的前提是手势识别成功,如果手势不成功也不会产生手势回调响应。

通过手势识别器的setEnable方法,控制手势是否响应用户回调。使用parallelGesture配合onGestureRecognizerJudgeBegin的方式最终实现拦截操作

  • onGestureRecognizerJudgeBegin 用于手势拦截,获取手势识别器,初始化手势识别器开闭状态。
  • parallelGesture 并行的手势绑定方法,可以在父子组件上绑定可以同时响应的相同手势。

代码:

@Entry
@Component
struct Index {
  @State list: string[] = []
  @State searchHeight: number = 50
  private mHeight: number = 50
  private scrollGestureRecognizer?: GestureRecognizer

  aboutToAppear(): void {
    for (let i = 0; i < 20; i++) {
      this.list.push(i.toString())
    }
  }

  build() {
    Stack() {
      Scroll() {
        Column({ space: 15 }) {
          Column({ space: 10 }) { 
            Text('List')
              .fontSize(25)
              .fontColor(Color.Black)
              .fontWeight(FontWeight.Bold)
            //深色可变高度区域
            Stack()
              .width('90%')
              .height(this.searchHeight)
              .backgroundColor('#999999')
              .borderRadius(10)
              .animation({ duration: 150 })
          }
          .width('100%')
          .alignItems(HorizontalAlign.Center)

          List() {
            ForEach(this.list, (item: string, index: number) => {
              ListItem() {
                Text(item)
                  .fontSize(15)
                  .fontColor(Color.Brown)
              }
              .width('100%')
              .height(80)
            })
          }
          .width('100%')
          .height('auto')
          .divider({ strokeWidth: 1, color: Color.Gray })
        }
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .height('100%')
      .id('scroll')
      .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer,
        others: Array<GestureRecognizer>) => {
        //给手势识别器赋值
        this.scrollGestureRecognizer = current
        //展开状态下手势识别器关闭
        if (this.searchHeight != 0) {
          this.scrollGestureRecognizer.setEnabled(false)
        }
        return GestureJudgeResult.CONTINUE
      })
    }
    .parallelGesture( // 绑定一个Pan手势作为动态控制器,处理下滑手势
      PanGesture({ direction: PanDirection.Up | PanDirection.Down })
        .onActionUpdate((event: GestureEvent) => {
              //处理下滑操作
        })
      , GestureMask.Normal)
  }
}

我们在Scroll外层再套一层Stack作为父节点用来拦截手势并注册一个平行手势处理深色区域的高度,在手势更新时根据条件关闭/开放Scroll的手势识别器

处理滑动事件

在处理滑动事件时我们需要考虑滑动方向以及是否为快速滑动的情况。

  • 使用offsetY来判断滑动方向(手势事件y轴相对当前组件元素原始区域的偏移量,从上向下滑动offsetY为正反之为负)
  • 使用velocityY来判断是否为快速滑动(获取当前手势的y轴方向速度。坐标轴原点为屏幕左上角,分正负方向速度,从上往下为正,反之为负)

当向下滑动并且为快速滑动时深色区域高度直接赋值为0,并且开启Scroll手势识别器; 当向下滑动且非快速滑动时:

  • 当深色区域高度大于0: 关闭Scroll手势识别器,并使用预设高度减去offsetY绝对值改变高度。
  • 当深色区域高度等于0: 开启Scroll手势识别器响应滑动。 代码:
.parallelGesture( // 绑定一个Pan手势作为动态控制器
  PanGesture({ direction: PanDirection.Up | PanDirection.Down })
    .onActionUpdate((event: GestureEvent) => {
      if (this.isDown(event.offsetY)) {
        this.handleDown(event)
      }
    })
  , GestureMask.Normal)
isDown(offset: number): boolean {
  return offset < 0
}

isFastSwipe(event: GestureEvent) {
  return Math.abs(event.velocityY) > 1200 //自己估摸着来的
}

handleDown(event: GestureEvent) {
  if (this.isFastSwipe(event)) {
    this.scrollGestureRecognizer?.setEnabled(true)
    this.searchHeight = 0
  } else {
    if (this.searchHeight > 0) {
      this.scrollGestureRecognizer?.setEnabled(false)
      this.searchHeight = this.mHeight - Math.abs(event.offsetY)
    } else {
      this.scrollGestureRecognizer?.setEnabled(true)
    }
  }
}

处理手势中断

当深色区域高度正在改变时,中断手势的情况也需要一并考虑。 使用onTouch监听父节点touch事件,当手抬起时对深色区域高度进行处理:

  • 如果深色区域高度大于自身高度一半则恢复默认高度
  • 如果深色区域高度小于等于自身高度一半则折叠,高度为0
.onTouch((event) => {
  if (event.type == TouchType.Up) {
    if (this.searchHeight <= (this.mHeight / 2)) {
      this.searchHeight = 0
    } else {
      this.searchHeight = this.mHeight
    }
  }
})

效果:

手势拦截2.gif

结尾

那么好了,本篇内容到这里就结束了,其实实现起来并不算难只需要在合适时机控制目标组件的手势控制器就可以了,如果你有更好的版本在评论区展现出来罢,下期再见,评论区扣1复活博主,爱你们捏💗💗。

参考链接

完整代码

@Entry
@Component
struct Index {
  @State list: string[] = []
  @State searchHeight: number = 50
  private mHeight: number = 50
  private scrollGestureRecognizer?: GestureRecognizer

  aboutToAppear(): void {
    for (let i = 0; i < 20; i++) {
      this.list.push(i.toString())
    }
  }

  build() {
    Stack() {
      Scroll() {
        Column({ space: 15 }) {
          Column({ space: 10 }) {
            Text('List')
              .fontSize(25)
              .fontColor(Color.Black)
              .fontWeight(FontWeight.Bold)
            Stack()
              .width('90%')
              .height(this.searchHeight)
              .backgroundColor('#999999')
              .borderRadius(10)
              .animation({ duration: 150 })
          }
          .width('100%')
          .alignItems(HorizontalAlign.Center)

          List() {
            ForEach(this.list, (item: string, index: number) => {
              ListItem() {
                Text(item)
                  .fontSize(15)
                  .fontColor(Color.Brown)
              }
              .width('100%')
              .height(80)
            })
          }
          .width('100%')
          .height('auto')
          .divider({ strokeWidth: 1, color: Color.Gray })
        }
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .height('100%')
      .id('scroll')
      .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer,
        others: Array<GestureRecognizer>) => {
        this.scrollGestureRecognizer = current
        if (this.searchHeight != 0) {
          this.scrollGestureRecognizer.setEnabled(false)
        }
        return GestureJudgeResult.CONTINUE
      })
      .onReachStart(() => {
        this.searchHeight = this.mHeight
      })
    }
    .parallelGesture( // 绑定一个Pan手势作为动态控制器
      PanGesture({ direction: PanDirection.Up | PanDirection.Down })
        .onActionUpdate((event: GestureEvent) => {
          if (this.isDown(event.offsetY)) {
            this.handleDown(event)
          }
        })
      , GestureMask.Normal)
    .onTouch((event) => {
      if (event.type == TouchType.Up) {
        if (this.searchHeight <= (this.mHeight / 2)) {
          this.searchHeight = 0
        } else {
          this.searchHeight = this.mHeight
        }
      }
    })
  }

  isDown(offset: number): boolean {
    return offset < 0
  }

  isFastSwipe(event: GestureEvent) {
    return Math.abs(event.velocityY) > 1200
  }

  handleDown(event: GestureEvent) {
    if (this.isFastSwipe(event)) {
      this.scrollGestureRecognizer?.setEnabled(true)
      this.searchHeight = 0
    } else {
      if (this.searchHeight > 0) {
        this.scrollGestureRecognizer?.setEnabled(false)
        this.searchHeight = this.mHeight - Math.abs(event.offsetY)
      } else {
        this.scrollGestureRecognizer?.setEnabled(true)
      }
    }
  }
}