赞
踩
列表下拉刷新、上拉加载更多,不管在web时代还是鸿蒙应用都是一个非常常用的功能,基于ArkUI中TS扩展的声明式开发范式实现一个下拉刷新,上拉加载。
如果数据量过大,可以使用LazyForEach代替ForEach
高阶组件-上拉加载,下拉刷新https://gitee.com/bingtengaoyu/harmonyos-advanced-componen/tree/master/UpDownRefresh
根据触摸事件onTouch()处理下拉和上拉,通过记录手指按下的y坐标和move移动的距离判断属于刷拉还是下滑,从而展示不同的内容。
- @Builder UpDownRefreshBuilder(text: string, state: string) {
- Row() {
- Image($r('app.media.refreshing'))
- .width(32)
- .height(32)
- Text(text).fontSize(16)
- }
- .justifyContent(FlexAlign.Center)
- .position({
- y: state === 'down' ? 20 : -20
- })
- .zIndex(999)
- .width('94%')
- .height('8%')
- }
- private currentOffsetY: number = 0;
- private timer: number = 0;
- @State refreshStatus: boolean = false;
- @State upRefreshStatus: boolean = false;
-
- aboutToDisappear() {
- this.timer = null
- }
-
- putDownRefresh(event?: TouchEvent): void {
- if (event === undefined) {
- return;
- }
- switch (event.type) {
- case TouchType.Down:
- this.currentOffsetY = event.touches[0].y;
- break;
- case TouchType.Move:
- if(this.scroller.currentOffset().yOffset < 50) {
- this.refreshStatus = event.touches[0].y - this.currentOffsetY > 50;
- }
- this.upRefreshStatus = event.touches[0].y - this.currentOffsetY < -50;
- break;
- case TouchType.Cancel:
- break;
- case TouchType.Up:
- // Only simulation effect, no data request
- this.timer = setTimeout(() => {
- if (this.upRefreshStatus) {
- this.scroller.scrollTo({ // 调用scrollTo滚动到具体位置
- xOffset: 0, // 竖直方向滚动,该值不起作用
- yOffset: 680, // 滚动到底部
- animation: { // 滚动动画
- duration: 1500,
- curve: Curve.EaseOut
- }
- })
- }
- this.refreshStatus = false;
- this.upRefreshStatus = false;
- }, 1500);
- break;
- }
- }
兵腾傲宇/harmonyos-healthy-live - Gitee.com
- import router from '@ohos.router'
- import curves from '@ohos.curves'
- import { BreakpointSystem, BreakPointType } from '../common/BreakpointSystem'
- import { FoodInfo, Category } from '../model/DataModels'
- import { getFoods, getFoodCategories, getSortedFoodData } from '../model/DataUtil'
- import { Records } from './components/DietRecord'
- import { PersonalCenter } from './PersonalCenter'
-
- interface FoodId {
- foodId: FoodInfo;
- }
-
- @Component
- struct FoodListItem {
- private foodItem?: FoodInfo
-
- build() {
- Navigator({ target: 'pages/FoodDetail' }) {
- Row() {
- Image(this.foodItem!.image!)
- .objectFit(ImageFit.Contain)
- .autoResize(false)
- .height(40)
- .width(40)
- .backgroundColor('#FFf1f3f5')
- .margin({ right: 16 })
- .borderRadius(6)
- .sharedTransition(this.foodItem!.letter, {
- duration: 400,
- curve: curves.cubicBezier(0.2, 0.2, 0.1, 1.0),
- delay: 100
- })
- Text(this.foodItem?.name)
- .fontSize(14)
- Blank()
- Text($r('app.string.calorie_with_kcal_unit', this.foodItem?.calories.toString()))
- .fontSize(14)
- }
- .height(64)
- .width('100%')
- }
- .params({ foodId: this.foodItem } as FoodId)
- .margin({ right: 24, left: 32 })
- }
- }
-
- @Component
- struct ListModeFoods {
- private foodItems: Array<FoodInfo | string> = getSortedFoodData()
- private currentOffsetY: number = 0;
- private timer: number = 0;
- @State refreshStatus: boolean = false;
- @State upRefreshStatus: boolean = false;
-
- aboutToDisappear() {
- this.timer = null
- }
-
- putDownRefresh(event?: TouchEvent): void {
- if (event === undefined) {
- return;
- }
- switch (event.type) {
- case TouchType.Down:
- this.currentOffsetY = event.touches[0].y;
- break;
- case TouchType.Move:
- if(this.scroller.currentOffset().yOffset < 50) {
- this.refreshStatus = event.touches[0].y - this.currentOffsetY > 50;
- }
- this.upRefreshStatus = event.touches[0].y - this.currentOffsetY < -50;
- break;
- case TouchType.Cancel:
- break;
- case TouchType.Up:
- // Only simulation effect, no data request
- this.timer = setTimeout(() => {
- if (this.upRefreshStatus) {
- this.scroller.scrollTo({ // 调用scrollTo滚动到具体位置
- xOffset: 0, // 竖直方向滚动,该值不起作用
- yOffset: 680, // 滚动到底部
- animation: { // 滚动动画
- duration: 1500,
- curve: Curve.EaseOut
- }
- })
- }
- this.refreshStatus = false;
- this.upRefreshStatus = false;
- }, 1500);
- break;
- }
- }
-
- @Builder DownRefreshBuilder(text: string, state: string) {
- Row() {
- Image($r('app.media.refreshing'))
- .width(32)
- .height(32)
- Text(text).fontSize(16)
- }
- .justifyContent(FlexAlign.Center)
- .position({
- y: state === 'down' ? 20 : -20
- })
- .zIndex(999)
- .width('94%')
- .height('8%')
- }
-
- private scroller: Scroller = new Scroller(); // 创建一个滚动控制器
-
- build() {
- Column() {
- Text($r("app.string.title_food_list"))
- .width('100%')
- .height(56)
- .padding({ left: 20 })
- .backgroundColor('#FF1f3f5')
- .fontSize(20)
-
- Scroll(this.scroller) {
- if(this.refreshStatus) {
- this.DownRefreshBuilder('正在刷新', 'down')
- }
- List() {
- ForEach(this.foodItems, (item: FoodInfo) => {
- ListItem() {
- if (item.letter !== undefined) {
- FoodListItem({ foodItem: item })
- } else {
- if (typeof (item) === 'string') {
- Text(item)
- .fontSize(14)
- .height(48)
- .margin({ left: 24 })
- .width('100%')
- }
- }
- }
- })
- if(this.upRefreshStatus) {
- ListItem(){
- this.DownRefreshBuilder('正在加载', 'up')
- }
- }
- }
- .layoutWeight(1)
- }
- .scrollBar(BarState.Off)
- .edgeEffect(EdgeEffect.Spring)
- .width('100%')
- .height('90%')
- .onTouch((event?: TouchEvent) => {
- this.putDownRefresh(event);
- })
- }
- }
- }
-
- @Component
- struct FoodGridItem {
- private foodItem?: FoodInfo
-
- build() {
- Column() {
- Image(this.foodItem!.image!)
- .objectFit(ImageFit.Contain)
- .backgroundColor('#f1f3f5')
- .width('100%')
- .height(152)
- .sharedTransition(this.foodItem!.letter, {
- duration: 400,
- curve: curves.cubicBezier(0.2, 0.2, 0.1, 1.0),
- delay: 100
- })
- Row() {
- Text(this.foodItem?.name)
- .fontSize(14)
- Blank()
- Text($r('app.string.calorie_with_kcal_unit', this.foodItem?.calories.toString()))
- .fontSize(14)
- .fontColor(0x99000000)
- }
- .padding({ left: 12, right: 12 })
- .width('100%')
- .height(32)
- .backgroundColor('#E5E5E5')
- }
- .height(184)
- .clip(new Rect({ width: '100%', height: '100%', radius: 12 }))
- .onClick(() => {
- router.pushUrl({ url: 'pages/FoodDetail', params: { foodId: this.foodItem } })
- })
- }
- }
-
- @Component
- struct FoodGrid {
- @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
- private foodItems?: FoodInfo[]
-
- build() {
- Grid() {
- ForEach(this.foodItems!, (item: FoodInfo) => {
- GridItem() {
- FoodGridItem({ foodItem: item })
- }
- })
- }
- .columnsTemplate(new BreakPointType({
- sm: '1fr 1fr',
- md: '1fr 1fr 1fr',
- lg: '1fr 1fr 1fr 1fr'
- }).getValue(this.currentBreakpoint) as string)
- .columnsGap(8)
- .rowsGap(8)
- .padding({ left: 16, right: 16 })
- }
- }
-
- @Component
- struct CategoryModeFoods {
- @State currentTabIndex: number = 0
- private foodItems: FoodInfo[] = getFoods()
- private foodCategories: Category[] = getFoodCategories()
-
- /* *
- * 头部分类导航栏
- *
- * */
- @Builder
- tabBarItemBuilder(value: Resource, index: number) {
- Text(value)
- .fontColor(this.currentTabIndex === index ? Color.Blue : 'rgba(0,0,0,0.6)')
- .fontSize(this.currentTabIndex === index ? 20 : 18)
- .fontWeight(this.currentTabIndex === index ? FontWeight.Bold : FontWeight.Normal)
- .margin({ top: 2 })
- .height(56)
- }
-
- build() {
- Tabs() {
- TabContent() {
- FoodGrid({ foodItems: this.foodItems })
- }.tabBar(this.tabBarItemBuilder($r('app.string.category_all'), 0))
-
- ForEach(this.foodCategories, (foodCategory: Category, index?: number) => {
- TabContent() {
- FoodGrid({ foodItems: this.foodItems.filter(item => (item.categoryId === foodCategory.id)) })
- }.tabBar(this.tabBarItemBuilder(foodCategory.name!,
- index! + 1))
- })
- }
- .animationDuration(0)
- .barWidth('80%')
- .onChange((index) => {
- this.currentTabIndex = index
- })
- }
- }
-
- @Component
- struct FoodsDisplay {
- @State isCategoryMode: boolean = true
- @State isMoreIconOnClick: boolean = false
- @State isMoreIconOnHover: boolean = false
- @State isMoreIconOnFocus: boolean = false
-
- getMoreIconBgColor() {
- if (this.isMoreIconOnClick) {
- return $r('sys.color.ohos_id_color_click_effect')
- } else if (this.isMoreIconOnHover) {
- return $r('sys.color.ohos_id_color_hover')
- } else {
- return this.isCategoryMode ? Color.White : '#F1F3F5' || Color.Transparent
- }
- }
-
- build() {
- Stack({ alignContent: Alignment.TopEnd }) {
- if (this.isCategoryMode) {
- CategoryModeFoods()
- } else {
- ListModeFoods()
- }
- Row() {
- Image($r("app.media.ic_switch"))
- .height(24)
- .width(24)
- .margin({ left: 24, right: 24 })
- .focusable(true)
- }
- .height(56)
- .backgroundColor(this.getMoreIconBgColor())
- .stateStyles({
- focused: {
- .border({
- radius: $r('sys.float.ohos_id_corner_radius_clicked'),
- color: $r('sys.color.ohos_id_color_focused_outline'),
- style: BorderStyle.Solid
- })
- },
- normal: {
- .border({
- radius: $r('sys.float.ohos_id_corner_radius_clicked'),
- width: 0
- })
- }
- })
- .onFocus(() => this.isMoreIconOnFocus = true)
- .onBlur(() => this.isMoreIconOnFocus = false)
- .onHover((isOn) => this.isMoreIconOnHover = isOn)
- .onClick(() => {
- this.isCategoryMode = !this.isCategoryMode
- })
- }
- }
- }
-
- @Entry
- @Component
- struct Home {
- @State currentTabIndex: number = 0
- @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
- private breakpointSystem: BreakpointSystem = new BreakpointSystem()
-
- /* *
- * 主页和记录的tabs
- * */
- @Builder
- bottomBarItemBuilder(name: Resource, icon: Resource, index: number) {
- Flex({
- direction: new BreakPointType({
- sm: FlexDirection.Column,
- md: FlexDirection.Row,
- lg: FlexDirection.Column
- }).getValue(this.currentBreakpoint),
- justifyContent: FlexAlign.Center,
- alignItems: ItemAlign.Center
- }) {
- Image(icon)
- .height(24)
- .width(24)
- .fillColor(this.getTabBarColor(index))
- Text(name)
- .margin(new BreakPointType<Padding>({
- sm: { top: 4 },
- md: { left: 8 },
- lg: { top: 4 }
- }).getValue(this.currentBreakpoint) as Padding)
- .fontSize(11)
- .fontColor(this.getTabBarColor(index))
- }
- }
-
- aboutToAppear() {
- this.breakpointSystem.register()
- }
-
- aboutToDisappear() {
- this.breakpointSystem.unregister()
- }
-
- build() {
- Tabs({
- barPosition: new BreakPointType({
- sm: BarPosition.End,
- md: BarPosition.End,
- lg: BarPosition.Start
- }).getValue(this.currentBreakpoint)
- }) {
- TabContent() {
- FoodsDisplay()
- }.tabBar(this.bottomBarItemBuilder($r("app.string.tab_bar_home"), $r("app.media.ic_bottom_home"), 0))
-
- TabContent() {
- Records()
- }.tabBar(this.bottomBarItemBuilder($r("app.string.tab_bar_record"), $r("app.media.ic_bottom_record"), 1))
-
- TabContent() {
- PersonalCenter()
- }.tabBar(this.bottomBarItemBuilder($r("app.string.tab_bar_me"), $r("app.media.ic_public_me"), 2))
- }
- .vertical(new BreakPointType({ sm: false, md: false, lg: true }).getValue(this.currentBreakpoint) as boolean)
- .barWidth(new BreakPointType({ sm: '100%', md: '100%', lg: '56vp' }).getValue(this.currentBreakpoint) as string)
- .barHeight(new BreakPointType({ sm: '56vp', md: '56vp', lg: '60%' }).getValue(this.currentBreakpoint) as string)
- .animationDuration(300)
- .onChange((index) => {
- this.currentTabIndex = index
- })
- }
-
- private getTabBarColor(index: number) {
- return this.currentTabIndex == index ? $r('app.color.tab_bar_select_color') : $r('app.color.tab_bar_normal_color')
- }
- }
根据List中的回调方法onScrollIndex()监听当前列表首尾索引,根据触摸事件onTouch()处理下拉和上拉。
- const TopHeight = 200;
-
- @Entry
- @Component
- struct Index {
- @State list: Array<number> = []
- // 列表y坐标偏移量
- @State offsetY: number = 0
- // 按下的y坐标
- private downY = 0
- // 上一次移动的y坐标
- private lastMoveY = 0
- // 当前列表首部的索引
- private startIndex = 0
- // 当前列表尾部的索引
- private endIndex = 0
-
- // 下拉刷新的布局高度
- private pullRefreshHeight = 70
- // 下拉刷新文字:下拉刷新、松开刷新、正在刷新、刷新成功
- @State pullRefreshText: string= '下拉刷新'
- // 下拉刷新图标:与文字对应
- @State pullRefreshImage: Resource = $r("app.media.ic_pull_refresh_down")
- // 是否可以刷新:未达到刷新条件,收缩回去
- private isCanRefresh = false
- // 是否正在刷新:刷新中不进入触摸逻辑
- private isRefreshing: boolean = false
- // 是否已经进入了下拉刷新操作
- private isPullRefreshOperation = false
-
- // 上拉加载的布局默认高度
- private loadMoreHeight = 70
- // 上拉加载的布局是否显示
- @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)
-
- Text(this.pullRefreshText)
- .margin({ left: 7, bottom: 1 })
- .fontSize(17)
- }
- .width('100%')
- .height(this.pullRefreshHeight)
- // 布局跟着列表偏移量移动
- .offset({ x: 0, y: `${vp2px(-this.pullRefreshHeight) + this.offsetY}px` })
- }
-
- // 自定义加载更多布局
- @Builder CustomLoadMoreLayout(){
- Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
- Image($r("app.media.ic_loading"))
- .width(18)
- .height(18)
-
- Text('加载更多中...')
- .margin({ left: 7, bottom: 1 })
- .fontSize(17)
- }
- .width('100%')
- .height(this.loadMoreHeight)
- .backgroundColor('#f4f4f4')
- .visibility(this.isVisibleLoadMore ? Visibility.Visible : Visibility.None)
- }
-
- // 刷新测试数据
- private refreshData(){
- this.list = []
- for (var i = 0; i < 15; i++) {
- this.list.push(i)
- }
- }
-
- // 加载更多测试数据
- private loadMoreData(){
- let initValue = this.list[this.list.length-1] + 1
- for (var i = initValue; i < initValue + 10; i++) {
- this.list.push(i)
- }
- }
-
- build() {
- Column() {
- // 下拉刷新布局
- this.CustomPullRefreshLayout()
-
- // 列表布局
- List() {
- ForEach(this.list, item => {
- ListItem() {
- Column() {
- Text(`Item ${item}`)
- .padding(15)
- .fontSize(18)
- }
- }
- }, item => item.toString())
-
- // 加载更多布局
- ListItem(){
- this.CustomLoadMoreLayout()
- }
- }
- .backgroundColor(Color.White) // 背景
- .divider({ color: '#e2e2e2', strokeWidth: 1 }) // 分割线
- .edgeEffect(EdgeEffect.None) // 去掉回弹效果
- .offset({ x: 0, y: `${this.offsetY - TopHeight}px` }) // touch事件计算的偏移量单位是px,记得加上单位
- .onScrollIndex((start, end) => { // 监听当前列表首位索引
- console.info(`${start}=start============end=${end}`)
- this.startIndex = start
- this.endIndex = end
- })
-
- }
- .width('100%')
- .height('100%')
- .backgroundColor('#f4f4f4')
- .onTouch((event) => this.listTouchEvent(event))// 父容器设置touch事件,当列表无数据也可以下拉刷新
- .onAppear(() => {
- this.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
- // 下拉刷新布局高度
- var height = vp2px(this.pullRefreshHeight)
- // 滑动的偏移量
- this.offsetY = event.touches[0].y - this.downY
-
- // 偏移量大于下拉刷新布局高度,达到刷新条件
- if (this.offsetY >= height) {
- // 状态1:松开刷新
- this.pullRefreshState(1)
- // 偏移量的值缓慢增加
- this.offsetY = height + this.offsetY * 0.15
- } else {
- // 状态0:下拉刷新
- this.pullRefreshState(0)
- }
-
- if (this.offsetY < 0) {
- this.offsetY = 0
- this.isPullRefreshOperation = false
- }
- }
- }
-
- // 手指抬起,处理下拉刷新
- touchUpPullRefresh(){
- // 是否可以刷新
- if (this.isCanRefresh) {
- console.info('======执行下拉刷新========')
- // 偏移量为下拉刷新布局高度
- this.offsetY = vp2px(this.pullRefreshHeight)
- // 状态2:正在刷新
- this.pullRefreshState(2)
-
- // 模拟耗时操作
- setTimeout(() => {
- this.refreshData()
- this.closeRefresh()
- }, 2000)
-
- } else {
- console.info('======关闭下拉刷新!未达到条件========')
- // 关闭刷新
- this.closeRefresh()
- }
- }
-
- // 下拉刷新状态
- // 0下拉刷新、1松开刷新、2正在刷新、3刷新成功
- pullRefreshState(state:number){
- switch (state) {
- case 0:
- // 初始状态
- this.pullRefreshText = '下拉刷新'
- this.pullRefreshImage = $r("app.media.ic_pull_refresh_down")
- this.isCanRefresh = false
- this.isRefreshing = false
- break;
- case 1:
- this.pullRefreshText = '松开刷新'
- this.pullRefreshImage = $r("app.media.ic_pull_refresh_up")
- this.isCanRefresh = true
- this.isRefreshing = false
- break;
- case 2:
- this.offsetY = vp2px(this.pullRefreshHeight)
- this.pullRefreshText = '正在刷新'
- this.pullRefreshImage = $r("app.media.ic_loading")
- this.isCanRefresh = true
- this.isRefreshing = true
- break;
- case 3:
- this.pullRefreshText = '刷新成功'
- this.pullRefreshImage = $r("app.media.ic_refresh_succeed")
- this.isCanRefresh = true
- this.isRefreshing = true
- break;
- }
- }
-
- // 关闭刷新
- closeRefresh() {
- // 如果允许刷新,延迟进入,为了显示刷新中
- setTimeout(() => {
- var 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.list.length - 1 || this.endIndex == this.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()
- }, 2000)
- } else {
- console.info('======关闭加载更多!未达到条件========')
- this.closeLoadMore()
- }
- }
-
- // 关闭加载更多
- closeLoadMore() {
- this.isCanLoadMore = false
- this.isLoading = false
- this.isVisibleLoadMore = false
- }
-
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。