鸿蒙开发第一篇,侧滑菜单实现

352 阅读3分钟

侧滑菜单是一种比常用的设置界面或配置界面的效果,在主界面左滑或者右滑出现设置界面效果,能方便的进行各种操作。很多优秀的应用都采用了这种界面方案,像文心一言、豆瓣、plex、Google+、网易新闻、知乎日报、有道云笔记等等。 以下是我用鸿蒙 arkTS 实现的侧滑菜单。

2023-12-19 23-20-38.2023-12-19 23_21_07.gif

侧滑菜单实现原理: 以左侧菜单为例,左侧菜单内容偏移宽度为菜单宽度,以实现菜单初始状态隐藏,内容区域监听滑动手势,根据滑动的速度以及滑动的距离判断展开或者折叠菜单.

实现代码如下 `

@Component
export struct DrawerContainer {
  showStyle: number = 1
  controller?: DrawerContainerController
  @BuilderParam ContentBuilder: () => void
  @BuilderParam MenuBuilder: () => void

  // onChange:(event: (index: number) => void)

  onChange: (open?: boolean) => void

  @State menuWidth: number = 300 // 菜单宽度
  @State menuOffsetLeft: number = 0 // 菜单左偏移量
  @State menuBgOpacity: number = 0 // 菜单背景透明度

  @State offsetLeft: number = 0 // 手指滑动距左偏移量
  @State pressX: number = 0 // 按下时X坐标
  @State lastX: number = 0 // 上次X坐标
  @State pressTime: number = 0 // 按下时的时间戳

  // 动画
  private options: AnimatorOptions = {
    duration: 300,
    easing: "friction",
    delay: 0,
    direction: "normal",
    iterations: 1,
    fill: 'forwards', // 启停模式:保留在动画结束状态
    begin: 0, // 起始值
    end: 0 // 结束值
  };
  animator = animator.create(this.options)

  aboutToAppear() {
    // 初始化控制器
    if (this.controller !== null) {
      this.controller.listener = {
        openMenu: () => {
          this.openMenu()
        },
        closeMenu: () => {
          this.closeMenu()
        }
      } as Listener
    }

    // showStyle 显示样式:0菜单在下面,1菜单在上面
    if (this.showStyle == 0) {
      this.offsetLeft = 0
      // 设置菜单偏移量为负的菜单宽度,为了解决z-index设置后,菜单界面到内容下面,
      // 事件还停留到内容上面,导致点击菜单区域,响应的还是菜单点击事件
      this.menuOffsetLeft = -this.menuWidth
    } else {
      this.offsetLeft = -this.menuWidth
      this.menuOffsetLeft = this.offsetLeft
    }
  }

  build() {
    Stack() {
      // 内容
      Stack() {
        this.ContentBuilder()
      }
      .width('100%')
      .height('100%')
      .offset({ x: this.menuWidth + this.menuOffsetLeft, y: 0 })

      // 背景
      if (this.menuBgOpacity != 0) {
        Stack() {
          Text(this.menuBgOpacity.toFixed(2)).fontSize(44)
        }
        .onClick(() => {
          this.closeMenu()
        })
        .alignContent(Alignment.End)
        .opacity(this.menuBgOpacity)
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Black)
      }

      // 菜单
      Stack() {
        this.MenuBuilder()
      }
      .width(this.menuWidth)
      .height('100%')
      .offset({ x: this.menuOffsetLeft, y: 0 })

      // 测试内容
      Column() {
        Text('menuBgOpacity' + this.menuBgOpacity.toFixed(2)).fontSize(22).onClick(() => {
          promptAction.showToast({ message: 'adsf' })
        })

        Text('offsetLeft' + this.offsetLeft.toFixed(2)).fontSize(22).onClick(() => {
          promptAction.showToast({ message: 'adsf' })
        })

        Text('menuOffsetLeft' + this.menuOffsetLeft.toFixed(2)).fontSize(22).onClick(() => {
          promptAction.showToast({ message: 'adsf' })
        })
      }.backgroundColor(Color.White)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Start)
    .gesture(
      // 以下组合手势为顺序识别,当长按手势事件未正常触发时则不会触发拖动手势事件
      GestureGroup(GestureMode.Sequence,
        PanGesture()
          .onActionStart((event) => {
            // 记录首次按下的x坐标
            this.pressX = event.offsetX
            this.lastX = this.pressX
            // 记录首次按下的时间戳
            this.pressTime = event.timestamp
            // console.error('pan start tiltX' + event.tiltX)
            console.error('onActionStart offsetX' + event.offsetX)
            // console.error('pan start pinchCenterX' + event.pinchCenterX)
          })
          .onActionUpdate((event: GestureEvent) => {
            console.error('onActionUpdate offsetX' + event.offsetX)
            // 当前x坐标
            let localX = event.offsetX
            // 计算与上次的x坐标的偏移量
            let offsetX = this.lastX - localX;
            // 记录上次的x坐标
            this.lastX = localX
            // 累计偏移量
            this.offsetLeft -= offsetX

            // showStyle 显示样式:0菜单在下面,1菜单在上面
            if (this.showStyle == 0) {
              // 设置偏移量的范围
              if (this.offsetLeft < 0) {
                this.offsetLeft = 0
              } else if (this.offsetLeft > this.menuWidth) {
                this.offsetLeft = this.menuWidth
              }
            } else {
              // 设置偏移量的范围
              if (this.offsetLeft > 0) {
                this.offsetLeft = 0
              } else if (this.offsetLeft < -this.menuWidth) {
                this.offsetLeft = -this.menuWidth
              }
            }

            this.changeMenuOffsetLeft()
          })
          .onActionEnd((event) => {
            // let offsetX = this.pressX - this.lastX;
            let offsetX = this.pressX - event.offsetX
            if (offsetX == 0) {
              return
            }
            // 滑向的x坐标
            var toX = 0
            // 快速滑动
            if (event.timestamp - this.pressTime < 300) {
              if (Math.abs(offsetX) > 10) {
                // showStyle 显示样式:0菜单在下面,1菜单在上面
                if (this.showStyle == 0) {
                  if (offsetX > 0) {
                    toX = 0
                  } else {
                    toX = this.menuWidth
                  }
                } else {
                  if (offsetX > 0) {
                    toX = -this.menuWidth
                  } else {
                    toX = 0
                  }
                }
              }
            } else {
              // showStyle 显示样式:0菜单在下面,1菜单在上面
              if (this.showStyle == 0) {
                // 当移动偏移量大于菜单一半宽度,完全打开菜单,否则反之
                if (this.offsetLeft > this.menuWidth / 2) {
                  toX = this.menuWidth
                } else {
                  toX = 0
                }
              } else {
                if (this.offsetLeft > -this.menuWidth / 2) {
                  toX = 0
                } else {
                  toX = -this.menuWidth
                }
              }
            }
            // 开启动画
            this.startAnimator(toX)
          })
      )
        .onCancel(() => {
          console.error('sequence gesture canceled')
        })
    )
  }

  /**
   * 改变菜单偏移量
   */
  changeMenuOffsetLeft() {
    // showStyle 显示样式:0菜单在下面,1菜单在上面
    if (this.showStyle == 0) {
      console.log('1111')
      // 设置菜单偏移量
      if (this.offsetLeft == 0) {
        console.log('0000')
        this.menuOffsetLeft = -this.menuWidth
      } else {
        console.log('3333')
        this.menuOffsetLeft = -this.menuWidth + this.offsetLeft
        // 改变菜单背景的透明度
        this.menuBgOpacity = (1 - Math.abs(this.offsetLeft / this.menuWidth)) / 3
      }
    } else {
      console.log('22222')
      // 设置菜单偏移量
      this.menuOffsetLeft = this.offsetLeft
      // 改变菜单背景的透明度
      this.menuBgOpacity = (1 - Math.abs(this.offsetLeft / this.menuWidth)) / 3
    }
    console.log('设置菜单偏移量', this.menuOffsetLeft)
  }

  /**
   * 开启动画
   */
  startAnimator(toX) {
    let options: AnimatorOptions = {
      duration: 300,
      easing: "friction",
      delay: 0,
      direction: "normal",
      iterations: 1,
      fill: 'forwards', // 启停模式:保留在动画结束状态
      begin: this.offsetLeft, // 起始值
      end: toX // 结束值
    };
    // animator.create(options);
    // 更新动画参数
    this.animator.reset(options)
    // 监听动画值变化事件
    this.animator.onframe = (value) => {
      console.log('动画', value /**/
      )
      this.offsetLeft = value
      this.changeMenuOffsetLeft()
    }

    // 开启动画
    this.animator.play()
  }

  /**
   * 打开菜单
   */
  openMenu() {
    // let offsetX = this.pressX - this.lastX;
    // if (offsetX == 0) {
    // 如果菜单关闭,则开启
    if (this.showStyle == 0) {
      if (this.offsetLeft == 0) {
        this.startAnimator(this.menuWidth);
      }
    } else {
      if (this.offsetLeft == -this.menuWidth) {
        this.startAnimator(0);
      }
    }
    // }
  }
  /**
   * 关闭菜单
   */
  closeMenu() {
    // let offsetX = this.pressX - this.lastX;
    // if (offsetX == 0) {
    // 如果菜单开启,则关闭
    if (this.showStyle == 0) {
      if (this.offsetLeft == this.menuWidth) {
        this.startAnimator(0);
      }
    } else {
      if (this.offsetLeft == 0) {
        this.startAnimator(-this.menuWidth);
      }
    }
    // }
  }
}

`