前言
上一篇介绍了如何使用自带的 Refresh 来实现下拉刷新和添加一个额外的 ListItem 来实现上拉加载功能。这一篇我们来看下不使用这这两种方式,还能如何实现。
大致思路
- 在Stack组件中放置三个子组件,自定义的下拉刷新组件、List组件、加载更多组件。
- 下拉刷新和上拉加载更多组件通过 position 和translate属性控制位置。并且将父组件 Stack 的clip属性设置为 true,以裁剪掉超出范围的部分。
- 并在 Stack 中通过priorityGesture绑定优先识别手势PanGesture,在上下滑动的过程中判断子组件 List 的位置,以判断
- 根据滑动的方向和距离,做出不同的效果来提示用户
- 注意处理各种状态,比如刷新、加载更多过程中不能再次刷新、加载更多等
先看效果:


实现
整体布局
先看下页面整体的布局

整个页面,除去顶部的ActionBar,只看剩下的刷新相关的布局,类似下面这种
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Stack() { Stack({ alignContent: Alignment.Top }) { this.header() }.translate({ y: this.headerTransitionY }).height('100%')
Stack({ alignContent: Alignment.Bottom }) { this.footer() }.translate({ y: this.footerTransitionY }).height('100%')
List({ space: 20, scroller: this.scroller }) .edgeEffect(EdgeEffect.None) .translate({ y: this.headerTransitionY || this.footerTransitionY })
}.layoutWeight(1).clip(true)
|
顶部的Stack高度设置为 100%,通过alignContent: Alignment.Top属性将子组件this.header()放在最上面。对于this.footer()也是同样的处理。注意这里设置的translate和List组件的translate。因为 List 要和 header、footer 同时滑动。
但这样还不够,初始状态下我们需要将 header 向上平移它本身的高度。footer 也需要向下平移它本身的高度,将 header 和 footer 隐藏起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @LocalBuilder header() { Column() { Row() { Image($r('app.media.tab_guide_unselect')).width(20).height(20).rotate({ centerX: "50%", centerY: "50%", angle: this.headerImageRotate }) Blank().width(10).height(0) Text(`${this.headerText} ${this.getLastRefreshTime()}`) }.width("100%").alignItems(VerticalAlign.Center).justifyContent(FlexAlign.Center)
} .width("100%") .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .height(60) .backgroundColor(Color.Red) .onAreaChange((_, newValue) => { this.headerMaxTransition = newValue.height as number }) .position({ top: -this.headerMaxTransition }) }
|
对于 header,放了一个图片随着滑动进行旋转。
并且定义了一个headerMaxTransition来记录 header 的高度,方便我们后面操作。
随后通过position将 header 移动到绘制区域之外。
1 2 3 4 5 6 7 8
| @LocalBuilder footer() { Column() { Text(`${this.footerText} ${this.getLastLoadMoreTime()}`).height(60).backgroundColor(Color.Green) }.width("100%").onAreaChange((_, newValue) => { this.footerMaxTransition = newValue.height as number }).position({ bottom: -this.footerMaxTransition }).backgroundColor(Color.Orange) }
|
对于 footer,也是同样的操作,footerMaxTransition记录footer 的高度,同样通过position将 footer 移动到绘制区域之外
滑动
我们对最外层的 Stack 的添加有限识别手势来识别垂直方向的滑动
1 2 3 4 5 6 7
| .priorityGesture( PanGesture({ direction: PanDirection.Vertical }) .onActionStart((event: GestureEvent) => {} .onActionUpdate((event: GestureEvent) => {} .onActionEnd((event: GestureEvent) => {} .onActionCancel(() => {} )
|
这样我就可以优先于子控件 List 来响应上下滑动事件了。
我们在onActionStart回调中记录按下的位置;在onActionUpdate处理并分发滑动事件,也就是判断应该滑动哪个组件;在onActionEnd中判断是否需要刷新等操作
onActionStart
我们在事件开始的时候记录一下按下的位置
1 2 3
| .onActionStart((event: GestureEvent) => { this.lastScroll = event.offsetY })
|
onActionUpdate
先计算一下滑动的距离,然后将滑动的速度分发到List组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .onActionUpdate((event: GestureEvent) => { let diff = event.offsetY - this.lastScroll this.lastScroll = event.offsetY if (diff) { if (event.velocity) { if (diff > 0) { this.scroller.fling(event.velocity) } else { this.scroller.fling(-event.velocity) }
} this.handleScroll(diff) } })
|
这里调用的scroller.fling方法,其中参数velocity值设置为0,视为异常值,本次滚动不生效。如果值为正数,则向顶部滚动;如果值为负数,则向底部滚动。
下面看下处理滑动的handleScroll方法
先分类总结一下各种情况
向上滑动
列表在最底部
- footer 的偏移距离已经超过了它本身的高度
- 这时候
footer已经完全漏出来了。这时候我们可以将阻尼系数写小一些,也就是手指滑动 10单位,控件移动2 或者3 单位;
- 需要标记松手后需要执行加载更多操作,也就是在onActionEnd方法中处理
- 需要将提示文案修改为
松开后加载更多
- footer的偏移距离没有超过它本身高度
- 这时候
footer已经漏出来一部分,这时候我们可以将阻尼系数稍微调大一些, 也就是手指滑动 10单位,控件移动5 或者6 单位;
- 这时候如果松手,则需要将组件偏移距离取消
- 取消松手时加载更多标记
- 需要将提示文案修改为
上拉加载更多
列表不在最底部
- header 有偏移
- 将 header 向上平移
- 将 header 中的图片反向旋转
header 没有偏移
向下滑动
- 列表在最顶部
- header 偏移距离超过它本身高度
- header 偏移距离不变
- 图片继续随手指滑动而旋转
- 标记松开手时处理刷新
- 提示文案修改为
松开后刷新
- header 偏移距离没有超过它本身高度
- 文案修改为
下拉刷新
- 取消松开手时的刷新标记
- 松手时将组件偏移距离取消
- 列表不在最顶部
- footer 有偏移距离
- footer 没有偏移距离
大致就这些情况,我们来看下具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| handleScroll(offset: number) { if (offset < 0) { if (this.scroller.isAtEnd()) { console.error(`列表在底部,继续上拉`) if (Math.abs(this.footerTransitionY) > this.footerMaxTransition) { this.footerTransitionY += (offset * 0.2) if(this.currentState != State.Loading){ this.footerText = '松开后加载更多' this.needLoadMoreOnDidScroll = true }
} else {
this.footerTransitionY += (offset * 0.3) if(this.currentState != State.Loading){ this.footerText = '上拉加载更多' this.needLoadMoreOnDidScroll = false }
} } else { console.error(`列表不在底部,继续上拉`) if (this.headerTransitionY > 0) { this.headerTransitionY += (offset * 0.3) this.headerImageRotate -= (offset * 1.5) } else { this.headerTransitionY = 0 this.scroller.scrollBy(0, -offset) }
} } else { if (this.scroller.currentOffset().yOffset == 0) { this.headerTransitionY += (offset * 0.3) this.headerImageRotate += (offset * 1.5) if (Math.abs(this.headerTransitionY) >= this.headerMaxTransition) { this.headerTransitionY = this.headerMaxTransition if(this.currentState != State.Loading){ this.headerText = '松开后刷新' this.needRefreshOnDidScroll = true }
} else { if(this.currentState != State.Loading){ this.headerText = '下拉刷新' this.needRefreshOnDidScroll = false }
} } else { if (this.footerTransitionY > 0) { this.footerTransitionY -= (offset * 0.3) } else { this.footerTransitionY = 0 this.scroller.scrollBy(0, -offset) }
} } }
|
onActionEnd
在滑动事件结束的时候,需要处理的事情有如下几个:
如果不需要刷新,则取消掉 header 的偏移。
如果需要刷新,则在刷新完成后取消掉 header 的偏移。
如果不需要加载更多,则取消掉 footer 的偏移。
如果需要加载更多,则将 footer 的偏移设置为它本的高度,加载完成后取消掉偏移
在取消偏移量的时候,为了能有更好的体验,可以使用动画做平滑过渡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| .onActionEnd((event: GestureEvent) => { if (event.velocityY) { this.scroller.fling(event.velocityY) } if (this.needRefreshOnDidScroll && this.currentState != State.Loading) { this.currentState = State.Loading this.headerText = '刷新中' this.needRefreshOnDidScroll = false setTimeout(() => { this.headerText = '刷新成功' this.currentState = State.Idle this.lastRefreshTime = systemDateTime.getTime() setTimeout(() => { this.headerTransitionY = -this.headerMaxTransition this.getUIContext().animateTo({ duration: 500, curve: Curve.EaseOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { console.error('刷新后恢复 play end'); } }, () => { this.headerTransitionY = 0 })
}, 1000) }, 2000) } else { this.resetHeaderTransition() }
if (this.needLoadMoreOnDidScroll && this.currentState != State.Loading) { this.currentState = State.Loading this.getUIContext().animateTo({ duration: 500, curve: Curve.EaseOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { console.error('加载更多时刚好漏出完整的 footer'); } }, () => { this.footerTransitionY = -this.footerMaxTransition
})
this.footerText = '加载中' this.needLoadMoreOnDidScroll = false setTimeout(() => { this.footerText = '加载成功' this.currentState = State.Idle this.lastLoadMoreTime = systemDateTime.getTime() setTimeout(() => { this.footerTransitionY = this.footerMaxTransition this.getUIContext().animateTo({ duration: 500, curve: Curve.EaseOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { console.error('加载更多后恢复 play end'); this.footerText = '上拉加载更多' } }, () => { this.footerTransitionY = 0
}) }, 1000) }, 2000) } else { this.resetFooterTransition() }
})
|
下面是两个取消偏移量的动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| resetHeaderTransition() { this.getUIContext().animateTo({ duration: 500, curve: Curve.EaseOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { console.error('刷新后恢复 play end'); } }, () => { this.headerTransitionY = 0 })
}
resetFooterTransition() { this.getUIContext().animateTo({ duration: 500, curve: Curve.EaseOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { console.error('刷新后恢复 play end'); } }, () => { this.footerTransitionY = 0 }) }
|
这样我们就粗暴的实现了上拉加载更多和下拉刷新的组件。这里只是给出大致的思路,还有很多细节没有考虑:
- 比如嵌套滑动的情况
- 比如List内容不满一屏幕的情况等等
以上