arkts-list

360 阅读4分钟

1.list 的基本使用

list 用于显示列表,当内容超出list实际大小时,支持上下滑动且配合 v1/v2 不同版本的监听器,当内容划出屏幕之后会保存模板新的item直接复用以减少内存占用

1.1 基本显示

@Entry
@ComponentV2
struct Test {
  @Local arr: number[] = []

  aboutToAppear(): void {
    for (let x = 0; x < 100; x++) {
      this.arr.push(x)
    }
  }

  build() {
    // space 设置主轴方向 item 的间距为
    List({ space: 10 }) {
      ForEach(this.arr, (item: number) => {
        // list 内部实际只能给 listItem 和 listGroup,其他的都不行
        // listItem 内部只允许存在一个子组件
        ListItem() {
          Text(`${item}`)
            .width("100%")
            .padding({ top: 10, bottom: 10 })
            .backgroundColor("#f6f7f9")

          // 多个子组件报错
          // Text(`${item}`)
          //   .width("100%")
          //   .padding({ top: 10, bottom: 10 })
          //   .backgroundColor("#f6f7f9")
        }

        // listItem 也支持通用属性
        // .width("100%")
        // .margin({ left: 12, right: 12 })

      })
    }
    .listDirection(Axis.Vertical) //list元素方向为竖向,默认就是 Axis.Vertical
    .scrollBar(BarState.Off) // 不显示 scrollBar
    .edgeEffect(EdgeEffect.None) // 关闭边缘滑动效果
    .divider({
      //设置分割线
      strokeWidth: 1,
      color: "#000",
      startMargin: 20,
      endMargin: 20,
    })
    .backgroundColor("#fff")
    .width("100%")
    .height("100%")
  }
}

image.png

1.2 list 的表格

使用 lanes(value: number | LengthConstrain, gutter?: Dimension): ListAttribute; 实现表格模式,其中 value 为 number | LengthConstrain 的联合类型, 当传入的类型为 LengthConstrain 时,设置好 item 的最大宽度和最小宽度后,会根据屏幕实际大小自动计算列数,在适配上比较方便但可能产品不会同意~

@Entry
@ComponentV2
struct Test {
  @Local arr: number[] = []

  aboutToAppear(): void {
    for (let x = 0; x < 100; x++) {
      this.arr.push(x)
    }
  }

  build() {
    List({ space: 10 }) {
     // 这里的index 指的是 item 在 this.arr 中的index,而不是在list中的index
     // 不使用时可以不写
      ForEach(this.arr, (item: number,index:number) => { 
        ListItem() {
          Text(`${item}`)
            .width("100%")
            .padding({ top: 10, bottom: 10 })
            .backgroundColor("#f6f7f9")
        }
      })
    }
    .lanes(3, 10) // 分成3 列,列间距为10
    .backgroundColor("#fff")
    .width("100%")
    .height("100%")
  }
}

image.png

1.3 头和尾

列表可以直接添加 listItem 以达到头和尾的效果,如列表顶部需要一个广告的 Image,且需要随着列表一起滑动,或者更为复杂的布局且跟随list 一起滑动,此时不必嵌套scroll。这个设计相较于安卓来说要简便的多。如下

@Entry
@ComponentV2
struct Test {
  @Local arr: number[] = []
  private scroller = new Scroller()
  private scrollIndex: number = 20

  aboutToAppear(): void {
    for (let x = 0; x < 100; x++) {
      this.arr.push(x)
    }
  }

  build() {
    Column() {
      List({ space: 10 }) {

        ListItem() {
          Text("我是头部")
            .width("100%")
            .height(100)
            .backgroundColor("#f00")
        }

        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text(`${item}`)
              .width("100%")
              .padding({ top: 10, bottom: 10 })
              .backgroundColor("#f6f7f9")
          }
        })

        ListItem() {
          Text("我是尾部")
            .width("100%")
            .height(100)
            .backgroundColor("#f00")
        }
      }
      .backgroundColor("#fff")
      .width("100%")
      .layoutWeight(1)
    }
    .width("100%")
    .height("100%")
    .padding({ top: 50 })

  }
}

image.png

2.list 的 scroller

list 的 scroll 提供额外的滑动控制及状态获取

private scroller = new Scroller()

build(){
    List({ scroller: this.scroller })
}

根据 偏移量 offset 滑动
this.scroller.scrollTo({ xOffset: 0, yOffset: 0 })

根据 list 的下标滑动,如果有头和尾,则实际 index 和 数据源 this.arr 的下标不同
this.scroller.scrollToIndex(this.scrollIndex++)

获取当前 list 的滑动偏移量,如果滑动时监听了 list 的 onScrollFrameBegin 方法,实际用于判断的值应该通过 scroller.currentOffset() 获取而不是 onScrollFrameBegin 的参数
this.scroller.currentOffset()

api.14及以后- 以当前list 组件坐上为0,0 坐标计算,根据 x,y 坐标获取 item 的下标
getItemIndex(x: number, y: number): number;

api.12及以后- 根据index获取item的 Rect(坐标x,y,item的宽高)
getItemRect(index: number): RectResult;

3.复用

直接使用 foreach 渲染组件会渲染所有的内容,当列表数据异常多的时候,会造成卡顿甚至内存溢出闪退。这个时候就需要用到list的复用。
当 item 滑出可视范围时,复用组件会把 这个 item 保存当作模板,已有模板时会直接销毁,节省内存。继续滑动加载时直接使用这个item 的模板直接显示。

3.1 v1组件 的复用

v1 组件的复用使用 lazyForeach,因为目前都升级到 v2 了,这个可以查看官方文档。

3.2 v2组件 的复用

v2 组件使用 Repeat 组件实现复用,简单示例如下:

/**
* 实体类,使用基本数据类型效果不明显
*/
@ObservedV2
class Bean {
  @Trace age: number = 0
  @Trace type: number = 0
}

@Entry
@ComponentV2
struct Test {
  @Local arr: Bean[] = []

  aboutToAppear(): void {
    for (let x = 0; x < 100; x++) {
      let tempBean = new Bean()
      tempBean.age = x
      this.arr.push(tempBean)
    }
  }

  build() {
    Column() {
      List() {
        // 复用组件,类似于 foreach,也是循环渲染
        Repeat(this.arr)
        .each(() => {})  //写就完了~
        .virtualScroll()  // 开启复用,不写这个视为不使用复用
        .template(   //模板属性
            "temp1",   //模板名称
            // item 内容
            (obj: RepeatItem<Bean>) => {  
              Item1({ bean: obj.item })
            },
            // 模板缓存个数
            { cachedCount: 2 }
        )
        // 根据当前 item 的 bean 返回模板名称,对应之后使用该模板
        .templateId((item: Bean) => "temp1")
      }
      .width("100%")
      .layoutWeight(1)
    }
    .width("100%")
    .height("100%")
    .padding({ top: 50 })
  }
}

@ComponentV2
struct Item1 {
  @Param @Require bean: Bean

  build() {
    Text(`item1:${this.bean.age}`)
      .onClick(() => {
        this.bean.age++
      })
  }
}

image.png

示例中模板的显示使用自定义组件,实际模板也可以使用 @Build 来显示,但@Build 没有生命周期,在实际应用中使用的概率不大,需要的可自行了解。
备注:如使用 @Build 显示,需要传递 obj 而不是 obj.item,否则会影响刷新显示。

3.2 多个模板

通过 templateId() 返回的模板名称和对应的 template() 的 name 对应上时,则使用该模板。
template() 可以设置多个,当 templateId() 返回不同 name 时,选择显示不同的模板。示例如下:

@ObservedV2
class Bean {
  @Trace age: number = 0
  @Trace type: number = 0
}

@Entry
@ComponentV2
struct Test {
  @Local arr: Bean[] = []

  aboutToAppear(): void {
    this.setData()
  }

  setData() {
    let tempArr: Bean[] = []
    for (let x = 0; x < 100; x++) {
      let tempBean = new Bean()
      tempBean.age = x
      tempBean.type = x % 2 == 0 ? 0 : 1
      tempArr.push(tempBean)
    }
    this.arr = tempArr
  }

  build() {
    Column() {
      Button("刷新数据")
        .onClick(() => {
          this.setData()
        })
      List() {
        Repeat(this.arr)
          .each(() => {
          })
          .virtualScroll()
          //第一个模板
          .template("temp1", (obj: RepeatItem<Bean>) => {
            Item1({ bean: obj.item })
          }, { cachedCount: 2 })
          //第二个模板
          .template("temp2", (obj: RepeatItem<Bean>) => {
            Item2({ bean: obj.item })
          }, { cachedCount: 2 })
          // 根据 type 返回不同类型的模板
          .templateId((item: Bean) => item.type == 0 ? "temp1" : "temp2")
      }
      .width("100%")
      .layoutWeight(1)
    }
    .width("100%")
    .height("100%")
    .padding({ top: 50 })
  }
}

@ComponentV2
struct Item1 {
  @Param @Require bean: Bean

  build() {
    Text(`item1:${this.bean.age}`)
      .onClick(() => {
        this.bean.age++
      })
  }
}

@ComponentV2
struct Item2 {
  @Param @Require bean: Bean

  build() {
    Text(`item2:${this.bean.age}`)
      .onClick(() => {
        this.bean.age++
      })
  }
}

image.png

多个模板的好处是可以像 安卓适配器的type一样,同一个适配器显示多种类型的数据。
备注:如果多个组件有通用的如上边用户显示,下边的操作显示,则不推荐使用模板,而是使用同一个组件,并根据类型区分中间的显示。只有完全不通用的item显示,才需要模板

3.3 复用的问题

因为是直接使用已经存在的组件,而不是重新创建的。在滑动时新显示的组件不会调用生命周期 aboutToAppear(),如有操作需要在 aboutToAppear()中进行,则需要单独封装并 使用 @Monitor监听变动,如下:

@ComponentV2
struct Item1 {
  @Param @Require bean: Bean

  aboutToAppear(): void {
    this.setData()
  }

  @Monitor("bean")
  setData(){
    // 搞事...
  }

  build() {
    Text(`item1:${this.bean.age}`)
      .onClick(() => {
        this.bean.age++
      })
  }
}

4 List 的相关问题

4.1 多列时无法指定某个元素跨列占满一行

使用List 时,头+内容列表+尾 是常见的组合,有些时候需要内容列表分为2列或多列显示时,此时头和尾没有办法跨行占满一行。如需实现此功能,需使用 瀑布流组件WaterFlow

4.2 在tab 中使用 List(或任意使用 scorller 的组件),内容不满一屏时无法响应滑动

使用代码以确保任何时候都响应滑动

.nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) //嵌套滑动兼容
.edgeEffect(EdgeEffect.None, { alwaysEnabled: true })  // 任何时候都响应滑动

4.3 上下拉刷新

方案1:使用社区刷新库 ohos/pulltorefresh

方案2:使用官方 Refresh+List,上拉加载更多时,使用 .scrollToIndex 在滑动到 尾部多少个时进行预加载,并在加载时在 list 中添加加载组件显示