因为业务需要在下拉时列表不能滑动,而是一个悬浮式的loading跟手滑动,在全网找了一圈都没找到案例,只能自己动手写一个demo去试验,发现这东西还真有点难... 不说了 直接上效果吧 问题肯定是有的 但核心逻辑应该能行得通
效果图(模拟器环境下)

核心代码
ArkTS
@Component
export struct SuspensionRefresh {
// scroll controller
scrollController:Scroller = new Scroller()
// default slot
@BuilderParam content:() => void = this.defaultContent
// 刷新控制变量 必传
@Link @Watch('progressAnimation') isRefresh:boolean
// 设置下拉距离多少时可触发刷新
@Prop refreshOffset:number = 64
// 设置下拉距离的极限
@Prop refreshOffsetLimit:number = 96
// 下滑距离
@State pullOffsetY:number = -30
// 识别距离 即下拉多少时认为开始触发下拉动画
@State pullRefreshStartLimit:number = 10
// 记录每一次触发滑动事件时 上一次和这一次的滑动距离
@State currPullOffsetY:number = 0
@State lastPullOffsetY:number = 0
// 透明度
@State opacityValue:number = 0
// 进度条值
@State progressValue:number = 0
// 定时器
@State timer:number = -1
// 比值
@State radio:number = 0
// 是否正在下拉中
@State isPulling:boolean = false
// 内部内容是否触顶
@State isReactPageByTop:boolean = true
// 动画计算参数 start
// 旋转角度
@State rotateAngle:number = 0
// 旋转角度最大值
@State rotateAngleLimit:number = 360
// 动画计算参数 end
// callback start
onRefreshHandler: () => void = async () => {}
// callback end
progressAnimation(){
}
build() {
this.layout()
}
/*
* 布局builder
* */
@Builder
layout(){
Stack(){
Column(){
Scroll(this.scrollController){
this.content()
}
.layoutContainerStyleByScroll()
.enableScrollInteraction(!this.isPulling)
.onReachStart(() => {
this.isReactPageByTop = true
})
.onDidScroll(() => {
if (this.scrollController.currentOffset().yOffset !== 0) {
this.isReactPageByTop = false
}
})
}
.full()
// 并行手势
.parallelGesture(
// 滑动手势
PanGesture({fingers:1,distance:this.pullRefreshStartLimit,direction:PanDirection.Vertical})
// 手势开始时
.onActionStart(() => {
this.currPullOffsetY = 0
this.lastPullOffsetY = 0
})
// 手势中
.onActionUpdate((event) => {
// 记录当前滑动位置 以及记录上一次的滑动位置
this.lastPullOffsetY = this.currPullOffsetY
this.currPullOffsetY = event.offsetY
// 计算比值
let radio = this.pullOffsetY / this.refreshOffsetLimit
// 判断下拉还是上拉
let isPullDown:boolean = this.currPullOffsetY - this.lastPullOffsetY > 0 ? true : false
// 如果此时触顶并下拉 则标记为下拉状态中
if (this.isReactPageByTop && isPullDown){
this.isPulling = true
}
// 当前不在刷新中
if (!this.isRefresh) {
// 下拉时
if (isPullDown){
// 如果是下拉状态中 再计算位置
if (this.isPulling) {
// 此时如果超出下拉最大距离 则给一个固定值
if (this.pullOffsetY >= this.refreshOffsetLimit) {
this.pullOffsetY = this.refreshOffsetLimit
this.rotateAngle = this.rotateAngleLimit
}
// 如果没超出 则计算比值并按比例赋值
else {
this.pullOffsetY += (this.currPullOffsetY - this.lastPullOffsetY)
this.rotateAngle = radio * this.rotateAngleLimit
// 如果已经达到刷新距离但未到极限距离 则把透明度给一个默认值
if (this.pullOffsetY > this.refreshOffset) {
this.opacityValue = 1
}
else {
this.opacityValue = (this.pullOffsetY / this.refreshOffset)
}
}
}
}
// 上拉时
else {
// 如果此时仍在下拉中 则继续改变loading位置
if (this.isPulling) {
this.pullOffsetY += (this.currPullOffsetY - this.lastPullOffsetY)
this.rotateAngle = radio * this.rotateAngleLimit
// 如果已经达到刷新距离但未到极限距离 则把透明度给一个默认值
if (this.pullOffsetY > this.refreshOffset) {
this.opacityValue = 1
}
else {
this.opacityValue = (this.pullOffsetY / this.refreshOffset)
}
}
}
}
})
// 手势结束时
.onActionEnd(async (event) => {
// 如果下拉距离已经超出 就赋值固定值
let timer = -1
let isAdd = true
if (!this.isRefresh){
if (this.pullOffsetY > this.refreshOffset) {
this.isRefresh = true
this.pullOffsetY = this.refreshOffsetLimit
this.rotateAngle = this.rotateAngleLimit
timer = setInterval(()=>{
if (this.progressValue === 100) {
isAdd = false
}
else if (this.progressValue <= 0) {
isAdd = true
}
if (isAdd) {
this.progressValue += 2
}
else {
this.progressValue -= 2
}
this.rotateAngle += 5
},10)
// 执行刷新方法
await this.onRefreshHandler()
}
// 清空定时器
clearInterval(timer)
// 恢复初始状态
this.isRefresh = false
this.isPulling = false
this.pullOffsetY = -30
this.opacityValue = 0
this.isReactPageByTop = true
}
})
// 取消手势
.onActionCancel(() => {
console.log('cancel')
})
)
this.refreshLoadingBuilder()
}
.full()
.align(Alignment.Top)
}
/*
* 占位builder
* */
@Builder
defaultContent(){
Column(){
// 可以放置通用空状态
}
.full()
}
/*
* 刷新loading
* */
@Builder
refreshLoadingBuilder(){
Column(){
if (this.isRefresh){
Progress({type:ProgressType.Ring,value:this.progressValue,total:100})
.width(20)
.height(20)
.color('#FF4C58')
.style({
strokeWidth:2,
})
.backgroundColor(Color.Transparent)
}
else {
SymbolGlyph($r('sys.symbol.arrow_clockwise'))
.fontSize(20)
.fontWeight(700)
.fontColor(['#FF4C58'])
}
}
.translate({
y:this.pullOffsetY
})
.rotate({
x:0,
y:0,
z:1,
angle:this.rotateAngle
})
.opacity(this.opacityValue)
.animation({
duration:100,
curve:Curve.Linear
})
.refreshLoadingContainerStyle()
}
}
// 样式-占满
@Styles
function full(){
.width('100%')
.height('100%')
}
@Styles
function refreshLoadingContainerStyle(){
.borderRadius(20)
.backgroundColor('#FFFFFF')
.zIndex(100)
.padding(5)
}
@Extend(Scroll)
function layoutContainerStyleByScroll(){
.width('100%')
.height('100%')
.align(Alignment.Top)
.scrollBar(BarState.Off)
}