HarmonyOS 鸿蒙Next 上下拉动画效果实现
HarmonyOS 鸿蒙Next 上下拉动画效果实现
效果如图:(为什么用xhs?因为手头没华为设备没法导出视频……orz)
const TopHeight = 150;
@Entry
@Component
export default struct QHNearbyOutletListView {
@ObjectLink outletServiceResult: QHOutletServiceResult
@ObjectLink outletService: QHOutletServiceItem
private listScroller: Scroller = new Scroller();
// 列表y坐标偏移量
@State offsetY: number = 0
// 按下的y坐标
private downY = 0
// 上一次移动的y坐标
private lastMoveY = 0
// 当前列表首部的索引
private startIndex = 0
// 当前列表尾部的索引
private endIndex = 0
// 下拉刷新的布局高度
private pullRefreshHeight = 50
// 下拉刷新文字:下拉刷新、松开刷新、正在刷新、刷新成功
@State pullRefreshText: string= ‘下拉刷新’
// 下拉刷新图标:与文字对应
@State pullRefreshImage: Resource = $r(“app.media.qh_refresh_icon”)
// 是否可以刷新:未达到刷新条件,收缩回去
private isCanRefresh = false
// 是否正在刷新:刷新中不进入触摸逻辑
private isRefreshing: boolean = false
// 是否已经进入了下拉刷新操作
private isPullRefreshOperation = false
// 上拉加载的布局默认高度
private loadMoreHeight = 40
// 上拉加载的布局是否显示
@State isVisibleLoadMore: boolean = false
// 是否可以加载更多
private isCanLoadMore = false
// 是否加载中:加载中不进入触摸逻辑
private isLoading: boolean = false
// 自定义下拉刷新布局
@Builder CustomPullRefreshLayout(){
Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Image(this.pullRefreshImage)
.width(18)
.height(18)
.rotate({
x: 0,
y: 0,
z: 1,
centerX: ‘50%’,
centerY: ‘50%’,
angle:
this.isCanRefresh && this.isRefreshing
? 0
: this.offsetY*3
}) // 组件以矢量(0,0,1)为旋转轴,绕中心点顺时针旋转 angle 度
Text(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pullRefreshText)
.margin({ left: <span class="hljs-number"><span class="hljs-number">7</span></span>, bottom: <span class="hljs-number"><span class="hljs-number">1</span></span> })
.fontSize(<span class="hljs-number"><span class="hljs-number">17</span></span>)
}
.width(<span class="hljs-string"><span class="hljs-string">'100%'</span></span>)
.height(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pullRefreshHeight)
<span class="hljs-comment"><span class="hljs-comment">// 布局跟着列表偏移量移动</span></span>
.offset({ x: <span class="hljs-number"><span class="hljs-number">0</span></span>, y: `${vp2px(-<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pullRefreshHeight) + <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY}px` })
}
// 自定义加载更多布局
@Builder CustomLoadMoreLayout(){
Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Image($r(“app.media.qh_refresh_icon”))
.width(18)
.height(18)
.rotate({
x: 0,
y: 0,
z: 1,
centerX: ‘50%’,
centerY: ‘50%’,
angle: -this.offsetY*20
})
Text(<span class="hljs-string"><span class="hljs-string">'放开以刷新...'</span></span>)
.margin({ left: <span class="hljs-number"><span class="hljs-number">7</span></span>, bottom: <span class="hljs-number"><span class="hljs-number">1</span></span> })
.fontSize(<span class="hljs-number"><span class="hljs-number">17</span></span>)
}
.width(<span class="hljs-string"><span class="hljs-string">'100%'</span></span>)
.height(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.loadMoreHeight)
.backgroundColor(<span class="hljs-string"><span class="hljs-string">'#f4f4f4'</span></span>)
.visibility(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.isVisibleLoadMore ? Visibility.Visible : Visibility.None)
}
// 刷新测试数据
private refreshData(){
OutletServiceViewModel.requestBranchData(
this.outletServiceResult,
‘1’,
this.outletService.PROV_CODE,
this.outletService.CITY_CODE,
this.outletService.TOWN_CODE,
this.outletService.BRCH_TYPE,
this.outletService.BRCH_POS_X,
this.outletService.BRCH_POS_Y,
)
this.listScroller.scrollToIndex(0)
}
// 加载更多测试数据
private loadMoreData(){
OutletServiceViewModel.requestBranchData(
this.outletServiceResult,
this.outletServiceResult.NEXT_KEY,
this.outletService.PROV_CODE,
this.outletService.CITY_CODE,
this.outletService.TOWN_CODE,
this.outletService.BRCH_TYPE,
this.outletService.BRCH_POS_X,
this.outletService.BRCH_POS_Y,
)
this.listScroller.scrollToIndex(0)
}
build() {
Column() {
// 下拉刷新布局
this.CustomPullRefreshLayout()
<span class="hljs-comment"><span class="hljs-comment">// 列表布局</span></span>
List({scroller: <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.listScroller}) {
ForEach(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.outletServiceResult.LIST,(item: QHOutletServiceItem, index: number)=>{
ListItem(){
QHNearbyOutletItemView({
index: index,
outletServiceItem: item,
clickNavEvent: ()=>{
},
clickTelEvent: ()=>{
YTCallUtil.call(item.BRCH_TEL)
}
})
}
})
<span class="hljs-comment"><span class="hljs-comment">// 加载更多布局</span></span>
ListItem(){
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.CustomLoadMoreLayout()
}
}
.scrollBar(BarState.Off)
.alignListItem(ListItemAlign.Center)
.width(<span class="hljs-string"><span class="hljs-string">'100%'</span></span>)
.backgroundColor(<span class="hljs-string"><span class="hljs-string">'#FFF2F3F4'</span></span>)<span class="hljs-comment"><span class="hljs-comment">// 背景</span></span>
.edgeEffect(EdgeEffect.None) <span class="hljs-comment"><span class="hljs-comment">// 去掉回弹效果</span></span>
.offset({ x: <span class="hljs-number"><span class="hljs-number">0</span></span>, y: `${<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY - TopHeight}px` }) <span class="hljs-comment"><span class="hljs-comment">// touch事件计算的偏移量单位是px,记得加上单位</span></span>
.onScrollIndex((start, end) => { <span class="hljs-comment"><span class="hljs-comment">// 监听当前列表首位索引</span></span>
console.info(`${start}=start============end=${end}`)
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.startIndex = start
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.endIndex = end
})
}
.width(<span class="hljs-string"><span class="hljs-string">'100%'</span></span>)
.height(<span class="hljs-string"><span class="hljs-string">'100%'</span></span>)
.backgroundColor(<span class="hljs-string"><span class="hljs-string">'#f4f4f4'</span></span>)
.onTouch((event) => <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.listTouchEvent(event))<span class="hljs-comment"><span class="hljs-comment">// 父容器设置touch事件,当列表无数据也可以下拉刷新</span></span>
.onAppear(() => {
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.refreshData()
})
}
// 触摸事件
listTouchEvent(event: TouchEvent){
switch (event.type) {
case TouchType.Down: // 手指按下
// 记录按下的y坐标
this.downY = event.touches[0].y
this.lastMoveY = event.touches[0].y
break
case TouchType.Move: // 手指移动
// 下拉刷新中 或 加载更多中,不进入处理逻辑
if(this.isRefreshing || this.isLoading){
console.info(’========Move刷新中,返回=========’)
return
}
// 判断手势
let isDownPull = event.touches[0].y - this.lastMoveY > 0
// 下拉手势 或 已经进入了下拉刷新操作
if ((isDownPull || this.isPullRefreshOperation) && !this.isCanLoadMore) {
this.touchMovePullRefresh(event)
} else {
this.touchMoveLoadMore(event)
}
this.lastMoveY = event.touches[0].y
break
case TouchType.Up: // 手指抬起
case TouchType.Cancel: // 触摸意外中断:来电界面
// 刷新中 或 加载更多中,不进入处理逻辑
if(this.isRefreshing || this.isLoading){
console.info(’========Up刷新中,返回=========’)
return
}
if (this.isPullRefreshOperation) {
this.touchUpPullRefresh()
} else {
this.touchUpLoadMore()
}
break
}
}
//============================================下拉刷新==================================================
// 手指移动,处理下拉刷新
touchMovePullRefresh(event:TouchEvent){
// 当首部索引位于0
if (this.startIndex == 0) {
this.isPullRefreshOperation = true
// 下拉刷新布局高度
let height = vp2px(this.pullRefreshHeight)
// 滑动的偏移量
this.offsetY = event.touches[0].y - this.downY
<span class="hljs-comment"><span class="hljs-comment">// 偏移量大于下拉刷新布局高度,达到刷新条件</span></span>
<span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY >= height) {
<span class="hljs-comment"><span class="hljs-comment">// 状态1:松开刷新</span></span>
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pullRefreshState(<span class="hljs-number"><span class="hljs-number">1</span></span>)
<span class="hljs-comment"><span class="hljs-comment">// 偏移量的值缓慢增加</span></span>
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY = height + <span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY * <span class="hljs-number"><span class="hljs-number">0.15</span></span>
} <span class="hljs-keyword"><span class="hljs-keyword">else</span></span> {
<span class="hljs-comment"><span class="hljs-comment">// 状态0:下拉刷新</span></span>
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.pullRefreshState(<span class="hljs-number"><span class="hljs-number">0</span></span>)
}
<span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY < <span class="hljs-number"><span class="hljs-number">0</span></span>) {
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.offsetY = <span class="hljs-number"><span class="hljs-number">0</span></span>
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.isPullRefreshOperation = <span class="hljs-literal"><span class="hljs-literal">false</span></span>
}
}
}
// 手指抬起,处理下拉刷新
touchUpPullRefresh(){
// 是否可以刷新
if (this.isCanRefresh) {
console.info(’======执行下拉刷新========’)
// 偏移量为下拉刷新布局高度
this.offsetY = vp2px(this.pullRefreshHeight)
// 状态2:正在刷新
this.pullRefreshState(2)
<span class="hljs-comment"><span class="hljs-comment">// 模拟耗时操作</span></span>
setTimeout(() => {
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.refreshData()
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.closeRefresh()
}, <span class="hljs-number"><span class="hljs-number">500</span></span>)
} <span class="hljs-keyword"><span class="hljs-keyword">else</span></span> {
console.info(<span class="hljs-string"><span class="hljs-string">'======关闭下拉刷新!未达到条件========'</span></span>)
<span class="hljs-comment"><span class="hljs-comment">// 关闭刷新</span></span>
<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.closeRefresh()
}
}
// 下拉刷新状态
// 0下拉刷新、1松开刷新、2正在刷新、3刷新成功
pullRefreshState(state:number){
switch (state) {
case 0:
// 初始状态
this.pullRefreshText = ‘下拉刷新…’
this.pullRefreshImage = $r(“app.media.qh_refresh_icon”)
this.isCanRefresh = false
this.isRefreshing = false
break;
case 1:
this.pullRefreshText = ‘松开以刷新…’
this.pullRefreshImage = $r(“app.media.qh_refresh_icon”)
this.isCanRefresh = true
this.isRefreshing = false
break;
case 2:
this.offsetY = vp2px(this.pullRefreshHeight)
this.pullRefreshText = ‘正在载入…’
this.pullRefreshImage = $r(“app.media.qh_refresh_loading_icon”)
this.isCanRefresh = true
this.isRefreshing = true
break;
case 3:
this.pullRefreshText = ‘刷新成功’
this.pullRefreshImage = $r(“app.media.qh_refresh_done_icon”)
this.isCanRefresh = true
this.isRefreshing = true
break;
}
}
// 关闭刷新
closeRefresh() {
// 如果允许刷新,延迟进入,为了显示刷新中
setTimeout(() => {
let delay = 50
if (this.isCanRefresh) {
// 状态3:刷新成功
this.pullRefreshState(3)
// 为了显示刷新成功,延迟执行收缩动画
delay = 500
}
animateTo({
duration: 150, // 动画时长
delay: delay, // 延迟时长
onFinish: () => {
// 状态0:下拉刷新
this.pullRefreshState(0)
this.isPullRefreshOperation = false
}
}, () => {
this.offsetY = 0
})
}, this.isCanRefresh ? 500 : 0)
}
//============================================加载更多==================================================
// 手指移动,处理加载更多
touchMoveLoadMore(event:TouchEvent) {
// 因为加载更多是在列表后面新增一个item,当一屏能够展示全部列表,endIndex 为 length+1
if (this.endIndex == this.outletServiceResult.LIST.length - 1 || this.endIndex == this.outletServiceResult.LIST.length) {
// 滑动的偏移量
this.offsetY = event.touches[0].y - this.downY
if (Math.abs(this.offsetY) > vp2px(this.loadMoreHeight)/2) {
// 可以刷新了
this.isCanLoadMore = true
// 显示加载更多布局
this.isVisibleLoadMore = true
// 偏移量缓慢增加
this.offsetY = - vp2px(this.loadMoreHeight) + this.offsetY * 0.1
}
}
}
// 手指抬起,处理加载更多
touchUpLoadMore() {
animateTo({
duration: 200, // 动画时长
}, () => {
// 偏移量设置为0
this.offsetY = 0
})
if (this.isCanLoadMore) {
console.info(’======执行加载更多========’)
// 加载中…
this.isLoading = true
// 模拟耗时操作
setTimeout(() => {
this.closeLoadMore()
this.loadMoreData()
}, 500)
} else {
console.info(’======关闭加载更多!未达到条件========’)
this.closeLoadMore()
}
}
// 关闭加载更多
closeLoadMore() {
this.isCanLoadMore = false
this.isLoading = false
this.isVisibleLoadMore = false
}
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>
在HarmonyOS(鸿蒙)中实现上下拉动画效果,通常涉及到自定义组件和动画资源的运用。你可以通过编写自定义的CustomScrollView
或者扩展现有的ScrollView
,在其中使用Animator
或Animation
类来定义动画效果。设置动画的属性,如translationY
,以模拟上下拉的视觉效果。
此外,利用HarmonyOS的Canvas API在绘制层面上直接操作也可能是一种选择,但通常这种方式更为复杂且不易于维护。
确保你的动画在合适的生命周期事件中启动和停止,如滑动开始和结束。
如果问题依旧没法解决请加我微信,我的微信是itying888。