HarmonyOS鸿蒙Next中使用ListView列表实现左右双向滑动效果

HarmonyOS鸿蒙Next中使用ListView列表实现左右双向滑动效果

使用ListView 列表实现下拉刷新,上拉加载,左右双向滑动效果(需同步列表下面左右效果)有没有好的实现方式(网络请求)
7 回复

开发者您好,可查看如下方案:

【解决方案】

开发者您好,下拉刷新上拉加载可通过PullToRefresh实现(这是一款OpenHarmony环境下可用的下拉刷新、上拉加载组件),左右双向滑动可通过List组件和Scroll组件构建页面布局,结合PanGesture手势滑动事件实现滑动效果。参考示例如下:

  • 在目录entry/src/oh-package.json5文件中配置PullToRefresh依赖:
{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "[@ohos](/user/ohos)/pulltorefresh": "^2.1.2"
  }
}
  • 示例代码如下:
// Index.ets
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { PullToRefresh } from '[@ohos](/user/ohos)/pulltorefresh';

interface IndustryInfo {
  industryName: string;
  datas: StocksInfo;
}

interface StocksInfo {
  id: string;
  increase: string;
  limit: number;
  score: number;
  buy: string;
  funds: string;
  quantity: string;
  growth: string;
  board: number;
  leading: number;
}

@Entry
@Component
struct Index {
  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right });
  @Provide offsetX: number | undefined = undefined;

  aboutToAppear(): void {
    let windowClass: window.Window | undefined = undefined;
    window.getLastWindow(this.getUIContext()?.getHostContext(), (err: BusinessError, data) => {
      windowClass = data;
      let SystemBarProperties: window.SystemBarProperties = {
        statusBarColor: '#F1F3F5',
        navigationBarColor: '#000'
      };
      try {
        let promise = windowClass.setWindowSystemBarProperties(SystemBarProperties);
        promise.then(() => {
          console.info('Succeeded in setting the system bar properties.');
        }).catch((err: BusinessError) => {
          console.error(`Failed to set the system bar properties. Cause code: ${err.code}, message: ${err.message}`);
        });
      } catch (exception) {
        console.error(`Failed to set the system bar properties. Cause code: ${exception.code}, message: ${exception.message}`);
      }
    });
  }

  build() {
    Column() {
      Column() {
        MainArea();
      }
      .backgroundColor('#fff')
      .padding(12)
      .gesture(
        // 用于触发拖动手势事件,滑动的最小距离为5vp时拖动手势识别成功。
        PanGesture(this.panOption).onActionStart((event?: GestureEvent) => {
          this.offsetX = event?.offsetX;
          console.info('gesture11: PanGesture start' + event?.offsetX);
        })
      )
      .borderRadius(24);
    }
    .height('100%')
    .width('100%')
    .padding(10)
    .backgroundColor('#F1F3F5')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
  }
}

@Component
struct tableRow {
  @Prop infos: IndustryInfo;

  build() {
    Flex({ alignItems: ItemAlign.Center }) {
      Text(this.infos.industryName).fontSize(16).width(86).textAlign(TextAlign.Start);
      ScrollComponent({ stock: this.infos.datas });
    }.width('100%');
  }
}

@Component
struct ScrollComponent {
  scroller: Scroller = new Scroller();
  @Prop stock: StocksInfo;
  @Consume @Watch('onCountUpdated') offsetX: number;

  // 监听滑动距离
  onCountUpdated() {
    if (this.offsetX) {
      this.scroller.scrollTo({
        xOffset: this.scroller.currentOffset()?.xOffset - this.offsetX * 50, // 滑动距离可根据实际情况设置
        yOffset: 0,
        animation: { duration: 1000, curve: Curve.Ease }
      });
    }
  }

  build() {
    Scroll(this.scroller) {
      Row() {
        Text(this.stock.increase).fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.limit + '').fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.score + '').fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.buy).fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.funds).fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.quantity).fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.growth).fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.board + '').fontSize(16).width(86).textAlign(TextAlign.Center);
        Text(this.stock.leading + '').fontSize(16).width(86).textAlign(TextAlign.Center);
      }.padding({ top: 4, bottom: 4 });
    }
    .scrollable(ScrollDirection.Horizontal) // 滚动方向
    .scrollBar(BarState.Off) // 滚动条常驻显示
    .edgeEffect(EdgeEffect.None)
    .width('100%')
    // 禁止scroll滚动
    .enabled(false);
  }
}


@Component
struct MainArea {
  private scroller: Scroller = new Scroller();
  private fullScroller: Scroller = new Scroller();
  @State private arr: IndustryInfo[] = [
    {
      industryName: '乳制品',
      datas: {
        id: '10001',
        increase: '5%',
        limit: 1,
        score: 49,
        buy: '1.01亿',
        funds: '1.60亿',
        quantity: '0.6%',
        growth: '2073万',
        board: 3,
        leading: 102
      }
    }
  ];
  private settings: string[] = ['涨幅', '涨停数', 'AI得分', '主动净买', '主力资金', '量比', '涨速', '连板', '龙头股'];
  @Consume @Watch('onCountUpdated') offsetX: number;

  pushInfo(info: IndustryInfo): IndustryInfo {
    let indusInfo: IndustryInfo = {
      industryName: '乳制品',
      datas: {
        id: '10001',
        increase: '5%',
        limit: 1,
        score: 49,
        buy: '1.01亿',
        funds: '1.60亿',
        quantity: '0.6%',
        growth: '2073万',
        board: 3,
        leading: 102
      }
    }
    indusInfo.datas.limit += info.datas.limit
    return indusInfo
  }

  aboutToAppear(): void {
    for (let i = 1; i <= 30; i++) {
      let info = this.arr[this.arr.length-1]
      this.arr.push(this.pushInfo(info))
    }
  }

  @Builder
  private getListView() {
    List({ scroller: this.scroller }) {
      ForEach(this.arr, (item: IndustryInfo, index: number) => {
        ListItem() {
          Column() {
            tableRow({ infos: item }).padding(4);
            if (index !== this.arr.length - 1) {
              Divider()
                .strokeWidth(1)
                .width("100%")
                .color('#eee');
            }
          };
        };
      });
    }.sticky(StickyStyle.Header)
    .nestedScroll({
      scrollForward: NestedScrollMode.PARENT_FIRST,
      scrollBackward: NestedScrollMode.SELF_FIRST
    })
    .backgroundColor('#ffffffff')
    .edgeEffect(EdgeEffect.None) // 必须设置列表为滑动到边缘无效果
  }

  @Builder
  tabContentData() {
    Column() {
      PullToRefresh({
        data: this.arr,
        scroller: this.scroller,
        customList: () => {
          this.getListView();
        },
        // 可选项,下拉刷新回调
        onRefresh: () => {
          return new Promise<string>((resolve) => {
            // 模拟网络请求操作,请求网络2秒后得到数据,通知组件,变更列表数据
            setTimeout(() => {
              resolve('刷新成功');
              let info = this.arr[this.arr.length-1]
              this.arr.push(this.pushInfo(info))
            }, 500);
          });
        },
        // 可选项,上拉加载更多回调
        onLoadMore: () => {
          return new Promise<string>((resolve) => {
            setTimeout(() => {
              resolve('');
              let info = this.arr[this.arr.length-1]
              this.arr.push(this.pushInfo(info))
            }, 2000);
          });
        },
        customLoad: null,
        customRefresh: null,
      });
    };
  }

  // table header
  @Builder
  tabHeader() {
    Column() {
      Flex({ alignItems: ItemAlign.Center }) {
        Text('设置')
          .fontSize(16)
          .width(86)
          .padding({ bottom: 4 })
          .textAlign(TextAlign.Start);
        Scroll(this.fullScroller) {
          Row() {
            ForEach(this.settings, (setting: string) => {
              Text(setting)
                .fontSize(16)
                .width(86)
                .textAlign(TextAlign.Center);
            });
          };
        }
        .enabled(true)
        .scrollable(ScrollDirection.Horizontal) // 滚动方向横向
        .width('100%')
        .padding({ bottom: 4 })
        .scrollBar(BarState.Off) // 滚动条常驻显示
        .edgeEffect(EdgeEffect.None)
        // 禁止scroll滚动
        .enabled(false);

      }
      .padding(4);

      Divider()
        .strokeWidth(1)
        .width("100%")
        .color('#eee');
    }.width('100%');
  }

  // 监听滑动距离
  onCountUpdated() {
    if (this.offsetX) {
      this.scroller.scrollTo({
        xOffset: this.scroller.currentOffset()?.xOffset - this.offsetX * 50, // 滑动距离可根据实际情况设置
        yOffset: 0,
        animation: { duration: 1000, curve: Curve.Ease }
      });
      this.fullScroller.scrollTo({
        xOffset: this.fullScroller.currentOffset()?.xOffset - this.offsetX * 50, // 滑动距离可根据实际情况设置
        yOffset: 0,
        animation: { duration: 1000, curve: Curve.Ease }
      });
    }
  }

  build() {
    Column() {
      this.tabHeader()
      this.tabContentData()
    }
  }
}

【背景知识】

  • PullToRefresh是一款OpenHarmony环境下可用的下拉刷新、上拉加载组件。支持设置内置动画的各种属性,支持设置自定义动画,支持lazyForEach的数据作为数据源。

    PullToRefresh使用限制:

    1、目前只支持List、Scroll、Tabs、Grid和WaterFlow系统容器组件;

    2、暂不支持设置系统容器组件的弹簧效果和阴影效果,使用时需要将系统组件edgeEffect属性的值设置为(EdgeEffect.None);

    3、暂不支持页面触底时自动触发上拉加载功能;

    4、暂不支持在页面数据不满一屏时触发上拉加载功能;

    5、暂不支持通过代码的方式去触发下拉刷新功能;

    6、暂不支持在下拉刷新动画结束时提供手势结束的回调。

  • Refresh:可以进行页面下拉操作并显示刷新动效的容器组件。

  • List组件:列表包含一系列相同宽度的列表项。

  • Scroll:可滚动的容器组件,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。

  • PanGesture:滑动手势事件,当滑动的最小距离达到设定的最小值时触发滑动手势事件。

更多关于HarmonyOS鸿蒙Next中使用ListView列表实现左右双向滑动效果的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


List组件外面包一个Refresh组件,然后用List组件的onReachEnd回调做触底加载更多,用Refresh组件做下拉刷新。左右滑动的话,就用这个:#示例4通过componentcontent设置划出组件

列表的标题跟item滑动同步

比如横向排列有十多个已经超出屏幕,需左右滑动查看

你说的效果是不是像华为浏览器搜索框下面的标题栏效果?那样的话可以用ListItem嵌套横向Scroll组件,foreach循环渲染,

类似股票软件效果,

在HarmonyOS Next中,使用ArkUI的List组件实现左右双向滑动效果,需通过List的scrollBar属性设置为BarState.On开启滚动条,并配合布局方向控制。主要步骤包括:

  1. 创建List组件,设置其布局方向为水平(orientation: Axis.Horizontal)。
  2. 在List内使用ListItem子组件,每个ListItem可放置一个Row或自定义组件,实现横向排列。
  3. 通过手势事件或滑动监听,可进一步自定义滑动交互。此方法基于ArkTS/ArkUI框架,不涉及Java或C语言。

在HarmonyOS Next中,要实现ListView的左右双向滑动效果,可以结合使用SwipeControllerListItem的滑动能力。以下是核心实现思路:

  1. 使用SwipeController

    • 为ListView的每个ListItem配置SwipeController,通过swipeAction方法定义左右滑动的视图和操作。
    • 示例代码:
      class MySwipeController implements SwipeController {
        leftActions(): SwipeActionItem[] {
          return [{
            action: () => { /* 左滑操作 */ },
            builder: () => { /* 左滑视图 */ }
          }];
        }
        rightActions(): SwipeActionItem[] {
          return [/* 右滑操作配置 */];
        }
      }
      
  2. 同步列表下方效果

    • 若需同步列表下方的左右滑动(如导航指示器),可在滑动回调中更新状态,并通过状态变量控制下方视图的偏移量。
    • 使用ScrollListonScroll事件监听滑动位置,动态计算并应用平移效果。
  3. 结合下拉刷新和上拉加载

    • 使用RefreshLoadingProgress组件包裹ListView,分别实现下拉刷新和上拉加载。
    • 网络请求在刷新/加载回调中触发,更新数据后刷新列表。
  4. 注意事项

    • 左右滑动需合理设置滑动阈值,避免与垂直滚动冲突。
    • 列表性能优化:对于长列表,建议使用LazyForEach加载数据。

此方案能有效实现双向滑动与数据加载的同步,且符合HarmonyOS Next的声明式开发范式。

回到顶部