# 常见列表流

#### 概述

列表流是采用以“行”为单位进行内容排列的布局形式，每“行”列表项通过文本、图片等不同形式的组合，高效地显示结构化的信息，当列表项内容超过屏幕大小时，可以提供滚动功能。列表流具有排版整齐、重点突出、对比方便、浏览速度快等特点。同时列表流也具有非常广泛的使用场景，例如：应用首页、通讯录、音乐列表、购物清单等。

列表流主要使用List组件，按垂直方向线性排列子组件ListItemGroup或ListItem，混合渲染任意数量的图文视图，从而构建列表内容。在实际场景中，一般会根据需要，结合其它基础组件，形成相对复杂的交互功能。

本文将介绍以下列表流场景的实现：

*   常见列表流
    *   概述
    *   多类型列表项场景
    *   Tabs吸顶场景
    *   分组吸顶场景
    *   二级联动场景
    *   示例代码

#### 多类型列表项场景

**场景描述**

List组件作为整个首页长列表的容器，通过ListItem对不同模块进行视图界面定制，常用于门户首页、商城首页等多类型视图展示的列表信息流场景。

本场景以应用首页为例，将除页面顶部搜索框区域的其它内容，放在List组件内部，进行整体页面的构建。进入页面后，下滑刷新模拟网络请求；滑动页面列表内容，景区标题吸顶；滑动到页面底部，上滑模拟请求添加数据。

| 页面整体结构图 | 页面效果图 |
| --- | --- |
|  |  |

**实现原理**

根据列表内部各部分视图对应数据类型的区别，渲染不同的ListItem子组件。

Refresh组件可以进行页面下拉操作并显示刷新动效，List组件配合使用Swiper、Grid等基础组件用于页面的整体构建，再通过List组件的sticky属性、onReachEnd()事件和Refresh组件的onRefreshing()事件，实现下滑模拟刷新、上滑模拟添加数据及列表标题吸顶的效果。

**开发步骤**

1.  顶部搜索框区域。
    
    ```
    Row() {
      Text($r('app.string.beijing'))
        // ...
      TextInput({ placeholder: $r('app.string.want_search')})
        // ...
      Text($r('app.string.more'))
        // ...
    }
    ```
    
    实现效果：
    
2.  在List的第一个ListItem分组中，使用Swiper组件构建页面轮播图内容。
    
    ```
    List({ space: 12 }) {
      ListItem() {
        Swiper() {
          ForEach(this.swiperContent, (item: SwiperType) => {
            Stack({ alignContent: Alignment.BottomStart }) {
              Image($r(item.pic))
            }
          }, (item: SwiperType) => JSON.stringify(item))
        }
        // ...
        .autoPlay(true)
        .duration(1000)
        .curve(Curve.Linear)
        .indicator(
          new DotIndicator()
            .selectedColor(Color.White)
        )
        .itemSpace(10)
        // ...
      }
      // ...
    }
    ```
    
    实现效果：
    
3.  在List的第二个ListItem分组中，使用Grid组件构建页面网格区域。
    
    ```
    List({ space: 12 }) {
      ListItem() {
        // ...
      }
      ListItem() {
        Grid() {
          ForEach(this.gridTitle, (item: Resource) => {
            GridItem() {
              Column() {
                Image($r('app.media.pic1'))
                  // ...
                Text(item)
                  // ...
              }
            }
          }, (item: Resource) => JSON.stringify(item))
        }
        .rowsGap(16)
        .columnsGap(19)
        .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
        // ...
      }
      // ...
    }
    ```
    
    实现效果：
    
4.  推荐内容及列表内容的构建。
    
    ```
    @Builder
    scenicSpotDetailBuilder(title: Resource) {
      Column() {
        Image($r('app.media.pic1'))
          // ...
        Column() {
          Text(title)
            // ...
          Text() {
            Span($r('app.string.group_discount'))
              // ...
            Span('999￥')
              // ...
          }
          .margin({ top: 4, bottom: 4 })
    
          Text() {
            Span($r('app.string.group_discount'))
            Span('1999￥')
          }
          // ...
        }
        // ...
      }
    }
    ```
    
    ```
    List({ space: 12 }) {
      ListItem() {
        // ...
      }
      ListItem() {
        // ...
      }
      ListItem() {
        Row() {
          Image($r('app.media.pic1'))
            // ...
          Image($r('app.media.pic1'))
            // ...
        }
        // ...
      }
    
      ForEach(this.scenicSpotTitle, (item: Resource) => {
        ListItemGroup({ header: this.scenicSpotHeader(item) }) {
          ForEach(this.scenicSpotArray, (scenicSpotItem: Resource) => {
            ListItem() {
              this.scenicSpotDetailBuilder(scenicSpotItem);
            }
          }, (scenicSpotItem: Resource) => JSON.stringify(scenicSpotItem))
        }
        .borderRadius(this.borderRadiusVal)
      }, (item: Resource) => JSON.stringify(item))
    
      // ...
    }
    ```
    
    实现效果：
    
5.  将构建好的页面内容，放在Refresh组件内部，并给List和Refresh组件添加对应的onReachEnd()和onRefreshing()回调，实现下拉模拟刷新和上滑添加列表数据的效果。
    
    ```
    Row() {
      // ...
    }
    // ...
    
    Refresh({ refreshing: $$this.isRefreshing }) {
      List({ space: 12 }) {
        ListItem() {
          // ...
        }
        ListItem() {
          // ...
        }
        ListItem() {
          // ...
        }
    
        ForEach(this.scenicSpotTitle, (item: Resource) => {
          // ...
        }, (item: Resource) => JSON.stringify(item))
    
        ListItem() {
          Row() {
            if (!this.noMoreData) {
              LoadingProgress()
                // ...
            }
            Text(this.noMoreData ? $r('app.string.no_more_data') : $r('app.string.loading_more'))
          }
          // ...
        }
        // ...
      }
      // ...
      .onReachEnd(() => {
        if (this.scenicSpotArray.length >= 20) {
          this.noMoreData = true;
          return;
        }
        setTimeout(() => {
          this.scenicSpotArray.push('scenic area' + (this.scenicSpotArray.length + 1));
        }, 500)
      })
    }
    .onRefreshing(() => {
      this.isRefreshing = true;
      setTimeout(() => {
        this.scenicSpotArray =
          this.scenicSpotArray = ['scenic area 1', 'scenic area 2', 'scenic area 3', 'scenic area 4', 'scenic area 5'];
        this.noMoreData = false;
        this.isRefreshing = false;
      }, 2000)
    })
    ```
    

**实现效果**

| 模拟下拉刷新+标题吸顶效果 | 上滑加载更多效果 |
| --- | --- |
|  |  |

#### Tabs吸顶场景

**场景描述**

Tabs嵌套List的吸顶效果，常用于新闻、资讯类应用的首页。

本场景以Tabs页签首页内容为例，在首页TabContent的内容区域使用List组件配合其它组件，构建下方列表数据内容。进入页面后，向上滑动内容，中间Tabs页签区域实现吸顶展示的效果。

| 页面整体结构图 | 页面效果图 |
| --- | --- |
|  |  |

**实现原理**

Tabs组件可以在页面内快速实现视图内容的切换，让用户能够聚焦于当前显示的内容，并对页面内容进行分类，提高页面空间利用率。

通过Tabs组件，配合使用Stack、Scroll、Search以及List等基础组件构建完整页面，再使用List组件的nestedScroll属性，结合calc计算高度，实现中间Tabs页签区域吸顶展示的效果。

**开发步骤**

1.  构建Tabs的自定义tabBar内容。
    
    ```
    @Builder
    tabBuilder(img: Resource, title: Resource, index: number) {
      Column() {
        Image(img)
          // ...
          .fillColor(this.currentIndex === index ? '#0a59f7' : '#66000000')
        Text(title)
          // ...
          .fontColor(this.currentIndex === index ? '#0a59f7' : '#66000000')
      }
      // ...
      .onClick(() => {
        this.currentIndex = index;
        this.tabsController.changeIndex(this.currentIndex);
      })
    }
    ```
    
    ```
    Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
      TabContent() {
        // ...
      }
      .tabBar(this.tabBuilder($r('app.media.mine'), $r('app.string.tabBar1'), 0))
    
      // ...
    }
    // ...
    .onChange((index: number) => {
      this.currentIndex = index;
    })
    ```
    
    实现效果：
    
2.  构建顶部搜索区域。
    
    ```
    Row() {
      Image($r('app.media.app_icon'))
        // ...
      Search({
        placeholder: $r('app.string.want_search'),
      })
        .searchButton('search', { fontSize: 14 })
        // ...
      Text($r('app.string.search'))
        // ...
    }
    ```
    
    实现效果：
    
3.  图片占位区域、自定义导航内容及列表内容构建。
    
    ```
    Scroll(this.scrollController) {
      Column() {
        Image($r('app.media.pic5'))
          // ...
    
        Column() {
          Row({ space: 16 }) {
            ForEach(this.tabArray, (item: string, index: number) => {
              Text(item)
                .fontColor(this.currentTabIndex === index ? '#0a59f7' : Color.Black)
                .onClick(() => {
                  this.contentTabController.changeIndex(index);
                  this.currentTabIndex = index;
                })
            }, (item: string) => item)
          }
          // ...
    
          Tabs({ barPosition: BarPosition.Start, controller: this.contentTabController }) {
            TabContent() {
              List({ space: 10, scroller: this.listScroller }) {
                CustomListItem({
                  imgUrl: $r('app.media.pic1'),
                  title: $r('app.string.manager_content')
                })
                // ...
              }
              // ...
            }
            .tabBar('follow')
            // ...
          }
          // ...
        }
        // ...
      }
    }
    // ...
    .scrollBar(BarState.Off)
    ```
    
    实现效果：
    
4.  给List组件添加的nestedScroll属性，结合calc计算实现中间自定义Tab页签区域吸顶展示的效果。
    
    ```
    Tabs({ barPosition: BarPosition.Start, controller: this.contentTabController }) {
      TabContent() {
        List({ space: 10, scroller: this.listScroller }) {
          // ...
        }
        // ...
        .nestedScroll({
          scrollForward: NestedScrollMode.PARENT_FIRST,
          scrollBackward: NestedScrollMode.SELF_FIRST
        })
      }
      .tabBar('follow')
      // ...
    }
    .barHeight(0)
    .height('calc(100% - 100vp)')
    .onChange((index: number) => {
      this.currentTabIndex = index;
    })
    ```
    

**实现效果**

#### 分组吸顶场景

**场景描述**

双列表同向联动，右边字母列表用于快速索引，内容列表根据首字母进行分组，常用于通讯录、城市选择、分组选择等页面。

本场景以城市列表页面为例，左侧城市列表数据和右侧字母导航数据通过List组件来展示，并通过Stack组件使两个列表数据分层显示。在进入页面后，通过滑动左侧城市列表数据，列表字母标题吸顶展示，对应右侧字母导航内容高亮显示；点击右侧字母导航内容，左侧城市列表展示对应内容。

| 页面整体结构图 | 页面效果图 |
| --- | --- |
|  |  |

**实现原理**

左侧List作为城市列表，右侧List为城市首字母快捷导航列表，通过ListItem对对应数据进行渲染展示，并使用Stack堆叠容器组件，字母导航列表覆盖城市列表上方，再给对应List添加sticky属性和onScrollIndex()方法，实现两个列表数据间的联动效果。

**开发步骤**

1.  城市列表使用ListItemGroup，对“当前城市”“热门城市”“城市数据”进行分组，并通过ListItem展示每个分组中的具体数据。
    
    ```
    @Builder
    textContent(content: string) {
      Text(content)
        // ...
    }
    ```
    
    ```
    List({ scroller: this.cityScroller }) {
      ListItemGroup({ header: this.itemHead($r('app.string.current_city')) }) {
        ListItem() {
          Text(this.currentCity)
            .width('100%')
            .height(45)
            .fontSize(16)
            .padding({ left: 16, top: 12, bottom: 12 })
            .textAlign(TextAlign.Start)
            .backgroundColor(Color.White)
        }
      }
    
      ListItemGroup({ header: this.itemHead($r('app.string.popular_cities')) }) {
        ForEach(this.hotCities, (item: string) => {
          ListItem() {
            this.textContent(item);
          }
        }, (item: string) => item)
      }
      .divider({
        strokeWidth: 1,
        color: '#EDEDED',
        startMargin: 10,
        endMargin: 45
      })
    
      ForEach(this.groupWorldList, (item: string) => {
        ListItemGroup({ header: this.itemHead(item) }) {
          ForEach(this.getCitiesWithGroupName(item), (cityItem: City) => {
            ListItem() {
              this.textContent(cityItem.city);
            }
          }, (cityItem: City) => cityItem.city)
        }
      })
    }
    ```
    
2.  右侧字母导航列表数据，同样通过List组件进行展示。
    
    ```
    Column() {
      List({ scroller: this.navListScroller }) {
        ForEach(this.groupWorldList, (item: string, index: number) => {
          ListItem() {
            Text(item)
              // ...
          }
        }, (item: string) => item)
      }
    }
    ```
    
3.  使用堆叠容器组件Stack，将字母导航内容覆盖到城市列表内容上方。
    
    ```
    Stack({ alignContent: Alignment.End }) {
      List({ scroller: this.cityScroller }) {
        // ...
      }
      // ...
    
      Column() {
        List({ scroller: this.navListScroller }) {
          // ...
        }
      }
      // ...
    }
    ```
    
4.  最后，给城市列表添加sticky属性实现标题吸顶效果，及添加onScrollIndex()方法，通过selectNavIndex变量与字母导航列表内容进行关联，控制的对应字母导航内容的选中状态。
    
    在字母导航列表中，添加点击事件，在点击事件中通过城市列表控制器cityScroller的scrollToIndex()事件，控制城市列表内容的改变，实现二者数据的联动效果。
    
    ```
    Stack({ alignContent: Alignment.End }) {
      List({ scroller: this.cityScroller }) {
        // ...
      }
      // ...
      .onScrollIndex((index: number) => {
        this.selectNavIndex = index - 2;
      })
    
      Column() {
        List({ scroller: this.navListScroller }) {
          ForEach(this.groupWorldList, (item: string, index: number) => {
            ListItem() {
              Text(item)
                // ...
                .onClick(() => {
                  this.selectNavIndex = index;
                  this.isClickScroll = true;
                  this.cityScroller.scrollToIndex(index + 2, false, ScrollAlign.START);
                })
            }
          }, (item: string) => item)
        }
      }
      // ...
    }
    ```
    

**实现效果**

#### 二级联动场景

**场景描述**

通过左边一级列表的选择，联动更新右边二级列表的数据，常用于商品分类选择、编辑风格等二级类别选择页面。

本场景以商品分类列表页面为例，分别通过List组件，对左侧分类导航和右侧导航内容进行展示。在进入页面后，点击左侧分类导航，右侧展示对应导航分类详情列表数据；滑动右侧列表内容，列表标题吸顶展示，左侧对应导航内容则高亮显示。

| 页面整体结构图 | 页面效果图 |
| --- | --- |
|  |  |

**实现原理**

左右各用一个List实现，分别设置其onScrollIndex()事件，左侧List在回调中判断数据项切换时，调用右侧List滚动到相应类别的对应位置，右侧同理。

**开发步骤**

1.  分别通过List组件构建左侧分类导航数据和右侧分类内容数据。
    
    ```
    List({ scroller: this.navTitleScroller }) {
      ForEach(this.categoryList, (item: NavTitleModel, index: number) => {
        ListItem() {
          Text(item.titleName)
            // ...
        }
      }, (item: NavTitleModel) => JSON.stringify(item.titleName))
    }
    // ...
    
    List({ scroller: this.goodsListScroller }) {
      ForEach(this.categoryList, (item: NavTitleModel) => {
        ListItemGroup({ space: 12, header: this.goodsHeaderBuilder(item.titleName) }) {
          ForEach(item.goodsList, (goodsItem: GoodsDataModel) => {
            ListItem() {
              Row() {
                Image(goodsItem.imgUrl)
                  // ...
                Column() {
                  Text(goodsItem.goodsName)
                    // ...
                  Text('￥' + goodsItem.price)
                    // ...
                }
                // ...
              }
              // ...
            }
          }, (goodsItem: GoodsDataModel) => JSON.stringify(goodsItem.goodsId))
        }
      }, (item: NavTitleModel) => JSON.stringify(item.goodsList))
    }
    ```
    
2.  给左侧导航列表添加点击事件，右侧分类详情列表添加onScrollIndex()事件，并调用自定义事件listChange方法，在listChange方法内部根据isGoods变量的值，调用对应列表控制器的scrollToIndex()事件，实现导航列表和分类详情数据的联动效果。
    
    ```
    listChange(index: number, isGoods: boolean) {
      if (this.currentTitleId !== index) {
        this.currentTitleId = index;
        if (isGoods) {
          this.goodsListScroller.scrollToIndex(index);
        } else {
          this.navTitleScroller.scrollToIndex(index);
        }
      }
    }
    ```
    
    ```
    List({ scroller: this.navTitleScroller }) {
      ForEach(this.categoryList, (item: NavTitleModel, index: number) => {
        ListItem() {
          Text(item.titleName)
            // ...
            .onClick(() => {
              this.listChange(index, true);
            })
        }
      }, (item: NavTitleModel) => JSON.stringify(item.titleName))
    }
    // ...
    
    List({ scroller: this.goodsListScroller }) {
      // ...
    }
    // ...
    .onScrollIndex((index: number) => {
      this.listChange(index, false)
    })
    ```
    

**实现效果**

#### 示例代码

*   基于List组件实现常见列表流场景