We use essential cookies for the website to function, as well as analytics cookies for analyzing and creating statistics of the website performance. To agree to the use of analytics cookies, click "Accept All". You can manage your preferences at any time by clicking "Cookie Settings" on the footer. More Information.

Only Essential Cookies
Accept All
Best PracticesLayout and Pop-up WindowsScrollComponents-based Waterfall Layout

ScrollComponents-based Waterfall Layout

Overview

The waterfall layout is widely adopted in app development. It uses container layout rules to arrange elements from top to bottom, forming a multi-column UI where the content cascades down like a waterfall.

The waterfall layout is suitable for displaying various types of data such as images, shopping items, and live videos. When you swipe up and down in a waterfall layout, a large amount of data can be displayed with the infinite loading feature. However, elements of varying sizes lead to performance consumption during measurement and drawing. This topic describes how to develop a page in waterfall layout using the third-party library ScrollComponents for scenarios such as cross-page reuse, first-screen rendering acceleration, infinite scrolling, pull-down refresh, and pull-up loading.

ScrollComponents is designed to address issues of component reuse and implements high-performance scrolling through minimal code. With this library, you do not need to focus on recycled pool management or other performance optimization details. For details about how to install and use ScrollComponents, see the ScrollComponents usage guide. ScrollComponents provides the following features:

  • Smooth scrolling for pages in waterfall layout.
  • Lazy loading, which eliminates the need for using LazyForEach and defining IDataSource, reducing code volume.
  • Component reuse, which prevents frame drops during scrolling and improves scrolling performance.
  • Recycled pool sharing, which enables component reuse across pages and parent components.
  • Pre-creation, which reduces initial frame drops upon cold startup and improves scrolling performance.
  • Preloading, which loads data in advance during scrolling to improve browsing experience.

Based on system capabilities such as NodeAdapter, BuilderNode, FrameNode, Prefetcher and FrameCallback, ScrollComponents implements high-performance scrolling through efficient component reuse, frame-by-frame component pre-creation, dynamic content creation, and lazy loading. In addition, with the WaterFlow component created by FrameNode, ScrollComponents provides the WaterFlowManager API for waterfall page rendering and other capabilities of the WaterFlow component, simplifies the component usage and facilitates future capability expansion while meeting your normal development requirements.

How to Implement

Key Technologies

ScrollComponents encapsulates NodeContainer and FrameNode at the bottom layer, and implements capabilities such as lazy loading, component reuse, and component pre-creation using NodeAdapter, BuilderNode, and custom recycled pool. In addition, it provides the WaterFlowManager component and various system scrollable component capabilities to achieve high-performance scrolling effect. You only need to pass in a data source and viewManager to quickly implement lazy loading and component reuse, focusing more on service development.

Figure 1 shows the overall flowchart of RecyclerView. When a node is removed from the visible area, NodeAdapter notifies the view manager to recycle the component, which is processed by NodeFactory and eventually stored in the component recycled pool. When NodeAdapter notifies viewManager that a new node is required, NodeFactory requests a node from the recycled pool, updates and assembles the components, and returns the node. Finally, NodeAdapter adds the returned node to the visible area.

Figure 1 Overall flowchart of RecyclerView

How to Develop

  1. Create a waterfall view manager.

    WaterFlowManager has only basic view capabilities. You need to customize a class that inherits from WaterFlowManager and implement the onWillCreateItem() method to obtain components from the recycled pool and enable the reuse capability. For details, see Reusing Child Node Templates.

    In the created view manager instance, the defaultNodeItem attribute indicates the name of the default node template, and the context attribute indicates the UI context.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. import { NodeItem, RecyclerView, WaterFlowManager } from '@hadss/scroll_components';
    2. class MyWaterFlowManager extends WaterFlowManager {
    3. onWillCreateItem(index: number, data: BlogData) {
    4. // ...
    5. }
    6. }
    7. @Entry
    8. @Component
    9. struct StandardWaterFlowPage {
    10. waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
    11. defaultNodeItem: 'StandardGridImageContainer',
    12. context: this.getUIContext()
    13. });
    14. // ...
    15. }
    NOTE

    To quickly create a waterfall layout with ScrollComponents and improve scrolling efficiency through lazy loading and pre-creation, use WaterFlowManager provided by ScrollComponents without worrying out component reuse. For details, see the quick start in the ScrollComponents usage guide.

  2. Initialize the WaterFlow component.

    During page initialization, call the setViewStyle() method of the view manager to set attributes for the WaterFlow component.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. this.waterFlowView.setViewStyle({ scroller: this.scroller })
    2. .height(CommonConstants.FULL_HEIGHT)
    3. .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE)
    4. .columnsGap(CommonConstants.COLUMNS_GAP)
    5. .rowsGap(CommonConstants.ROWS_GAP)
    6. .padding({
    7. top: CommonConstants.PADDING,
    8. left: CommonConstants.PADDING,
    9. right: CommonConstants.PADDING
    10. })
    11. .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) })
  3. Set the data source for the data-rendering component.
    1. Call the setDataSource() method of the view manager to set the data source. ScrollComponents supports lazy loading by default and provides APIs for adding, deleting, modifying, and querying data. You do not need to manage LazyForEach constraints or define a DataSource explicitly; just integrate and go. For details about the lazy loading APIs, see the description for providing the lazy loading capability for the view manager based on NodeAdapter.
    2. Call the registerNodeItem method of the view manager to register the child node item template and pass in the template name and @Builder constructor.
    3. ScrollComponents provides the view placeholder component RecyclerView, which can render a waterfall list after being bound to a view container instance.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. import { NodeItem, RecyclerView, WaterFlowManager } from '@hadss/scroll_components';
    2. @Entry
    3. @Component
    4. struct StandardWaterFlowPage {
    5. waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
    6. defaultNodeItem: 'StandardGridImageContainer',
    7. context: this.getUIContext()
    8. });
    9. // ...
    10. @State dataArray: BlogData[] = [] // Bind data source for data iteration
    11. aboutToAppear(): void {
    12. // ...
    13. generateRandomBlogData().then((data: BlogData[]) => {
    14. this.waterFlowView.setDataSource(data);
    15. this.dataArray = data;
    16. });
    17. this.waterFlowView.registerNodeItem('StandardGridImageContainer', wrapBuilder(StandardGridImageContainer));
    18. // ...
    19. }
    20. // ...
    21. build() {
    22. Column() {
    23. // ...
    24. RecyclerView({
    25. viewManager: this.waterFlowView
    26. })
    27. }
    28. .height(CommonConstants.FULL_HEIGHT)
    29. .backgroundColor($r('app.color.home_background_color'))
    30. }
    31. }
    32. // Define an item template
    33. @Builder
    34. function StandardGridImageContainer($$: Params) {
    35. GridImageView({ blogItem: $$.blogItem })
    36. }
    NOTE

    1. The @Builder function used in the registerNodeItem() method can only be defined globally.

    2. this.waterFlowView.preCreate() can be used to pre-create a component only after the node template is registered by calling the registerNodeItem() method.

  4. Reuse a child node template.
    After customizing a template, you need to implement the onWillCreateItem API when defining the waterfall view manager. In this API, you can obtain reusable nodes by calling dequeueReusableNodeByType(). You also need to update data in the aboutToReuse lifecycle of the reused component.
    1. List items with the same structure

      If the reused FlowItem components have the same structure, register the node template.

      Collapse
      Word wrap
      Dark theme
      Copy code
      1. class MyWaterFlowManager extends WaterFlowManager {
      2. onWillCreateItem(index: number, data: BlogData) {
      3. let node: NodeItem<Params> | null = this.dequeueReusableNodeByType('StandardGridImageContainer');
      4. node?.setData({ blogItem: data });
      5. return node;
      6. }
      7. }
      8. @Entry
      9. @Component
      10. struct StandardWaterFlowPage {
      11. waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
      12. defaultNodeItem: 'StandardGridImageContainer',
      13. context: this.getUIContext()
      14. });
      15. // ...
      16. aboutToAppear(): void {
      17. // ...
      18. this.waterFlowView.registerNodeItem('StandardGridImageContainer', wrapBuilder(StandardGridImageContainer));
      19. // ...
      20. }
      21. // ...
      22. }
      23. // Define an item template
      24. @Builder
      25. function StandardGridImageContainer($$: Params) {
      26. GridImageView({ blogItem: $$.blogItem })
      27. }
    2. List items with composable child components

      If the reused FlowItem components have similar structures with slight variations, you need to define different @Builder functions to implement component reuse. For example, the components have identical headers and footers but different middle content like Text or Image components. In this case, PartReuse provided by ScrollComponents handle component reuse, with only one @Builder function defined. For details, see the description about list items with composable child components for component reuse.

      When a component is to be destroyed, it is removed from the view container and enters the item recycled pool. When a component is to be created, the item node is obtained from the item recycled pool. If the item node is different from the target node, the component in PartReuse (the differential one) is recycled to the corresponding component recycled pool, combined with the item node to form the target component, and added to the view container.

      Figure 2 Process of component reuse

      You can check whether the reuse is successful based on the log "generateItem reuse" in Figure 3.

      Figure 3 Logs
      NOTE

      Logs are disabled by default. To enable logs, set the Config parameter in the initialization method of WaterFlowManager and set debug to true.

      Collapse
      Word wrap
      Dark theme
      Copy code
      1. import { NodeItem, PartReuse, RecyclerView, WaterFlowManager } from '@hadss/scroll_components';
      2. @Component
      3. struct BlogItem {
      4. @State blogItem: BlogData = new BlogData()
      5. // In reusable components, you must use aboutToReuse to update data, just like native recycling.
      6. aboutToReuse(params: ESObject): void {
      7. this.blogItem = params.blogItem;
      8. }
      9. aboutToRecycle(): void {
      10. this.blogItem.callback = undefined;
      11. this.blogItem.fetchUrl = ImageContent.EMPTY;
      12. }
      13. build() {
      14. Column({ space: 12 }) {
      15. HeaderComponent({ blogItem: this.blogItem })
      16. if (this.blogItem?.content.length > 0) {
      17. // cache component.
      18. PartReuse({
      19. type: 'AdaptiveTextComponent',
      20. builder: wrapBuilder(AdaptiveTextComponentContainer),
      21. data: { blogItem: this.blogItem }
      22. })
      23. }
      24. if (this.blogItem?.images && this.blogItem.images.length > 0) {
      25. PartReuse({
      26. type: 'GridImageViewContainer',
      27. builder: wrapBuilder(GridImageViewContainer),
      28. data: { blogItem: this.blogItem }
      29. })
      30. }
      31. BottomContent({ blogItem: this.blogItem })
      32. }
      33. .padding(12)
      34. .backgroundColor(Color.White)
      35. .borderRadius(12)
      36. }
      37. }
      38. @Builder
      39. export function AdaptiveTextComponentContainer($$: Params) {
      40. AdaptiveTextComponent({ blogItem: $$.blogItem })
      41. }
      42. @Builder
      43. function GridImageViewContainer($$: Params) {
      44. GridImageView({ blogItem: $$.blogItem })
      45. }

    3. List items with different structures

      If the FlowItem structures differ significantly in terms of layout, number of differential components, and component types, you can define multiple reuse templates.

      Collapse
      Word wrap
      Dark theme
      Copy code
      1. class MyWaterFlowManager extends WaterFlowManager {
      2. onWillCreateItem(index: number, data: BlogData) {
      3. let node: NodeItem<Params>
      4. if (index % 2 === 0) {
      5. node = this.dequeueReusableNodeByType('ImageContainer');
      6. } else {
      7. node = this.dequeueReusableNodeByType('TextContainer');
      8. }
      9. node?.setData({ blogItem: data });
      10. return node;
      11. }
      12. }
      13. @Entry
      14. @Component
      15. struct MultiFlowItemPage {
      16. waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
      17. defaultNodeItem: 'ImageContainer',
      18. context: this.getUIContext()
      19. });
      20. scroller: Scroller = new Scroller();
      21. @State dataArray: BlogData[] = [] // Bind data source for data iteration
      22. aboutToAppear(): void {
      23. this.initView();
      24. taskpool.execute(generateRandomBlogData).then((data: ESObject) => {
      25. this.dataArray = data;
      26. this.waterFlowView.setDataSource(data);
      27. })
      28. this.waterFlowView.registerNodeItem('ImageContainer', wrapBuilder(ImageContainer));
      29. this.waterFlowView.registerNodeItem('TextContainer', wrapBuilder(TextContainer));
      30. // ...
      31. }
      32. initView() {
      33. this.waterFlowView.setViewStyle({ scroller: this.scroller })
      34. // ...
      35. }
      36. build() {
      37. Column() {
      38. RecyclerView({
      39. viewManager: this.waterFlowView
      40. })
      41. }
      42. .height(CommonConstants.FULL_HEIGHT)
      43. .backgroundColor($r('app.color.home_background_color'))
      44. }
      45. }
      46. // Reusable Image Component Template.
      47. @Builder
      48. function ImageContainer($$: Params) {
      49. ImageContainerView({ blogItem: $$.blogItem })
      50. }
      51. // Reusable Text Component Template.
      52. @Builder
      53. function TextContainer($$: Params) {
      54. TextContainerView({ blogItem: $$.blogItem })
      55. }

Cross-Page Reuse of WaterFlow

When to Use

WaterFlow may be reused on multiple pages, for example, tabs. ScrollComponents allows you to reuse WaterFlow globally.

Figure 4 Final effect

How to Develop

  1. Define the recycled pool singleton.

    ScrollComponents generates a RecycledPool object by default. You can define a recycled pool singleton to store this object for cross-page use. The following singleton is for reference only. You can encapsulate it based on actual demands.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. import { RecycledPool } from '@hadss/scroll_components';
    2. export class Utils {
    3. // ...
    4. private static utils_: Utils;
    5. nodePool: RecycledPool | null = null;
    6. // ...
    7. }
  2. Use the recycled pool singleton to save the RecycledPool object on the first waterfall page.

    WaterFlowManager provides the getRecyclePool() method to obtain the RecyclePool object and store it in the global singleton.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. if (Utils.getInstance().nodePool) {
    2. // Registration Reuse Pool
    3. this.waterFlowView.registerRecyclePool(Utils.getInstance().nodePool!);
    4. } else {
    5. Utils.getInstance().nodePool = this.waterFlowView.getRecyclePool();
    6. }
  3. Share the RecyclePool object in the singleton across pages.

    Use the registerRecyclePool() method to register the RecyclePool object in the global singleton with the WaterFlow object defined on the page to implement recycled pool sharing of different RecyclerViews across pages.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. @Component
    2. export struct SharedPoolSecondPage {
    3. waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
    4. defaultNodeItem: 'TestBlogItemContainer',
    5. context: this.getUIContext()
    6. });
    7. // ...
    8. aboutToAppear(): void {
    9. // ...
    10. if (Utils.getInstance().nodePool) {
    11. // Registration Reuse Pool.
    12. this.waterFlowView.registerRecyclePool(Utils.getInstance().nodePool!);
    13. } else {
    14. Utils.getInstance().nodePool = this.waterFlowView.getRecyclePool();
    15. }
    16. // ...
    17. }
    18. // ...
    19. }

Accelerated Rendering for the First Waterfall Page

When to Use

When users first access a waterfall page upon a cold start, a blank screen or white patches may be displayed due to numerous media resources such as images or videos, which can take a few seconds to load and gradually display content. ScrollComponents supports component pre-creation, so users can immediately see text and image outlines as soon as the page opens, minimizing lag.

Figure 5 Final effect

How to Develop

Use the preCreate() method to pre-create the reuse template. The core code is as follows:

Collapse
Word wrap
Dark theme
Copy code
  1. aboutToAppear(): void {
  2. // ...
  3. // register components.
  4. this.waterFlowView.registerNodeItem('BlogItemContainer', wrapBuilder(BlogItemContainer));
  5. this.waterFlowView.registerNodeItem('AdaptiveTextComponent', wrapBuilder(AdaptiveTextComponentContainer));
  6. this.waterFlowView.registerNodeItem('GridImageViewContainer', wrapBuilder(GridImageViewContainer));
  7. this.waterFlowView.preCreate('BlogItemContainer', 30);
  8. this.waterFlowView.preCreate('AdaptiveTextComponent', 30);
  9. this.waterFlowView.preCreate('GridImageViewContainer', 30);
  10. // ...
  11. }

Performance Test

Load the same data to compare the completion latency results. Simulate the network request scenario a 1-second delay after the cold start.

@Reusable: During network requests, the main thread is idle for a long time. After the requests are complete, it takes a long time to draw frames for the first screen components.

Figure 6 Test result of using @Reusable

ScrollComponents: During network requests, the main thread is idle for a short time. After the requests are complete, it takes a short time to draw frames for the first screen components.

Figure 7 Test result of using ScrollComponents
Table 1 Duration comparison for the first screen component creation
Expand
  

Cold Start Latency

Main Thread Idle Duration

First Screen Component Creation Duration

ScrollComponents

2.5s

281 ms

223 ms

@Reusable

2.8s

997 ms

467 ms

As shown in the preceding table, ScrollComponents has a shorter completion latency (shortened by 300 ms) than the native @Reusable in the cold start scenario.

Infinite Scrolling of WaterFlow

When to Use

When a waterfall page contains numerous images or videos, white spaces (or blank areas) may occur when users swipe down quickly to the bottom of the list. This issue is particularly evident when users are accessing a lot of online content on a weak network and scrolling rapidly.

To minimize these white spaces caused by poor network during fast swiping, ScrollComponents provides the built-in Prefetcher, which supports dynamic adaptation to network conditions. This feature allows for resources like images to be loaded in advance, ensuring that they are ready for immediate display when needed, thereby reducing the occurrence of white spaces. Dynamic preloading is applicable to scenarios where data requests take a long time, such as when scrolling through a list filled with a large number of images.

Figure 8 Final effect

How to Develop

  1. When creating a component instance, register the callbacks for the fetch and cancel operations with the view manager and set the data source.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. // Registers the callback that prefetcher invokes when a data referenced by a data source item needs to be fetched.
    2. this.waterFlowView.registerFetchCallback(this.fetchCallback);
    3. /**
    4. * Registers the callback that prefetcher invokes when a specific fetch should be
    5. * canceled to avoid wasting system resources, such as network bandwidth.
    6. */
    7. this.waterFlowView.registerCancelCallback(this.cancelCallback);
    8. taskpool.execute(generateRandomBlogData).then((data: ESObject) => {
    9. this.data = data;
    10. this.waterFlowView.setDataSource(data);
    11. }).catch((err: Error) => {
    12. const error = err as BusinessError
    13. Logger.error(`generateRandomBlogData failed, code = ${error.code} message = ${error.message}`)
    14. });
  2. Implement the fetchCallback() and cancelCallback() methods.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. fetchCallback: (item: ESObject, fetchId: number) => Promise<void> = (item: ESObject, fetchId: number) => {
    2. let data = item as BlogData;
    3. if (data.images.length == 0) {
    4. return Promise.resolve();
    5. }
    6. let url = data.images[0];
    7. if (this.imageCaches.has(url)) {
    8. // If cached, skip re-downloading and load the data directly.
    9. if (data.callback) {
    10. data.callback(this.imageCaches.get(url));
    11. } else {
    12. data.fetchUrl = this.imageCaches.get(url) as string
    13. }
    14. return Promise.resolve();
    15. }
    16. this.fetches.set(fetchId, data);
    17. this.fetchAgent.postMessageWithSharedSendable({
    18. type: 'fetch',
    19. cachePath: this.cachePath,
    20. url: data.images[0],
    21. fetchId: fetchId
    22. });
    23. return Promise.resolve();
    24. }
    25. cancelCallback: (fetchId: number) => void = (fetchId: number) => {
    26. this.fetches.delete(fetchId);
    27. this.fetchAgent.postMessageWithSharedSendable({ type: 'cancel', fetchId: fetchId });
    28. }

  3. Listen for changes to the scrollable child components.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. this.waterFlowView.setViewStyle({
    2. scroller: this.scroller
    3. })
    4. // ...
    5. .onScrollIndex((start: number, end: number) => {
    6. if (end > 0) {
    7. /**
    8. * Call this method when the visible area boundaries change.
    9. * The prefetcher will start prefetching after the first call to this method
    10. * in all cases where the autoStart option is not set to false.
    11. */
    12. this.waterFlowView.visibleAreaChanged(start, end);
    13. }
    14. // ...
    15. })
    16. .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean) => {
    17. /**
    18. * By default, the prefetcher begins invoking user code to fetch data with the first call to the visibleAreaChanged method.
    19. * Sometimes, this can waste resources because, in practice, onScrollIndex triggers the callback even when the component is not actually visible to the user.
    20. * To avoid this, subscribe to the onVisibleAreaChange event.
    21. */
    22. if (isVisible) {
    23. // Call this method to start prefetching.
    24. this.waterFlowView.nodeAdapter.prefetcher?.start();
    25. } else {
    26. // Call this method to stop prefetching. For instance, this should be done if a related component becomes invisible.
    27. this.waterFlowView.nodeAdapter.prefetcher?.stop();
    28. }
    29. })
  4. Modify the data structure.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. // Preload images using prefetch.
    2. Image(this.fetchUrl)
    3. .sourceSize({ width: 100, height: 100 })
    4. .width(CommonConstants.FULL_WIDTH)
    5. .aspectRatio(1)
    6. .objectFit(ImageFit.Cover)

Performance Test

Swipe the waterfall page containing the same data in a moderate and weak network condition respectively, and you can see the comparison of the occurrence of white spaces in the table below.

  • Moderate network condition: The occurrence of white spaces decreases by 10% when the Prefetcher is used.
  • Weak network condition: The occurrence of white spaces decreases by 17% when the Prefetcher is used.
Table 2 Comparison of the occurrence of white space occurrences in different network conditions
  

White Space Loading Duration

White Space Occurrence Rate

Decrease Rate

Native

Prefetcher

Moderate network condition

> 100 ms

18.89%

8.27%

10.62%

> 40 ms

19.23%

8.47%

10.76%

Weak network condition

> 100 ms

55.38%

38.03%

17.35%

> 40 ms

55.43%

38.05%

17.38%

Pull-Down-to-Refresh in the Waterfall Layout

When to Use

Pull-down-to-refresh is a key function for improving user experience, which ensures seamless data loading and smooth interactions. You are advised to use lazy loading for data updates, preventing UI rendering from being blocked by media resource loading. For details about the implementation logic, see Implementing Pull-Down-to-Refresh and Pull-Up-to-Load-More.

Figure 9 Final effect

How to Develop

You can use the PullToRefresh component to wrap the waterfall page to implement the pull-down-to-refresh feature. Use the onRefresh() method to add data. The core code is as follows:

  1. Define a listener to listen for data refresh and simulate data updates.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. generateRandomBlogData().then((data: ESObject) => {
    2. this.data = data;
    3. this.waterFlowView.setDataSource(data);
    4. });
  2. Bind the waterfall page to the PullToRefresh component.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. @Builder
    2. getWaterFlow() {
    3. RecyclerView({
    4. viewManager: this.waterFlowView
    5. })
    6. }

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. PullToRefresh({
    2. data: $data,
    3. scroller: this.scroller,
    4. customList: () => {
    5. this.getWaterFlow()
    6. },
    7. // ...
    8. onLoadMore: () => {
    9. return new Promise<string>((resolve) => {
    10. resolve('');
    11. generateRandomBlogData().then((data: ESObject) => {
    12. this.waterFlowView.nodeAdapter.pushData(data);
    13. });
    14. })
    15. }
    16. })
    17. .layoutWeight(1)

Pull-Up-to-Load in the Waterfall Layout

When a waterfall page involves a large amount of data and requests for pagination, you can use lazy loading of ScrollComponents to implement the pull-up-to-load feature.

Figure 10 Final effect

The core code is as follows:

Collapse
Word wrap
Dark theme
Copy code
  1. PullToRefresh({
  2. data: $data,
  3. scroller: this.scroller,
  4. customList: () => {
  5. this.getWaterFlow()
  6. },
  7. onRefresh: () => {
  8. return new Promise<string>((resolve) => {
  9. isChinese()? resolve ('Refresh succeeded') : resolve ('Refresh succeeded')
  10. generateRandomBlogData().then((data: ESObject) => {
  11. this.data = data;
  12. this.waterFlowView.setDataSource(data);
  13. });
  14. })
  15. },
  16. // ...
  17. })
  18. .layoutWeight(1)

NOTE

Currently, footer and footerContent cannot be set when FrameNode creates WaterFlow.

Long-Press-to-Delete in the Waterfall Layout

When a user long presses an item, the delete button is displayed. The user can tap the button to delete the item.

Figure 11 Final effect
  1. Bind the long-press gesture.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. Stack() {
    2. Image(this.blogItem.images[0])
    3. // ...
    4. }
    5. // ...
    6. .priorityGesture(
    7. GestureGroup(GestureMode.Exclusive,
    8. LongPressGesture().onAction(() => {
    9. this.showMenu = true;
    10. }))
    11. )
  2. Delete the target data from the data source to delete the component.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. this.context.eventHub.on(CommonConstants.EVENT_REMOVE_ITEM, (blogItem: BlogData) => {
    2. let foundIndex =
    3. this.dataArray.findIndex((value: BlogData) => JSON.stringify(value) ===
    4. JSON.stringify(blogItem))
    5. if (foundIndex !== CommonConstants.NOT_FOUND_INDEX) {
    6. this.getUIContext().animateTo({ duration: 200 }, () => {
    7. this.waterFlowView.nodeAdapter.deleteData(foundIndex)
    8. })
    9. }
    10. })
    11. @Builder
    12. popUpBuilder() {
    13. Row({ space: 2 }) {
    14. Text($r('app.string.not_interested_button_text'))
    15. }
    16. .width(100)
    17. .height(50)
    18. .padding(5)
    19. .justifyContent(FlexAlign.Center)
    20. .onClick(() => {
    21. this.context.eventHub.emit(CommonConstants.EVENT_REMOVE_ITEM, this.blogItem)
    22. this.showMenu = false;
    23. })
    24. }

Mixed Section Layout on the Waterfall Page

When to Use

In the mixed section layout of the waterfall page, items vary based on areas. For example, the upper part displays one item per row, the middle part displays two items per row with consistent height, and the bottom part displays two items per row with different heights. For details about the mixed section layout, see Mixed Section Layout.

Figure 12 Final effect

How to Develop

Create a WaterFlowSections object, bind different FlowItem configuration information to the object, and specify parameters such as number of items, number of rows and columns, and spacing. The parameters are customized based on SectionOptions. The sample code is as follows:

  1. Set multiple SectionOptions.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. @State sections: WaterFlowSections = new WaterFlowSections();
    2. oneColumnSection: SectionOptions = {
    3. itemsCount: 3,
    4. crossCount: 1,
    5. columnsGap: 5,
    6. rowsGap: 10,
    7. margin: {
    8. top: 8,
    9. left: 0,
    10. bottom: 8,
    11. right: 0
    12. },
    13. onGetItemMainSizeByIndex: (index: number) => {
    14. if (index === 1) {
    15. return 100;
    16. } else {
    17. return 200;
    18. }
    19. }
    20. };
    21. twoColumnSection: SectionOptions = {
    22. itemsCount: 2,
    23. crossCount: 2,
    24. onGetItemMainSizeByIndex: () => {
    25. return 250;
    26. }
    27. };
  2. Bind SectionOptions to a section during initialization and bind the section to WaterFlow.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. initView() {
    2. let sectionOptions: SectionOptions[] = [];
    3. let count = 0;
    4. let oneOrTwo = 0;
    5. while (count < this.dataCount) {
    6. if (oneOrTwo++ % 2 == 0) {
    7. sectionOptions.push(this.oneColumnSection);
    8. count += this.oneColumnSection.itemsCount;
    9. } else {
    10. sectionOptions.push(this.twoColumnSection);
    11. count += this.twoColumnSection.itemsCount;
    12. }
    13. }
    14. this.sections.splice(-1, 0, sectionOptions);
    15. this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
    16. // ...
    17. // ...
    18. }

  3. Update the section during page scrolling. After the section is updated, call the initialize method to update the WaterFlow component created by FrameNode. This way, the updated section takes effect.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
    2. // ...
    3. .onScrollIndex((_first: number, last: number) => {
    4. if (last + 20 >= this.waterFlowView.nodeAdapter.totalNodeCount) {
    5. let dataArray: number[] = [];
    6. for (let i = 0; i < 100; i++) {
    7. dataArray.push(i)
    8. }
    9. // update data when the page is scrolling.
    10. this.waterFlowView.nodeAdapter.pushData(dataArray)
    11. let newSection: SectionOptions = {
    12. itemsCount: 100,
    13. crossCount: 2,
    14. onGetItemMainSizeByIndex: () => {
    15. return 100;
    16. }
    17. }
    18. // update section
    19. this.sections.push(newSection);
    20. this.waterFlowView.setViewStyle({
    21. scroller: this.scroller, // it's very important to do initialize that makes section update.
    22. sections: this.sections,
    23. })
    24. }
    25. })
    26. // ...

Ceiling-Mounted Component in the Waterfall Layout

When to Use

When the horizontal list component is swiped up, it will be pinned to the top and the list below continues scrolling.

Figure 13 Final effect

How to Develop

Listen for the scroll event of the waterfall page and set the position for the ceiling-mounted component based on the offset after list scrolling. In this way, the component can scroll with the list and can be pinned to the top when it reaches the specified position.

  1. Set SectionOptions.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. @State sections: WaterFlowSections = new WaterFlowSections();
    2. oneColumnSection: SectionOptions = {
    3. itemsCount: 3,
    4. crossCount: 1,
    5. columnsGap: 5,
    6. rowsGap: 10,
    7. margin: {
    8. top: 8,
    9. left: 0,
    10. bottom: 8,
    11. right: 0
    12. },
    13. onGetItemMainSizeByIndex: (index: number) => {
    14. if (index === 1) {
    15. return 100;
    16. } else {
    17. return 200;
    18. }
    19. }
    20. };
    21. twoColumnSection: SectionOptions = {
    22. itemsCount: 2,
    23. crossCount: 2,
    24. onGetItemMainSizeByIndex: () => {
    25. return 250;
    26. }
    27. };
  2. Bind SectionOptions to a section during initialization and bind the section to WaterFlow.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. initView() {
    2. let sectionOptions: SectionOptions[] = [];
    3. let count = 0;
    4. let oneOrTwo = 0;
    5. while (count < this.dataCount) {
    6. if (oneOrTwo++ % 2 == 0) {
    7. sectionOptions.push(this.oneColumnSection);
    8. count += this.oneColumnSection.itemsCount;
    9. } else {
    10. sectionOptions.push(this.twoColumnSection);
    11. count += this.twoColumnSection.itemsCount;
    12. }
    13. }
    14. this.sections.splice(-1, 0, sectionOptions);
    15. this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
    16. // ...
    17. // ...
    18. }
  3. Add a ceiling-mounted component to the reserved position and render FlowItem to exclude the index of the ceiling-mounted component.
    Collapse
    Word wrap
    Dark theme
    Copy code
    1. this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
    2. // ...
    3. .onWillScroll((offset: number) => {
    4. // Dynamically get the offset position of a waterfall flow
    5. this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
    6. })
    7. build() {
    8. Stack({ alignContent: Alignment.TopStart }) {
    9. RecyclerView({
    10. viewManager: this.waterFlowView
    11. })
    12. Stack() {
    13. // ...
    14. }
    15. .width(CommonConstants.FULL_WIDTH)
    16. .height(100)
    17. .padding({ left: CommonConstants.PADDING, right: CommonConstants.PADDING })
    18. .backgroundColor(Color.White)
    19. .hitTestBehavior(HitTestMode.Transparent)
    20. // Set the sticky component's offset.
    21. .position({ x: 0, y: this.scrollOffset >= 220 ? 0 : 220 - this.scrollOffset })
    22. }
    23. }

Dynamic Column Switching in the Waterfall Layout

Dynamically adjusting the column count allows apps to switch between list and waterfall modes or adapt to screen width changes. WaterFlow inherits from FrameNode. Use attribute of the node to modify that of WaterFlow. The sample code is as follows:

Collapse
Word wrap
Dark theme
Copy code
  1. aboutToAppear(): void {
  2. let orientation = window.Orientation.AUTO_ROTATION;
  3. this.windowClass.setPreferredOrientation(orientation, (err: BusinessError) => {
  4. const errCode: number = err.code;
  5. if (errCode) {
  6. Logger.error('Failed to set window orientation. Cause:' + JSON.stringify(err));
  7. return;
  8. }
  9. Logger.info('Succeed to setting window orientation');
  10. })
  11. this.windowClass.on('windowSizeChange', (size) => {
  12. let viewWidth = size.width;
  13. let viewHeight = size.height;
  14. if (viewWidth > viewHeight) {
  15. this.waterFlowView.setViewStyle().columnsTemplate('1fr 1fr 1fr');
  16. } else {
  17. this.waterFlowView.setViewStyle().columnsTemplate('1fr 1fr');
  18. }
  19. })
  20. this.initView();
  21. // ...
  22. }
  23. initView() {
  24. this.waterFlowView.setViewStyle({ scroller: this.scroller })
  25. .height(CommonConstants.FULL_HEIGHT)
  26. .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE)
  27. .columnsGap(CommonConstants.COLUMNS_GAP)
  28. .rowsGap(CommonConstants.ROWS_GAP)
  29. .padding({
  30. top: CommonConstants.PADDING,
  31. left: CommonConstants.PADDING,
  32. right: CommonConstants.PADDING
  33. })
  34. .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) })
  35. }

Waterfall Animations

Fading Edge

fadingEdge is used to enable the fading edge effect for the WaterFlow component, and the fadingEdgeLength parameter is used to set the fading edge length. For details, see Setting the Edge Fading Effect.

Collapse
Word wrap
Dark theme
Copy code
  1. this.waterFlowView.setViewStyle({ scroller: this.scroller })
  2. .height(CommonConstants.FULL_HEIGHT)
  3. .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE)
  4. .columnsGap(CommonConstants.COLUMNS_GAP)
  5. .rowsGap(CommonConstants.ROWS_GAP)
  6. .padding({
  7. top: CommonConstants.PADDING,
  8. left: CommonConstants.PADDING,
  9. right: CommonConstants.PADDING
  10. })
  11. .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) })

Animation for Deleting Components

Add animateTo when deleting a component to implement a smooth transition effect.

Collapse
Word wrap
Dark theme
Copy code
  1. this.context.eventHub.on(CommonConstants.EVENT_REMOVE_ITEM, (blogItem: BlogData) => {
  2. let foundIndex =
  3. this.dataArray.findIndex((value: BlogData) => JSON.stringify(value) ===
  4. JSON.stringify(blogItem))
  5. if (foundIndex !== CommonConstants.NOT_FOUND_INDEX) {
  6. this.getUIContext().animateTo({ duration: 200 }, () => {
  7. this.waterFlowView.nodeAdapter.deleteData(foundIndex)
  8. })
  9. }
  10. })
Search in Best Practices
Enter a keyword.