鸿蒙Harmony-自定义List组件,解决List组件手势滑动点击卡住问题

一,背景

在OpenHarmony 4.0系统中,可能会存在这样的问题,带滑动的系统组件包括List,Grid,Scrool,Tabs等等,在长时间滑动或多指同时剧烈滑动时,有概率出现该组件所有的触控失效的问题,表现为该组件卡死,但是点击其他组件仍可以正常显示。

二,问题分析

以List组件为例分析,问题发生时,List组件的滑动和点击全部都失效了,但是重写List的onTouch方法时,发现onTouch方法中仍然有日志打印,且在onTouch方法中设置滑动不生效,可见List组件的滑动并不是走的onTouch方法,而是走的手势gesture。

为了复现此问题,又用Scrool,Tabs,Grid等组件尝试复现,发现都存在此问题。

三,解决方案

当我们禁用List的手势时,代码如下:

TypeScript 复制代码
@Entry
@Component
struct TestPage {

  @State list:Array<string> =new Array

  aboutToAppear(){
    this.list = ["111","222","333","444","555"
      ,"666","777","888","999","1010","1111",
      "1212","1313","1414","1515","1616",
      "1717","1818","1919","2020"]
  }

  build() {
    List() {
      ForEach(this.list, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
            .onClick(() => {
              console.info("yz----点击了"+item)
            })
        }
      })
    }
    .enableScrollInteraction(false)//关闭滚动手势
    .width('100%')
    .height('100%')
  }
}

用多指复现未复现到卡死问题。因此,可以通过禁用手势滑动,重写onTouch方法,自定义滑动事件,来避免此问题。

四,重写滑动事件

首先我们先重写onTouch事件,代码如下:

TypeScript 复制代码
@Entry
@Component
struct TestPage {

  @State list:Array<string> =new Array

  aboutToAppear(){
    this.list = ["111","222","333","444","555"
      ,"666","777","888","999","1010","1111",
      "1212","1313","1414","1515","1616",
      "1717","1818","1919","2020"]
  }

  build() {
    List() {
      ForEach(this.list, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
            .onClick(() => {
              console.info("yz----点击了"+item)
            })
        }
      })
    }
    .enableScrollInteraction(false)//关闭滚动手势
    .width('100%')
    .height('100%')
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down: //按下
          break
        case TouchType.Move: //移动
          break
        case TouchType.Up: //抬起
          break
      }
    })
  }
}

我们先重写一跟手指的滑动事件,屏蔽掉多个手指的滑动,写完一个手指的再写多个手指的。

因此要在按下的时候记录此次触摸的手指个数,并且在按下移动和抬起的时候根据此次按下的手指的个数,屏蔽掉多个手指的触摸:

TypeScript 复制代码
@Entry
@Component
struct TestPage {

  @State list:Array<string> =new Array

  //当前按下的手指数量
  @State currentFingers: number = 0

  aboutToAppear(){
    this.list = ["111","222","333","444","555"
      ,"666","777","888","999","1010","1111",
      "1212","1313","1414","1515","1616",
      "1717","1818","1919","2020"]
  }

  build() {
    List() {
      ForEach(this.list, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
            .onClick(() => {
              console.info("yz----点击了"+item)
            })
        }
      })
    }
    .enableScrollInteraction(false)//关闭滚动手势
    .width('100%')
    .height('100%')
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 1){
            console.debug("yz-----有多个手指按下,忽略此事件")
            return
          }
          break
        case TouchType.Move: //移动
          if(this.currentFingers > 1){
            console.debug("yz-----有多个手指移动,忽略此事件")
            return
          }
          break
        case TouchType.Up: //抬起
          if(this.currentFingers > 1){
            console.debug("yz-----有多个手指移动,忽略此事件")
            return
          }
          break
      }
    })
  }
}

然后定义两个变量startX,startY,记录按下时的起始位置:

TypeScript 复制代码
@Entry
@Component
struct TestPage {

  @State list:Array<string> =new Array

  //当前按下的手指数量
  @State currentFingers: number = 0

  //手指触摸X起始位置
  @State startX: number = 0
  //手指触摸Y起始位置
  @State startY: number = 0

  aboutToAppear(){
    this.list = ["111","222","333","444","555"
      ,"666","777","888","999","1010","1111",
      "1212","1313","1414","1515","1616",
      "1717","1818","1919","2020"]
  }

  build() {
    List() {
      ForEach(this.list, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
            .onClick(() => {
              console.info("yz----点击了"+item)
            })
        }
      })
    }
    .enableScrollInteraction(false)//关闭滚动手势
    .width('100%')
    .height('100%')
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指按下,忽略此事件")
            return
          }
          // 记录起始位置
          this.startX = event.touches[0].windowX
          this.startY = event.touches[0].windowY
          break
        case TouchType.Move: //移动
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
          break
        case TouchType.Up: //抬起
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
          break
      }
    })
  }
}

因为要涉及到移动,所以我们需要新建一个Scroller去绑定List组件,来控制滑动,获取偏移距离等:

TypeScript 复制代码
scroller: Scroller = new Scroller()

....

List({ scroller: this.scroller}) {
     ...
}

这里我们先控制垂直方向的滑动,所以我们在按下的时候要先记录一下Y轴当前的偏移量,什么是偏移量,下面我画个图解释一下:

通过上面的图片我们可以看出,当我们向上滑动List组件的时候,向上超出List组件Y轴起始坐标的部分就是Y轴的偏移量。

我们用scrollY来表示目前List组件的Y轴的偏移量,当手指按下的时候,记录一下当前的偏移量。

TypeScript 复制代码
//上次偏移量
@State scrollY: number = 0


.....

.onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指按下,忽略此事件")
            return
          }
          // 记录起始位置
          this.startX = event.touches[0].windowX
          this.startY = event.touches[0].windowY
          // 记录当前偏移量
          this.scrollY =this.scroller.currentOffset().yOffset
          break

我们还要记录一下手指最后一次触摸屏幕时,Y轴的位置,方便来计算移动距离,当手指抬起时,清空距离。

TypeScript 复制代码
 //上次触摸Y位置
@State lastY: number = 0

...

.onTouch((event: TouchEvent) => {
  ....

  case TouchType.Move: //移动
     ...
     this.lastY =event.touches[0].windowY
     break
 case TouchType.Up: //抬起
   ...
  this.lastY =0
  break
  

下面我们还要再定义几个变量:

TypeScript 复制代码
//移动的距离
@State moveY: number = 0
//阈值
THRESHOLD: number = 10;
//动画时间
@State animationTime: number = 50

moveY其实就是实际我们要设置给scrooller的目标偏移量

THRESHOLD就是一个阈值,控制滑动多大才算我们的滑动生效

animationTime是给scrooller设置的滑动的动画时间,也是为了后面滑动看起来更自然有惯性做准备,现在我们只是给他设置一个10的默认值即可

下面就是核心的移动的代码了:

TypeScript 复制代码
...

case TouchType.Move: //移动
  if(this.currentFingers > 1){
    console.info("yz-----有多个手指移动,忽略此事件")
    return
  }
  let currentY = event.touches[0].windowY;
  //计算移动距离
  const deltaX = event.touches[0].windowX - this.startX
  const deltaY = currentY - this.startY
  const absX = Math.abs(deltaX)
  const absY = Math.abs(deltaY)
  const changeY =currentY -this.lastY
  const absChangeY = Math.abs(changeY)
  if(absY >= absX && absY > this.THRESHOLD){
    //计算滑动距离
    this.moveY =this.scrollY-deltaY+absChangeY
    console.debug("yh------滑动距离:"+this.moveY)
    //滑动
    this.scroller.scrollTo({
      xOffset: 0,
      yOffset:this.moveY,
      animation:{duration:this.animationTime,curve:Curve.Smooth}
    })
    //更新最后一次移动的Y轴坐标 方便计算下一次移动进来的距离计算
    this.lastY =event.touches[0].windowY
  }
  break

....

首先计算出x,y轴相比于按下的点滑动的距离deltaX和deltaY,并记录一下移动的绝对值absX和absY

然后计算一下这次移动的Y轴坐标相比于上一次移动的Y轴坐标的距离changeY

然后纵向移动的条件就是absY >= absX && absY > this.THRESHOLD

符合条件的情况下,计算需要移动到的目标偏移量:this.moveY =this.scrollY-deltaY+absChangeY

加上absChangeY是为了让滑动距离更大一点。

调用

TypeScript 复制代码
this.scroller.scrollTo({
      xOffset: 0,
      yOffset:this.moveY,
      animation:{duration:this.animationTime,curve:Curve.Smooth}
    })

进行滑动。

到这里一个简单的一指上下滑动就完事了。

下面贴出这部分的完整代码:

TypeScript 复制代码
@Entry
@Component
struct TestPage {

  @State list:Array<string> =new Array

  //当前按下的手指数量
  @State currentFingers: number = 0

  //手指触摸X起始位置
  @State startX: number = 0
  //手指触摸Y起始位置
  @State startY: number = 0

  scroller: Scroller = new Scroller()

  //上次偏移量
  @State scrollY: number = 0

  //上次触摸Y位置
  @State lastY: number = 0

  //移动的距离
  @State moveY: number = 0

  //阈值
  THRESHOLD: number = 10;

  //动画时间
  @State animationTime: number = 50

  aboutToAppear(){
    this.list = ["111","222","333","444","555"
      ,"666","777","888","999","1010","1111",
      "1212","1313","1414","1515","1616",
      "1717","1818","1919","2020"]
  }

  build() {
    List({ scroller: this.scroller}) {
      ForEach(this.list, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
            .onClick(() => {
              console.info("yz----点击了"+item)
            })
        }
      })
    }
    .enableScrollInteraction(false)//关闭滚动手势
    .width('100%')
    .height('100%')
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指按下,忽略此事件")
            return
          }
          // 记录起始位置
          this.startX = event.touches[0].windowX
          this.startY = event.touches[0].windowY
          // 记录当前偏移量
          this.scrollY =this.scroller.currentOffset().yOffset
          break
        case TouchType.Move: //移动
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
          let currentY = event.touches[0].windowY;
          //计算移动距离
          const deltaX = event.touches[0].windowX - this.startX
          const deltaY = currentY - this.startY
          const absX = Math.abs(deltaX)
          const absY = Math.abs(deltaY)
          const changeY =currentY -this.lastY
          const absChangeY = Math.abs(changeY)

          if(absY >= absX && absY > this.THRESHOLD){
            //计算滑动距离
            this.moveY =this.scrollY-deltaY+absChangeY
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset:this.moveY,
              animation:{duration:this.animationTime,curve:Curve.Smooth}
            })

            //更新最后一次移动的Y轴坐标 方便计算下一次移动进来的距离计算
            this.lastY =event.touches[0].windowY
          }
          break
        case TouchType.Up: //抬起
          if(this.currentFingers > 1){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
          this.lastY =0
          break
      }
    })
  }
}

五,增加惯性滑动

上面的代码我们虽然完成了List的滑动,但是用起来却非常的别扭,因为我们缺少了在抬起手指时的惯性滑动,下面我们就来解决一下。

关于抬起时的惯性滑动,我们要分三种情况来分析。

第一种就是从手指按下到抬起的时间很短但是有滑动,这种情况我们就认为是扫动,一般这种情况会连续多次去扫动屏幕,这种情况必须要给个惯性滑动

第二种就是从手指按下到抬起额时间很长,但是抬起时的速度很快,我们就认为是滑动了一段距离,但是抬起那一下猛地向上一滑,这种情况也必须要给个惯性滑动

第三种就是从手指按下到抬起额时间很长,而且抬起时的速度很慢,这种情况就是正常的拖动,不能给惯性

5.1 快速扫动增加惯性

上面提到了,这种情况下的惯性滑动,就是从手指按下到抬起的时间很短但是有滑动,这个时间我通过测量定义为300ms比较合适。

为了计算从按下到抬起的总时间,我们记录一个手指按下的时间戳,并计算时间从按下到抬起的总时间,小于300ms时,就认为是第一种情况:

TypeScript 复制代码
...

//手指按下的时间戳
@State startTime: number = 0

...

case TouchType.Down: //按下

  ...

 // 记录起始时间
  this.startTime = new Date().getTime()
  break
case TouchType.Up: //抬起
  if(this.currentFingers > 1){
    console.info("yz-----有多个手指移动,忽略此事件")
    return
  }
  //记录抬起时间戳
  const nowTime = new Date().getTime()
  //计算从按下到抬起的时间间隔
  const totalDuration = nowTime - this.startTime;
  //总时间小于300ms,认为是手指的扫动
  if(totalDuration < 300 ){
  }
  this.lastY =0
  break

这种情况下的移动和手指触摸的移动是差不多,但是因为是惯性,我们要将动画时间设置的长一点,我这里是800ms,也可以根据自己的需求更改。

滑动的距离也要根据滑动方向来分开计算,因为向下滑动时,滑动偏移量的最小值是0,向上滑动时,偏移量的最大值就得是根据具体的item大小来计算了。

下面我们先实现向下滑动的惯性:

TypeScript 复制代码
...

case TouchType.Up: //抬起
  ...

  //计算滑动方向和滑动距离
const deltaX = event.touches[0].windowX - this.startX
const deltaY = event.touches[0].windowY - this.startY
const absX = Math.abs(deltaX)
const absY = Math.abs(deltaY)
const changeY =event.touches[0].windowY -this.startY
const absChangeY = Math.abs(changeY)
//惯性滑动时,动画时间设置为800ms
this.animationTime =800
//移动方向为垂直移动 且移动距离大于阈值 则认为是上下滑动
if(absY >= absX && absY > this.THRESHOLD){
  //上下滑动
  let distince = 0
  //根据测量得到的比较合适的滑动距离
  if(absChangeY <150){
    distince =3*absChangeY
  }else {
    distince =300
  }
  if(changeY > 0) {
    //向下滑动 边界为0
    let result =this.moveY-deltaY+(absChangeY-distince)
    //边界检查
    if(result <0){
      result =0
    }
    //滑动
    this.scroller.scrollTo({
      xOffset: 0,
      yOffset:result,
      animation:{duration:this.animationTime,curve:Curve.Ease}
    })
  }else {
    //向上滑动
  }

由于向上滑动需要计算出List的总高度以及每个item的高度及List的item之间的间隔等距离,所以这里我们先空一下,等到要封装List组件的时候,再把这块加上。

5.2 缓慢滑动,但是抬手比较快,增加惯性

要判断抬手比较快,就要计算抬手时的速度。

要计算抬手时的速度,就得在移动的时候,根据上一次的时间和移动距离实时计算,因为在抬起时,我们是无法计算的。

所以先定义几个变量:

TypeScript 复制代码
// 上一次移动事件的垂直距离(相对于上一次移动的位置)
@State lastMoveY: number = 0
@State lastMoveDeltaY: number = 0
@State lastMoveInterval: number = 0

然后在移动时计算出与上一次移动的位移差和时间差:

TypeScript 复制代码
。。。
 

case TouchType.Move: //移动

。。。

   // 计算与上一次移动的位移差和时间差,用于计算速度
   this.lastMoveDeltaY = currentY - this.lastMoveY;
   this.lastMoveInterval = currentTime - this.lastMoveTime;
   this.lastMoveY = currentY;
   this.lastMoveTime = currentTime;
。。。

 case TouchType.Up: //抬起

   。。。
   
   //计算从按下到抬起的时间间隔
  const totalDuration = nowTime - this.startTime;
  // 计算抬起前的瞬时速度
  let flickSpeed = 0;
  if (this.lastMoveInterval > 0) {
   flickSpeed = this.lastMoveDeltaY / this.lastMoveInterval;
  }
  
  。。。

  if(totalDuration < 300 ){
   ....
   }else  if(totalDuration >= 500 && Math.abs(flickSpeed)>0.1){
 //缓慢滑动 但是抬手比较快 需要给个惯性 看着舒服点
 const currentScrollY :number= this.scroller.currentOffset().yOffset;
 // 根据速度计算惯性滑动距离(速度越大距离越长) 500为惯性系数
 const flickDistance =  flickSpeed * 500;
 // 计算滚动距离
 let targetY = 0;
 const changeY =event.touches[0].windowY -this.startY
 if(changeY>0){
   //向下滑动 边界为0
   targetY = currentScrollY -  Math.abs(flickDistance);
   if(targetY <0){
     targetY =0
   }
 }else {
   //向上滑动
 }
 //惯性滑动 将动画时间设置为800ms
 this.animationTime = 800;
 //滑动
 this.scroller.scrollTo({
   xOffset: 0,
   yOffset: targetY,
   animation: { duration: this.animationTime, curve: Curve.Ease }
 });
}
 ...
 

  
   

还是先省略向上滑动

六,增加两指操作

要进行两指操作,首先要放开两指:

TypeScript 复制代码
。。。

case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 2){
            console.info("yz-----有多个手指按下,忽略此事件")
            return
          }
。。。。
case TouchType.Move: //移动
          if(this.currentFingers > 2){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
。。。。
if(this.currentFingers > 2){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
。。。。

这样写完了之后运行会发现,这样会有一个问题,就是当我们两个手指并不是同时落下时,就会有跳动。

为什么会出现这样的情况呢?

比如说我食指先触摸的屏幕,然后中指又触摸的屏幕,食指和中指之间是有一段距离的,离开时我如果是食指先离开的屏幕,中指还在屏幕上,此时我们获取到的点的坐标就是中指的坐标,因为他们两之间有一段距离,所以就会出现跳动的情况。

要解决这种情况,我们就要知道只剩一个手指的时候,在移动的时候就要把两指之间的距离差补齐,这样就不会出现跳动了。

首先我们要知道什么时候只剩一个手指了,所以定义一个变量:

TypeScript 复制代码
@State isOneFingerLeave: boolean = false

在按下时将他初始化为false,在抬起只剩一个手指时,将他置为true

TypeScript 复制代码
。。。
case TouchType.Down: //按下
     。。。
      this.isOneFingerLeave = false
     。。。

case TouchType.Up: //抬起
   。。。
  if(event.touches.length == 2){
            this.isOneFingerLeave = true
            console.debug("yz-----抬起一个手指,还剩一个手指,忽略此事件")
            return
          }
   。。。。

然后定义两个变量,来分别表示两个手指之间的距离和上个手指触摸的坐标

TypeScript 复制代码
@State twoFingerDistance: number = 0
@State lastCurrentY: number = 0

按下时将twoFingerDistance置为0,在移动时通过是否还剩一跟手指触摸来计算距离:

TypeScript 复制代码
...
case TouchType.Move:
          if(this.currentFingers > 2){
            console.debug("yz-----有多个手指移动,忽略此事件")
            return
          }
          let currentY = event.touches[0].windowY;
          if(this.isOneFingerLeave){
            console.debug("yz----两个手指只剩一个手指了,currentY:"+currentY+" lastCurrentY:"+this.lastCurrentY)
            if(Math.abs(currentY - this.lastCurrentY) < 5){
              console.debug("yz----两个手指只剩一个手指了,但是该手指是先触摸屏幕的手指,忽略此事件")
            }else {
              if(this.twoFingerDistance ==0){
                this.twoFingerDistance =this.lastCurrentY -currentY
              }
              currentY = event.touches[0].windowY+this.twoFingerDistance
            }
          }else {
            this.lastCurrentY = currentY
          }
...

这样就解决了跳动的问题。

但是还有一个问题,就是如果两个手指不是同时落下的,Down事件就会走两次,这样初始化操作也会走两次,所以我们要屏蔽掉后面的down事件:

TypeScript 复制代码
//是否按下了
  @State isDownStart: boolean = false

。。。

case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 2){
            console.info("yz-----有多个手指按下,忽略此事件")
            return
          }

          if(this.isDownStart){
            console.debug("yh-----已经按下,忽略此事件")
            return
          }
          this.isDownStart = true
。。。

case TouchType.Up:
。。。

  this.isDownStart = false
。。。

这样就解决了两个手指按下的问题。

但是经过测试发现还有一个问题,就是当我们第一次滑动的时候,他会先滑动到之前的一个位置,然后再在下次滑动的时候跳转到手指的位置继续滑动,要解决这个问题,就要在第一次滑动的时候首先滑动到当前偏移量的位置,这样就不会出现重复滑动的问题了。

我们定义一个变量来判断是否是第一次滑动:

TypeScript 复制代码
  //是否正在移动中
  @State isMoving: boolean = false

然后在移动中增加判断:

TypeScript 复制代码
case TouchType.Move: //移动
  ...
  if(absY >= absX && absY > this.THRESHOLD){
            if(this.lastY ==0 && !this.isMoving ){
              //第一次移动时,滑动到当前偏移量位置,防止重复滑动
              console.debug("yh-----第一次进来:scrollY:"+ this.scrollY)
              this.scroller.scrollTo({
                xOffset: 0,
                yOffset:this.scrollY
              })
              this.lastY =event.touches[0].windowY
              return
            }
            //记录正在滑动中的状态,判断是否需要进入第一次移动
            this.isMoving =true
            //计算滑动距离
            this.moveY =this.scrollY-deltaY+absChangeY
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset:this.moveY,
              animation:{duration:this.animationTime,curve:Curve.Smooth}
            })

            //更新最后一次移动的Y轴坐标 方便计算下一次移动进来的距离计算
            this.lastY =event.touches[0].windowY
          }
          break
case TouchType.Up: //抬起
   ...
   this.isMoving =false
   break

至此,两指操作就完成了。

七,List组件的封装

我们虽然完成了对于List组件的滑动事件的重写,但是总不能任何使用的地方都要写这几百行代码吧,所以我们需要把这些代码封装成一个组件,外部只需要几行代码即可调用。

首先我们要自定义一个组件,YZListView:

TypeScript 复制代码
@Component
export default struct YZListView {
  
  build(){
    
  }
  
}

然后将我们重新的内容移到YZListView里面:

TypeScript 复制代码
@Component
export default struct YZListView {

  @State list:Array<string> =new Array

  //当前按下的手指数量
  @State currentFingers: number = 0

  //手指触摸X起始位置
  @State startX: number = 0
  //手指触摸Y起始位置
  @State startY: number = 0

  scroller: Scroller = new Scroller()

  //上次偏移量
  @State scrollY: number = 0

  //上次触摸Y位置
  @State lastY: number = 0

  //移动的距离
  @State moveY: number = 0

  //阈值
  THRESHOLD: number = 10;

  //动画时间
  @State animationTime: number = 50

  //手指按下的时间戳
  @State startTime: number = 0

  // 上一次移动事件的时间戳
  @State lastMoveTime: number = 0
  // 上一次移动事件的垂直距离(相对于上一次移动的位置)
  @State lastMoveY: number = 0

  @State lastMoveDeltaY: number = 0

  @State lastMoveInterval: number = 0

  @State isOneFingerLeave: boolean = false

  @State twoFingerDistance: number = 0

  @State lastCurrentY: number = 0

  //是否按下了
  @State isDownStart: boolean = false

  //是否正在移动中
  @State isMoving: boolean = false

  build(){
    List({ scroller: this.scroller}) {
      ForEach(this.list, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
            .onClick(() => {
              console.info("yz----点击了"+item)
            })
        }
      })
    }
    .enableScrollInteraction(false)//关闭滚动手势
    .width('100%')
    .height('100%')
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down: //按下
          this.currentFingers = event.touches.length
          if(this.currentFingers > 2){
            console.info("yz-----有多个手指按下,忽略此事件")
            return
          }

          if(this.isDownStart){
            console.debug("yh-----已经按下,忽略此事件")
            return
          }
          this.isDownStart = true
        // 记录起始位置
          this.startX = event.touches[0].windowX
          this.startY = event.touches[0].windowY
        // 记录当前偏移量
          this.scrollY =this.scroller.currentOffset().yOffset
          this.isOneFingerLeave = false
          this.twoFingerDistance = 0
        // 记录起始时间
          this.startTime = new Date().getTime()
          break
        case TouchType.Move: //移动
          if(this.currentFingers > 2){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
          let currentY = event.touches[0].windowY;
          if(this.isOneFingerLeave){
            console.debug("yh----两个手指只剩一个手指了,currentY:"+currentY+" lastCurrentY:"+this.lastCurrentY)
            if(Math.abs(currentY - this.lastCurrentY) < 5){
              console.debug("yh----两个手指只剩一个手指了,但是该手指是先触摸屏幕的手指,忽略此事件")
            }else {
              if(this.twoFingerDistance ==0){
                this.twoFingerDistance =this.lastCurrentY -currentY
              }
              currentY = event.touches[0].windowY+this.twoFingerDistance
            }
          }else {
            this.lastCurrentY = currentY
          }
          const currentTime = new Date().getTime();

        // 计算与上一次移动的位移差和时间差,用于计算速度
          this.lastMoveDeltaY = currentY - this.lastMoveY;
          this.lastMoveInterval = currentTime - this.lastMoveTime;
          this.lastMoveY = currentY;
          this.lastMoveTime = currentTime;

        //计算移动距离
          const deltaX = event.touches[0].windowX - this.startX
          const deltaY = currentY - this.startY
          const absX = Math.abs(deltaX)
          const absY = Math.abs(deltaY)
          const changeY =currentY -this.lastY
          const absChangeY = Math.abs(changeY)

          if(absY >= absX && absY > this.THRESHOLD){
            if(this.lastY ==0 && !this.isMoving ){
              //第一次移动时,滑动到当前偏移量位置,防止重复滑动
              console.debug("yh-----第一次进来:scrollY:"+ this.scrollY)
              this.scroller.scrollTo({
                xOffset: 0,
                yOffset:this.scrollY
              })
              this.lastY =event.touches[0].windowY
              return
            }
            //记录正在滑动中的状态,判断是否需要进入第一次移动
            this.isMoving =true
            //计算滑动距离
            this.moveY =this.scrollY-deltaY+absChangeY
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset:this.moveY,
              animation:{duration:this.animationTime,curve:Curve.Smooth}
            })

            //更新最后一次移动的Y轴坐标 方便计算下一次移动进来的距离计算
            this.lastY =event.touches[0].windowY
          }
          break
        case TouchType.Up: //抬起
          if(this.currentFingers > 2){
            console.info("yz-----有多个手指移动,忽略此事件")
            return
          }
          if(event.touches.length == 2){
            this.isOneFingerLeave = true
            console.debug("yh-----抬起一个手指,还剩一个手指,忽略此事件")
            return
          }
        //记录抬起时间戳
          const nowTime = new Date().getTime()
        //计算从按下到抬起的时间间隔
          const totalDuration = nowTime - this.startTime;
        // 计算抬起前的瞬时速度
          let flickSpeed = 0;
          if (this.lastMoveInterval > 0) {
            flickSpeed = this.lastMoveDeltaY / this.lastMoveInterval;
          }
        //总时间小于300ms,认为是手指的扫动
          if(totalDuration < 300 ){
            //计算滑动方向和滑动距离
            const deltaX = event.touches[0].windowX - this.startX
            const deltaY = event.touches[0].windowY - this.startY
            const absX = Math.abs(deltaX)
            const absY = Math.abs(deltaY)
            const changeY =event.touches[0].windowY -this.startY
            const absChangeY = Math.abs(changeY)
            //惯性滑动时,动画时间设置为800ms
            this.animationTime =800
            //移动方向为垂直移动 且移动距离大于阈值 则认为是上下滑动
            if(absY >= absX && absY > this.THRESHOLD){
              //上下滑动
              let distince = 0
              //根据测量得到的比较合适的滑动距离
              if(absChangeY <150){
                distince =3*absChangeY
              }else {
                distince =300
              }
              if(changeY > 0) {
                //向下滑动 边界为0
                let result =this.moveY-deltaY+(absChangeY-distince)
                //边界检查
                if(result <0){
                  result =0
                }
                //滑动
                this.scroller.scrollTo({
                  xOffset: 0,
                  yOffset:result,
                  animation:{duration:this.animationTime,curve:Curve.Ease}
                })
              }else {
                //向上滑动

              }
            }
          }else  if(totalDuration >= 500 && Math.abs(flickSpeed)>0.1){
            //缓慢滑动 但是抬手比较快 需要给个惯性 看着舒服点
            const currentScrollY :number= this.scroller.currentOffset().yOffset;
            // 根据速度计算惯性滑动距离(速度越大距离越长) 500为惯性系数
            const flickDistance =  flickSpeed * 500;
            // 计算滚动距离
            let targetY = 0;
            const changeY =event.touches[0].windowY -this.startY
            if(changeY>0){
              //向下滑动 边界为0
              targetY = currentScrollY -  Math.abs(flickDistance);
              if(targetY <0){
                targetY =0
              }
            }else {
              //向上滑动

            }
            //惯性滑动 将动画时间设置为800ms
            this.animationTime = 800;
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset: targetY,
              animation: { duration: this.animationTime, curve: Curve.Ease }
            });
          }
          this.isDownStart = false
          this.lastY =0
          this.isMoving =false
          break
      }
    })
  }

  ceilDivision(x: number, n: number): number {
    if (x >= n) {
      return Math.ceil(x / n);
    } else {
      return Math.floor(x / n) + 1;
    }
  }

}

这里就会遇到几个比较棘手的问题,

1,我们的item怎么传进来?

2,@Component不能使用继承和泛型,数据类型如何传进来?

首先,要解决数据类型问题,我们可以直接把数据类型定义为Object :

TypeScript 复制代码
@Prop list:Array<Object> =new Array

然后,这样就可以定义item了:

TypeScript 复制代码
@BuilderParam itemBuilder: (item: Object,index: number) => void= this.customBuilder

@Builder customBuilder() {}
TypeScript 复制代码
List({ scroller: this.scroller}) {
  ForEach(this.list, (item: Object,index:number) => {
    ListItem() {
      this.itemBuilder(item,index) // 使用外部传入的渲染函数
    }
  })
}

然后我们再来解决上面遗留的问题,向上滑动的时候,无法计算底部的高度。

这里我们先定义几个变量:

TypeScript 复制代码
//内容总高度
  @State  contentHeight: number = 0
  //容器高度
  @State  containerHeight: number = 0
  //item 高度
  @State itemHeight: number = 0

然后在ListItem的onAreaChange方法中获取item的高度:

TypeScript 复制代码
List({ scroller: this.scroller,space: this.space }) {
      ForEach(this.list, (item: Object, index: number) => {
        ListItem() {
          this.itemBuilder(item,index) // 使用外部传入的渲染函数
        }.onAreaChange((_, area) => {
          if(this.itemHeight==0){
            this.itemHeight =area.height as number
          }
        })
      })
    }

在List的onAreaChange中获取容器总高度:

TypeScript 复制代码
    List({ scroller: this.scroller,space: this.space }) {
      ForEach(this.list, (item: Object, index: number) => {
        ListItem() {
          this.itemBuilder(item,index) // 使用外部传入的渲染函数
        }.onAreaChange((_, area) => {
          if(this.itemHeight==0){
            this.itemHeight =area.height as number
          }
        })
      })
    }
    .onAreaChange((_, area) => {
      this.containerHeight =area.height as number
    })

然后在向上滑动的时候计算总高度:

TypeScript 复制代码
 case TouchType.Up:
   。。。
    //总时间小于300ms,认为是手指的扫动
          if(totalDuration < 300 ){
                。。。
            else {
                //向上滑动
                let result =this.moveY-deltaY+(absChangeY+distince)
                //底部边界的计算 根据每行6个item来计算的
                //总内容的高度
                let count =this.ceilDivision(this.list.length,this.lanes)
                this.contentHeight =count*this.itemHeight+count*this.space
                let bottomOffSet =this.contentHeight -this.containerHeight
                if(bottomOffSet < 0){
                  //需要滑动
                  bottomOffSet =0
                }else {
                  bottomOffSet =bottomOffSet + this.paddingNumber*count
                }

                //边界检查
                if(result >bottomOffSet){
                  result =bottomOffSet
                }
                //滑动
                this.scroller.scrollTo({
                  xOffset: 0,
                  yOffset:result,
                  animation:{duration:this.animationTime,curve:Curve.Ease}
                })
              }
              
          }else if(totalDuration >= 500 && Math.abs(flickSpeed)>0.1){
             ...
             else {
              //向上滑动
              //y轴最大边界值
              targetY = currentScrollY + Math.abs(flickDistance);
              let count =this.ceilDivision(this.list.length,this.lanes)
              this.contentHeight =count*this.itemHeight+count*this.space
              let bottomOffSet =this.contentHeight -this.containerHeight
              if(bottomOffSet < 0){
                //需要滑动
                bottomOffSet =0
              }else {
                bottomOffSet =bottomOffSet + this.paddingNumber*count
              }

              if(targetY>bottomOffSet){
                targetY =bottomOffSet
              }
            }
            //惯性滑动 将动画时间设置为800ms
            this.animationTime = 800;
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset: targetY,
              animation: { duration: this.animationTime, curve: Curve.Ease }
            });

          }

除此之外,我们还要开放一些变量给外部,方便外部布局使用,下面是完整代码:

TypeScript 复制代码
@Component
export default struct YZListView {

  //---------------------------必需---------------------------------


  // 列表数据(必需)
  @Prop list:Array<Object> =new Array

  // 自定义列表项渲染函数(必需)
  @BuilderParam itemBuilder: (item: Object,index: number) => void= this.customBuilder


  //---------------------------可选----------------------------------

  // item点击事件(可选)
  onButtonClick ?:(itemBean:Object,index:number)=>void

  //每个item之间的间距(可选,默认0)
  @State paddingNumber:number = 0

  // 每行显示的列数(可选,默认1列)
  @Prop lanes: number = 1

  //是否显示滚动条(可选,默认显示)
  @State barShow:BarState =BarState.On

  //水平轴的对齐方式(可选,默认居中)
  @State horizontalAlign:ListItemAlign =ListItemAlign.Center


  //------------------------分割线属性--------------------------------
  //每行之间的间距(可选,默认为0)
  @State space: number = 0
  //分割线宽度 (可选 默认为0)
  @State strokeWidth:number = 0
  //分割线距离左侧距离(可选 默认为0)
  @State startMargin:number = 0
  //分割线距离右侧距离(可选 默认为0)
  @State endMargin:number = 0
  //分割线颜色(可选)
  @State color: string ='#ffe9f0f0'
  //------------------------分割线属性--------------------------------



  //-----------------------组件内部变量 不需要传进来---------------------

  onItemClick = (itemBean:Object,index:number)=>{
    this.onButtonClick && this.onButtonClick(itemBean,index)
  }

  @Builder customBuilder() {}

  scroller: Scroller = new Scroller()

  //手指触摸X起始位置
  @State startX: number = 0
  //手指触摸Y起始位置
  @State startY: number = 0
  //上次触摸Y位置
  @State lastY: number = 0
  //上次偏移量
  @State scrollY: number = 0
  //阈值
  THRESHOLD: number = 10;

  //手指按下的时间戳
  @State startTime: number = 0
  //动画时间
  @State animationTime: number = 50
  //移动的距离
  @State moveY: number = 0
  //移动时最后的时间
  @State moveTime: number = 0
  //是否正在移动中
  @State isMoving: boolean = false
  // 上一次移动事件的时间戳
  @State lastMoveTime: number = 0
  // 上一次移动事件的垂直距离(相对于上一次移动的位置)
  @State lastMoveY: number = 0

  @State lastMoveDeltaY: number = 0

  @State lastMoveInterval: number = 0
  //当前按下的手指数量
  @State currentFingers: number = 0
  //是否按下了
  @State isDownStart: boolean = false

  @State lastCurrentY: number = 0

  @State isOneFingerLeave: boolean = false

  @State twoFingerDistance: number = 0

  //内容总高度
  @State  contentHeight: number = 0
  //容器高度
  @State  containerHeight: number = 0
  //item 高度
  @State itemHeight: number = 0

  aboutToAppear(){
  }

  build(){
    List({ scroller: this.scroller,space: this.space }) {
      ForEach(this.list, (item: Object, index: number) => {
        ListItem() {
          this.itemBuilder(item,index) // 使用外部传入的渲染函数
        }.onAreaChange((_, area) => {
          if(this.itemHeight==0){
            this.itemHeight =area.height as number
          }
        })
      })
    }
    .onAreaChange((_, area) => {
      this.containerHeight =area.height as number
    })
    .id('listContainer')
    .lanes(this.lanes)
    .enableScrollInteraction(false)
    .padding(this.paddingNumber)
    .scrollBar(this.barShow)
    .listDirection(Axis.Vertical)
    .alignListItem(this.horizontalAlign)
    .divider({
      strokeWidth:this.strokeWidth,
      startMargin:this.startMargin,
      endMargin:this.endMargin,
      color: this.color
    })
    .width('100%')
    .height('100%')
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down:
          this.currentFingers = event.touches.length
          if(this.currentFingers > 2){
            console.debug("yz-----有多个手指按下,忽略此事件")
            return
          }
          if(this.isDownStart){
            console.debug("yz-----已经按下,忽略此事件")
            return
          }
          this.isDownStart = true
          this.isOneFingerLeave = false
          this.twoFingerDistance = 0
        // 记录起始位置
          this.startX = event.touches[0].windowX
          this.startY = event.touches[0].windowY
        // 记录起始时间
          this.startTime = new Date().getTime()
        // 记录当前偏移量
          this.scrollY =this.scroller.currentOffset().yOffset
          this.moveY = 0
        // 初始化最后一次移动时间
          this.lastMoveTime = this.startTime;
        // 初始化最后一次移动的Y坐标
          this.lastMoveY = this.startY;
          break
        case TouchType.Move:
          if(this.currentFingers > 2){
            console.debug("yz-----有多个手指移动,忽略此事件")
            return
          }
          let currentY = event.touches[0].windowY;
          if(this.isOneFingerLeave){
            console.debug("yz----两个手指只剩一个手指了,currentY:"+currentY+" lastCurrentY:"+this.lastCurrentY)
            if(Math.abs(currentY - this.lastCurrentY) < 5){
              console.debug("yz----两个手指只剩一个手指了,但是该手指是先触摸屏幕的手指,忽略此事件")
            }else {
              if(this.twoFingerDistance ==0){
                this.twoFingerDistance =this.lastCurrentY -currentY
              }
              currentY = event.touches[0].windowY+this.twoFingerDistance
            }
          }else {
            this.lastCurrentY = currentY
          }
          const currentTime = new Date().getTime();
        // 计算与上一次移动的位移差和时间差,用于计算速度
          this.lastMoveDeltaY = currentY - this.lastMoveY;
          this.lastMoveInterval = currentTime - this.lastMoveTime;
          this.lastMoveY = currentY;
          this.lastMoveTime = currentTime;
        //计算移动距离
          const deltaX = event.touches[0].windowX - this.startX
          const deltaY = currentY - this.startY
          const absX = Math.abs(deltaX)
          const absY = Math.abs(deltaY)
          const changeY =currentY -this.lastY
          const absChangeY = Math.abs(changeY)
        //手指拖动移动时,动画时间设置为10ms
          this.animationTime =10
        //移动方向为垂直移动 且移动距离大于阈值 则认为是上下滑动
          if(absY >= absX && absY > this.THRESHOLD){
            if(this.lastY ==0 && !this.isMoving ){
              //第一次移动时,滑动到当前偏移量位置,防止重复滑动
              console.debug("yz-----第一次进来:scrollY:"+ this.scrollY)
              this.scroller.scrollTo({
                xOffset: 0,
                yOffset:this.scrollY
              })
              this.lastY =event.touches[0].windowY
              return
            }
            //记录正在滑动中的状态,判断是否需要进入第一次移动
            this.isMoving =true
            //计算滑动距离
            this.moveY =this.scrollY-deltaY+absChangeY
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset:this.moveY,
              animation:{duration:this.animationTime,curve:Curve.Smooth}
            })
            //更新最后一次移动的时间
            this.moveTime =  new Date().getTime()
            //更新最后一次移动的Y轴坐标
            this.lastY =event.touches[0].windowY+this.twoFingerDistance
          }
          break
        case TouchType.Up:
          if(this.currentFingers > 2){
            console.debug("yz-----有多个手指抬起,忽略此事件")
            return
          }
          if(event.touches.length == 2){
            this.isOneFingerLeave = true
            console.debug("yz-----抬起一个手指,还剩一个手指,忽略此事件")
            return
          }
        //记录抬起时间戳
          const nowTime = new Date().getTime()
        //计算从按下到抬起的时间间隔
          const totalDuration = nowTime - this.startTime;
        // 计算抬起前的瞬时速度
          let flickSpeed = 0;
          if (this.lastMoveInterval > 0) {
            flickSpeed = this.lastMoveDeltaY / this.lastMoveInterval;
          }
        //总时间小于300ms,认为是手指的扫动
          if(totalDuration < 300 ){
            //计算滑动方向和滑动距离
            const deltaX = event.touches[0].windowX - this.startX
            const deltaY = event.touches[0].windowY - this.startY
            const absX = Math.abs(deltaX)
            const absY = Math.abs(deltaY)
            const changeY =event.touches[0].windowY -this.startY
            const absChangeY = Math.abs(changeY)
            //惯性滑动时,动画时间设置为800ms
            this.animationTime =800
            //移动方向为垂直移动 且移动距离大于阈值 则认为是上下滑动
            if(absY >= absX && absY > this.THRESHOLD){
              //上下滑动
              let distince = 0
              //根据测量得到的比较合适的滑动距离
              if(absChangeY <150){
                distince =3*absChangeY
              }else {
                distince =300
              }
              if(changeY > 0) {
                //向下滑动 边界为0
                let result =this.moveY-deltaY+(absChangeY-distince)
                //边界检查
                if(result <0){
                  result =0
                }
                //滑动
                this.scroller.scrollTo({
                  xOffset: 0,
                  yOffset:result,
                  animation:{duration:this.animationTime,curve:Curve.Ease}
                })
              }else {
                //向上滑动
                let result =this.moveY-deltaY+(absChangeY+distince)
                //底部边界的计算 根据每行6个item来计算的
                //总内容的高度
                let count =this.ceilDivision(this.list.length,this.lanes)
                this.contentHeight =count*this.itemHeight+count*this.space
                let bottomOffSet =this.contentHeight -this.containerHeight
                if(bottomOffSet < 0){
                  //需要滑动
                  bottomOffSet =0
                }else {
                  bottomOffSet =bottomOffSet + this.paddingNumber*count
                }

                //边界检查
                if(result >bottomOffSet){
                  result =bottomOffSet
                }
                //滑动
                this.scroller.scrollTo({
                  xOffset: 0,
                  yOffset:result,
                  animation:{duration:this.animationTime,curve:Curve.Ease}
                })
              }
            }
          }else if(totalDuration >= 500 && Math.abs(flickSpeed)>0.1){
            //缓慢滑动 但是抬手比较快 需要给个惯性 看着舒服点
            const currentScrollY :number= this.scroller.currentOffset().yOffset;
            // 根据速度计算惯性滑动距离(速度越大距离越长) 500为惯性系数
            const flickDistance =  flickSpeed * 500;
            // 计算滚动距离
            let targetY = 0;
            const changeY =event.touches[0].windowY -this.startY
            if(changeY>0){
              //向下滑动 边界为0
              targetY = currentScrollY -  Math.abs(flickDistance);
              if(targetY <0){
                targetY =0
              }
            }else {
              //向上滑动
              //y轴最大边界值
              targetY = currentScrollY + Math.abs(flickDistance);
              let count =this.ceilDivision(this.list.length,this.lanes)
              this.contentHeight =count*this.itemHeight+count*this.space
              let bottomOffSet =this.contentHeight -this.containerHeight
              if(bottomOffSet < 0){
                //需要滑动
                bottomOffSet =0
              }else {
                bottomOffSet =bottomOffSet + this.paddingNumber*count
              }

              if(targetY>bottomOffSet){
                targetY =bottomOffSet
              }
            }
            //惯性滑动 将动画时间设置为800ms
            this.animationTime = 800;
            //滑动
            this.scroller.scrollTo({
              xOffset: 0,
              yOffset: targetY,
              animation: { duration: this.animationTime, curve: Curve.Ease }
            });
          }
          this.isDownStart = false
          this.lastY =0
          this.isMoving =false
          break
      }

    })

  }

  ceilDivision(x: number, n: number): number {
    if (x >= n) {
      return Math.ceil(x / n);
    } else {
      return Math.floor(x / n) + 1;
    }
  }


}

如何使用呢?

首先定义 自己的item:

TypeScript 复制代码
@Component
export default struct  TestItem{

  @State item:string =""

  index: number = 0

  onButtonClick ?:(item:string,index:number)=>void

  build(){
    Text(this.item)
      .width("100%")
      .height(50)
      .onClick(()=>{
        this.onButtonClick && this.onButtonClick(this.item,this.index)
      })
  }
}

然后在page中使用:

TypeScript 复制代码
import TestItem from './TestItem'
import YZListView from './YZListView'

@Entry
@Component
struct TestPage {

  @State list:Array<string> =new Array


  onButtonClick = (name: Object, index: number) => {
    //点击列表item
    console.log("yz-----点击了列表:"+name)
  }

  @Builder toomListBuilder(item: Object,index: number) {
    TestItem({ item: item as string, onButtonClick: this.onButtonClick,index:index })
  }

  aboutToAppear(){
    this.list = ["111","222","333","444","555"
      ,"666","777","888","999","1010","1111",
      "1212","1313","1414","1515","1616",
      "1717","1818","1919","2020"]
  }

  build() {
    Column(){
      YZListView({
        onButtonClick: this.onButtonClick,
        list: this.list as Array<Object>,
        lanes: 1,
        itemBuilder: this.toomListBuilder,
        space: 20
      }).width('100%')
    }

  }


}

这样使用起来是不是就简便了很多,也避免了系统的手势滑动卡住问题。

相关推荐
长弓三石6 分钟前
鸿蒙网络编程系列59-仓颉版TLS回声服务器示例
harmonyos·鸿蒙·tls·仓颉
HarmonyOS小助手2 天前
【宝藏贴】HarmonyOS官方模板优秀案例 · 第1期:便捷生活-购物中心
harmonyos·鸿蒙·鸿蒙生态
HarmonyOS小助手6 天前
“秒开”时代,HarmonyOS预加载让应用启动快如闪电
harmonyos·鸿蒙·鸿蒙生态
_waylau7 天前
跟老卫学HarmonyOS开发:HarmonyOS ArkTS 获取位置服务
华为·开源·harmonyos·鸿蒙
迷曳8 天前
28、鸿蒙Harmony Next开发:不依赖UI组件的全局气泡提示 (openPopup)和不依赖UI组件的全局菜单 (openMenu)、Toast
前端·ui·harmonyos·鸿蒙
脑袋大大的8 天前
跨端分栏布局:从手机到Pad的优雅切换
javascript·uni-app·uniapp·安卓·鸿蒙·app开发·混合开发
屿筱8 天前
鸿蒙与web混合开发双向通信
鸿蒙·harmonyos5
SuperHeroWu79 天前
【HarmonyOS】ArkTS语法详细解析
华为·harmonyos·arkts·鸿蒙·编程语言·语法·详解