DEV Community

HarmonyOS
HarmonyOS

Posted on

List nests Grid to display content by categories

Read the original article:List nests Grid to display content by categories

Problem Description

How to use List to nest Grid to achieve content classification display effect? How to achieve this combination layout?

Background Knowledge

The List component contains a series of list items of the same width. It is suitable for presenting similar data in multiple lines, such as pictures and text.

The Grid container is composed of cells divided by "rows" and "columns". Various layouts can be made by specifying the cells where the "items" are located.

The aboutToAppear function is executed after a new instance of a custom component is created and before its build() function is executed.

Solution

ListGridDemo.ets:

Define the state variable curIndex to track the currently selected tab index, and the tabItem array to store the names of each category.
Implement the tabBuilder method to build the style of each tab, that is, the font change when selected.
ForEach loops through the tabItem array and creates TabContent for each tab. Each TabContent contains a Scroll component, which embeds an ItemsPageView component and passes the tabBarIndex parameter.

The sample code is as follows:

import { ItemsPageView } from './ItemsPageView';

@Entry
@Component
struct ListGridDemo {
  @State curIndex: number = 0;
  @State tabItem: Array<string> =
    ['All', 'Consultation', 'Doctor', 'Meridian', 'Diet', 'Tea', 'Exercise', 'Foot Bath']

  @Builder
  tabBuilder(index: number, name: string) {
    Column() {
      Text(name)
        .fontSize(this.curIndex === index ? 20 : 16)
        .fontColor(Color.Black)
        .fontWeight(this.curIndex === index ? FontWeight.Bold : FontWeight.Normal)
        .id(index.toString())
        .margin({ bottom: 10 })
      if (this.curIndex === index) {
        Text()
          .width(16)
          .borderRadius(5)
          .backgroundColor(Color.White)
          .height(5)
      }
    }
    .margin({ left: 10, right: 10 })
    .width(this.curIndex === index ? name.length * 20 : name.length * 16)
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .height(56)
  }

  build() {
    Column() {
      Stack({ alignContent: Alignment.TopStart }) {
        Column() {
          Tabs({ index: this.curIndex, barPosition: BarPosition.Start }) {
            ForEach(this.tabItem, (item: string, index: number) => {
              TabContent() {
                Scroll() {
                  Column() {
                    ItemsPageView({ tabBarIndex: this.curIndex })
                  }
                }
              }
              .tabBar(this.tabBuilder(index, item))
            })
          }
          .onTabBarClick((index: number) => {
            this.curIndex = index
          })
          .onChange((index: number) => {
            this.curIndex = index
          })
          .layoutWeight(1)
          .barOverlap(false)
          .barMode(BarMode.Scrollable)
          .barHeight(56)
        }
      }
      .height('100%')
      .width('100%')
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

ItemsPageView.ets:

Define two data models, gridItems and listItems, for grid and list layouts respectively.
Use ListItemAdapter to manage data, add gridItems and listItems to the Adapter in the aboutToAppear method.
Use LazyForEach to traverse each data item in the Adapter and decide whether to render ListComponent or GridComponent based on the itemType field.

The sample code is as follows:

import { ListItemAdapter } from "./ListItemAdapter"

@Component
export struct ItemsPageView {
  @Prop tabBarIndex: number
  @State private Adapter: ListItemAdapter<DataModel> = new ListItemAdapter();
  gridItems: DataModel = {
    itemType: ItemType.gridItems,
    "result": [
      {
        "name": "grid_1",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "grid_2",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "grid_3",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "grid_4",
        "icon": "resources/base/media/startIcon.png"
      }
    ]
  }
  listItems: DataModel = {
    itemType: ItemType.listItems,
    "result": [
      {
        "name": "list_1",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_2",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_3",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_4",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_5",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_6",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_7",
        "icon": "resources/base/media/startIcon.png"
      },
      {
        "name": "list_8",
        "icon": "resources/base/media/startIcon.png"
      }
    ]
  }

  aboutToAppear(): void {
    let tmp: DataModel[] = []
    tmp.push(this.listItems)
    tmp.push(this.gridItems)
    this.Adapter.addList(tmp)
  }

  build() {
    Column() {
      LazyForEach(this.Adapter, (item: DataModel) => {
        if (item.itemType === ItemType.listItems) {
          ListComponent({ listItemData: item.result })
        } else if (item.itemType === ItemType.gridItems) {
          GridComponent({ gridItemData: item.result })
        }
      })
    }
    .height('auto')
    .padding(10)
  }
}

export interface Grids {
  icon?: Resource
  name: string
}

@Component
export struct GridComponent {
  girdAdapter: ListItemAdapter<Grids> = new ListItemAdapter();
  gridItemData: Array<Grids> = []

  aboutToAppear(): void {
    this.girdAdapter.addList(this.gridItemData)
  }

  build() {
    Grid() {
      LazyForEach(this.girdAdapter, (item: Grids) => {
        GridItem() {
          Column() {
            Image(item.icon)
              .borderRadius(25)
              .width(50)
              .aspectRatio(1)
            Text(item.name).fontSize(16).margin({ top: 15 })
          }
        }
      }, (item: Grids) => item.name)
    }
    .layoutDirection(GridDirection.Column)
    .columnsTemplate('1fr 1fr')
    .rowsGap(15)
    .columnsGap(10)
    .padding({ top: 10, bottom: 10 })
    .height(210)
    .width('100%')
  }
}

export interface Lists {
  icon?: Resource
  name: string
}

@Component
export struct ListComponent {
  listAdapter: ListItemAdapter<Lists> = new ListItemAdapter();
  listItemData: Array<Lists> = []

  aboutToAppear(): void {
    this.listAdapter.addList(this.listItemData)
  }

  build() {
    Column() {
      LazyForEach(this.listAdapter, (item: Lists) => {
        Column() {
          Image(item.icon)
            .borderRadius(25)
            .width(50)
            .aspectRatio(1)
          Text(item.name)
            .fontSize(16)
            .margin({ top: 15 })
        }
      }, (item: Lists) => item.name)
    }
    .padding({ top: 10, bottom: 10 })
    .height('auto')
    .width('100%')
  }
}

export enum ItemType {
  listItems = 'listItems',
  gridItems = 'gridItems'
}

export class DataModel {
  itemType: ItemType = ItemType.listItems
  result: ESObject
}
Enter fullscreen mode Exit fullscreen mode

ListItemAdapter.ets

Implement a generic class that implements the IDataSource interface.
Maintain a list of items and data change listeners.

The sample code is as follows:

export class ListItemAdapter<T> implements IDataSource {
  private listItems: T[] = [];
  private listeners: DataChangeListener[] = [];

  getList(): T[] {
    return this.listItems;
  }

  setList(list: T[]) {
    this.listItems = list;
  }

  addList(list: T[]) {
    this.listItems = this.listItems.concat(list);
    this.notifyDataAdd(this.listItems.length - 1);
  }

  totalCount(): number {
    return this.listItems.length;
  }

  getData(index: number): T {
    return this.listItems[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      // AppLog.i(TAG, 'add listener');
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Written by Emine Inan

Top comments (0)