当前位置: 首页 > news >正文

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

一,背景

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

二,问题分析

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

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

三,解决方案

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

@Entry
@Component
struct TestPage {@State list:Array<string> =new ArrayaboutToAppear(){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事件,代码如下:

@Entry
@Component
struct TestPage {@State list:Array<string> =new ArrayaboutToAppear(){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: //按下breakcase TouchType.Move: //移动breakcase TouchType.Up: //抬起break}})}
}

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

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

@Entry
@Component
struct TestPage {@State list:Array<string> =new Array//当前按下的手指数量@State currentFingers: number = 0aboutToAppear(){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.lengthif(this.currentFingers > 1){console.debug("yz-----有多个手指按下,忽略此事件")return}breakcase TouchType.Move: //移动if(this.currentFingers > 1){console.debug("yz-----有多个手指移动,忽略此事件")return}breakcase TouchType.Up: //抬起if(this.currentFingers > 1){console.debug("yz-----有多个手指移动,忽略此事件")return}break}})}
}

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

@Entry
@Component
struct TestPage {@State list:Array<string> =new Array//当前按下的手指数量@State currentFingers: number = 0//手指触摸X起始位置@State startX: number = 0//手指触摸Y起始位置@State startY: number = 0aboutToAppear(){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.lengthif(this.currentFingers > 1){console.info("yz-----有多个手指按下,忽略此事件")return}// 记录起始位置this.startX = event.touches[0].windowXthis.startY = event.touches[0].windowYbreakcase TouchType.Move: //移动if(this.currentFingers > 1){console.info("yz-----有多个手指移动,忽略此事件")return}breakcase TouchType.Up: //抬起if(this.currentFingers > 1){console.info("yz-----有多个手指移动,忽略此事件")return}break}})}
}

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

scroller: Scroller = new Scroller()....List({ scroller: this.scroller}) {...
}

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

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

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

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

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

 //上次触摸Y位置
@State lastY: number = 0....onTouch((event: TouchEvent) => {....case TouchType.Move: //移动...this.lastY =event.touches[0].windowYbreakcase TouchType.Up: //抬起...this.lastY =0break

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

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

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

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

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

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

...case TouchType.Move: //移动if(this.currentFingers > 1){console.info("yz-----有多个手指移动,忽略此事件")return}let currentY = event.touches[0].windowY;//计算移动距离const deltaX = event.touches[0].windowX - this.startXconst deltaY = currentY - this.startYconst absX = Math.abs(deltaX)const absY = Math.abs(deltaY)const changeY =currentY -this.lastYconst absChangeY = Math.abs(changeY)if(absY >= absX && absY > this.THRESHOLD){//计算滑动距离this.moveY =this.scrollY-deltaY+absChangeYconsole.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是为了让滑动距离更大一点。

调用

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

进行滑动。

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

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

@Entry
@Component
struct TestPage {@State list:Array<string> =new Array//当前按下的手指数量@State currentFingers: number = 0//手指触摸X起始位置@State startX: number = 0//手指触摸Y起始位置@State startY: number = 0scroller: Scroller = new Scroller()//上次偏移量@State scrollY: number = 0//上次触摸Y位置@State lastY: number = 0//移动的距离@State moveY: number = 0//阈值THRESHOLD: number = 10;//动画时间@State animationTime: number = 50aboutToAppear(){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.lengthif(this.currentFingers > 1){console.info("yz-----有多个手指按下,忽略此事件")return}// 记录起始位置this.startX = event.touches[0].windowXthis.startY = event.touches[0].windowY// 记录当前偏移量this.scrollY =this.scroller.currentOffset().yOffsetbreakcase TouchType.Move: //移动if(this.currentFingers > 1){console.info("yz-----有多个手指移动,忽略此事件")return}let currentY = event.touches[0].windowY;//计算移动距离const deltaX = event.touches[0].windowX - this.startXconst deltaY = currentY - this.startYconst absX = Math.abs(deltaX)const absY = Math.abs(deltaY)const changeY =currentY -this.lastYconst 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}breakcase TouchType.Up: //抬起if(this.currentFingers > 1){console.info("yz-----有多个手指移动,忽略此事件")return}this.lastY =0break}})}
}

五,增加惯性滑动

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

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

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

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

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

 5.1 快速扫动增加惯性

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

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

...//手指按下的时间戳
@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 =0break

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

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

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

...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) {//向下滑动 边界为0let 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 缓慢滑动,但是抬手比较快,增加惯性

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

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

所以先定义几个变量:

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

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

。。。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.startYif(changeY>0){//向下滑动 边界为0targetY = currentScrollY -  Math.abs(flickDistance);if(targetY <0){targetY =0}}else {//向上滑动}//惯性滑动 将动画时间设置为800msthis.animationTime = 800;//滑动this.scroller.scrollTo({xOffset: 0,yOffset: targetY,animation: { duration: this.animationTime, curve: Curve.Ease }});
}...

还是先省略向上滑动

六,增加两指操作

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

。。。case TouchType.Down: //按下this.currentFingers = event.touches.lengthif(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}
。。。。

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

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

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

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

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

@State isOneFingerLeave: boolean = false

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

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

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

@State twoFingerDistance: number = 0
@State lastCurrentY: number = 0

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

...
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事件:

//是否按下了@State isDownStart: boolean = false。。。case TouchType.Down: //按下this.currentFingers = event.touches.lengthif(this.currentFingers > 2){console.info("yz-----有多个手指按下,忽略此事件")return}if(this.isDownStart){console.debug("yh-----已经按下,忽略此事件")return}this.isDownStart = true
。。。case TouchType.Up:
。。。this.isDownStart = false
。。。

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

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

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

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

然后在移动中增加判断:

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].windowYreturn}//记录正在滑动中的状态,判断是否需要进入第一次移动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 =falsebreak

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

七,List组件的封装

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

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

@Component
export default struct YZListView {build(){}}

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

@Component
export default struct YZListView {@State list:Array<string> =new Array//当前按下的手指数量@State currentFingers: number = 0//手指触摸X起始位置@State startX: number = 0//手指触摸Y起始位置@State startY: number = 0scroller: 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 = falsebuild(){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.lengthif(this.currentFingers > 2){console.info("yz-----有多个手指按下,忽略此事件")return}if(this.isDownStart){console.debug("yh-----已经按下,忽略此事件")return}this.isDownStart = true// 记录起始位置this.startX = event.touches[0].windowXthis.startY = event.touches[0].windowY// 记录当前偏移量this.scrollY =this.scroller.currentOffset().yOffsetthis.isOneFingerLeave = falsethis.twoFingerDistance = 0// 记录起始时间this.startTime = new Date().getTime()breakcase 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.startXconst deltaY = currentY - this.startYconst absX = Math.abs(deltaX)const absY = Math.abs(deltaY)const changeY =currentY -this.lastYconst 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].windowYreturn}//记录正在滑动中的状态,判断是否需要进入第一次移动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}breakcase TouchType.Up: //抬起if(this.currentFingers > 2){console.info("yz-----有多个手指移动,忽略此事件")return}if(event.touches.length == 2){this.isOneFingerLeave = trueconsole.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.startXconst deltaY = event.touches[0].windowY - this.startYconst absX = Math.abs(deltaX)const absY = Math.abs(deltaY)const changeY =event.touches[0].windowY -this.startYconst absChangeY = Math.abs(changeY)//惯性滑动时,动画时间设置为800msthis.animationTime =800//移动方向为垂直移动 且移动距离大于阈值 则认为是上下滑动if(absY >= absX && absY > this.THRESHOLD){//上下滑动let distince = 0//根据测量得到的比较合适的滑动距离if(absChangeY <150){distince =3*absChangeY}else {distince =300}if(changeY > 0) {//向下滑动 边界为0let 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.startYif(changeY>0){//向下滑动 边界为0targetY = currentScrollY -  Math.abs(flickDistance);if(targetY <0){targetY =0}}else {//向上滑动}//惯性滑动 将动画时间设置为800msthis.animationTime = 800;//滑动this.scroller.scrollTo({xOffset: 0,yOffset: targetY,animation: { duration: this.animationTime, curve: Curve.Ease }});}this.isDownStart = falsethis.lastY =0this.isMoving =falsebreak}})}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 :

@Prop list:Array<Object> =new Array

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

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

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

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

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

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

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中获取容器总高度:

    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})

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

 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.spacelet bottomOffSet =this.contentHeight -this.containerHeightif(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.spacelet bottomOffSet =this.contentHeight -this.containerHeightif(bottomOffSet < 0){//需要滑动bottomOffSet =0}else {bottomOffSet =bottomOffSet + this.paddingNumber*count}if(targetY>bottomOffSet){targetY =bottomOffSet}}//惯性滑动 将动画时间设置为800msthis.animationTime = 800;//滑动this.scroller.scrollTo({xOffset: 0,yOffset: targetY,animation: { duration: this.animationTime, curve: Curve.Ease }});}

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

@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 = 0aboutToAppear(){}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.lengthif(this.currentFingers > 2){console.debug("yz-----有多个手指按下,忽略此事件")return}if(this.isDownStart){console.debug("yz-----已经按下,忽略此事件")return}this.isDownStart = truethis.isOneFingerLeave = falsethis.twoFingerDistance = 0// 记录起始位置this.startX = event.touches[0].windowXthis.startY = event.touches[0].windowY// 记录起始时间this.startTime = new Date().getTime()// 记录当前偏移量this.scrollY =this.scroller.currentOffset().yOffsetthis.moveY = 0// 初始化最后一次移动时间this.lastMoveTime = this.startTime;// 初始化最后一次移动的Y坐标this.lastMoveY = this.startY;breakcase 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.startXconst deltaY = currentY - this.startYconst absX = Math.abs(deltaX)const absY = Math.abs(deltaY)const changeY =currentY -this.lastYconst absChangeY = Math.abs(changeY)//手指拖动移动时,动画时间设置为10msthis.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].windowYreturn}//记录正在滑动中的状态,判断是否需要进入第一次移动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}breakcase TouchType.Up:if(this.currentFingers > 2){console.debug("yz-----有多个手指抬起,忽略此事件")return}if(event.touches.length == 2){this.isOneFingerLeave = trueconsole.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.startXconst deltaY = event.touches[0].windowY - this.startYconst absX = Math.abs(deltaX)const absY = Math.abs(deltaY)const changeY =event.touches[0].windowY -this.startYconst absChangeY = Math.abs(changeY)//惯性滑动时,动画时间设置为800msthis.animationTime =800//移动方向为垂直移动 且移动距离大于阈值 则认为是上下滑动if(absY >= absX && absY > this.THRESHOLD){//上下滑动let distince = 0//根据测量得到的比较合适的滑动距离if(absChangeY <150){distince =3*absChangeY}else {distince =300}if(changeY > 0) {//向下滑动 边界为0let 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.spacelet bottomOffSet =this.contentHeight -this.containerHeightif(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.startYif(changeY>0){//向下滑动 边界为0targetY = 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.spacelet bottomOffSet =this.contentHeight -this.containerHeightif(bottomOffSet < 0){//需要滑动bottomOffSet =0}else {bottomOffSet =bottomOffSet + this.paddingNumber*count}if(targetY>bottomOffSet){targetY =bottomOffSet}}//惯性滑动 将动画时间设置为800msthis.animationTime = 800;//滑动this.scroller.scrollTo({xOffset: 0,yOffset: targetY,animation: { duration: this.animationTime, curve: Curve.Ease }});}this.isDownStart = falsethis.lastY =0this.isMoving =falsebreak}})}ceilDivision(x: number, n: number): number {if (x >= n) {return Math.ceil(x / n);} else {return Math.floor(x / n) + 1;}}}

如何使用呢?

首先定义 自己的item:

@Component
export default struct  TestItem{@State item:string =""index: number = 0onButtonClick ?:(item:string,index:number)=>voidbuild(){Text(this.item).width("100%").height(50).onClick(()=>{this.onButtonClick && this.onButtonClick(this.item,this.index)})}
}

然后在page中使用:

import TestItem from './TestItem'
import YZListView from './YZListView'@Entry
@Component
struct TestPage {@State list:Array<string> =new ArrayonButtonClick = (name: Object, index: number) => {//点击列表itemconsole.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%')}}}

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

http://www.lryc.cn/news/603183.html

相关文章:

  • 【图像噪点消除】——图像预处理(OpenCV)
  • 创建型设计模式-工厂方法模式和抽象工厂方法模式
  • 社区老人健康信息管理系统|基于springboot社区老人健康信息管理系统设计与实现(源码+数据库+文档)
  • Gartner发布CTEM指南:使用持续威胁暴露管理来减少网络攻击
  • 智能体安全与可信AI:防护机制与伦理考量
  • 利用 C# 实现 Word 文档多维度统计(字数、字符数、页数、段落数、行数)
  • macOS “Sploitlight“漏洞曝光:攻击者可窃取Apple Intelligence缓存数据
  • FreeRTOS在中断上下文中设置事件组,调度很慢的的解决方法
  • JavaWeb 入门:CSS 基础与实战详解(Java 开发者视角)
  • 如何在在NPM发布一个React组件
  • pycharm中安装pythonocc
  • 队列算法之【用队列实现栈】
  • 【Android】三种弹窗 Fragment弹窗管理
  • 人工智能技术革命:AI工具与大模型如何重塑开发者工作模式与行业格局
  • Sentinel实现限流和熔断降级
  • 四、Linux核心工具:Vim, 文件链接与SSH
  • 整车应用------后电驱总成
  • Java 大视界 -- Java 大数据在智能教育学习社区知识图谱构建与知识传播分析中的应用(370)
  • centos服务器安装minio
  • React 中获取当前路由信息
  • 低空经济应用-无人机拉格朗日粒子追踪技术
  • 界面控件Telerik UI for WPF 2025 Q2亮点 - 重要组件全新升级
  • SeeMoE:从零开始实现一个MoE视觉语言模型
  • CentOS Nginx 1.13.9 部署文档
  • 14 - 大语言模型 — 抽取式问答系统 “成长记”:靠 BERT 学本事,从文本里精准 “揪” 答案的全过程(呆瓜版-1号)
  • 分布式链路追踪详解
  • 【C++】第十九节—一文万字详解 | AVL树实现
  • 【C++篇】“内存泄露”的宝藏手段:智能指针
  • 【腾讯云】EdgeOne免费版实现网站加速与安全防护
  • thingsboard 自定义动作JS编程