Intelligent Assistant
Chat with our virtual assistant to get answers promptly.
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:
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.
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.

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.
- import { NodeItem, RecyclerView, WaterFlowManager } from '@hadss/scroll_components';
- class MyWaterFlowManager extends WaterFlowManager {
- onWillCreateItem(index: number, data: BlogData) {
- // ...
- }
- }
- @Entry
- @Component
- struct StandardWaterFlowPage {
- waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
- defaultNodeItem: 'StandardGridImageContainer',
- context: this.getUIContext()
- });
- // ...
- }
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.
During page initialization, call the setViewStyle() method of the view manager to set attributes for the WaterFlow component.
- this.waterFlowView.setViewStyle({ scroller: this.scroller })
- .height(CommonConstants.FULL_HEIGHT)
- .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE)
- .columnsGap(CommonConstants.COLUMNS_GAP)
- .rowsGap(CommonConstants.ROWS_GAP)
- .padding({
- top: CommonConstants.PADDING,
- left: CommonConstants.PADDING,
- right: CommonConstants.PADDING
- })
- .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) })
- import { NodeItem, RecyclerView, WaterFlowManager } from '@hadss/scroll_components';
- @Entry
- @Component
- struct StandardWaterFlowPage {
- waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
- defaultNodeItem: 'StandardGridImageContainer',
- context: this.getUIContext()
- });
- // ...
- @State dataArray: BlogData[] = [] // Bind data source for data iteration
-
- aboutToAppear(): void {
- // ...
- generateRandomBlogData().then((data: BlogData[]) => {
- this.waterFlowView.setDataSource(data);
- this.dataArray = data;
- });
- this.waterFlowView.registerNodeItem('StandardGridImageContainer', wrapBuilder(StandardGridImageContainer));
- // ...
- }
-
- // ...
- build() {
- Column() {
- // ...
-
- RecyclerView({
- viewManager: this.waterFlowView
- })
-
- }
- .height(CommonConstants.FULL_HEIGHT)
- .backgroundColor($r('app.color.home_background_color'))
- }
- }
- // Define an item template
- @Builder
- function StandardGridImageContainer($$: Params) {
- GridImageView({ blogItem: $$.blogItem })
- }
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.
If the reused FlowItem components have the same structure, register the node template.
- class MyWaterFlowManager extends WaterFlowManager {
- onWillCreateItem(index: number, data: BlogData) {
- let node: NodeItem<Params> | null = this.dequeueReusableNodeByType('StandardGridImageContainer');
- node?.setData({ blogItem: data });
- return node;
- }
- }
- @Entry
- @Component
- struct StandardWaterFlowPage {
- waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
- defaultNodeItem: 'StandardGridImageContainer',
- context: this.getUIContext()
- });
- // ...
-
- aboutToAppear(): void {
- // ...
- this.waterFlowView.registerNodeItem('StandardGridImageContainer', wrapBuilder(StandardGridImageContainer));
- // ...
- }
-
- // ...
- }
- // Define an item template
- @Builder
- function StandardGridImageContainer($$: Params) {
- GridImageView({ blogItem: $$.blogItem })
- }
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.

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

Logs are disabled by default. To enable logs, set the Config parameter in the initialization method of WaterFlowManager and set debug to true.
- import { NodeItem, PartReuse, RecyclerView, WaterFlowManager } from '@hadss/scroll_components';
- @Component
- struct BlogItem {
- @State blogItem: BlogData = new BlogData()
-
- // In reusable components, you must use aboutToReuse to update data, just like native recycling.
- aboutToReuse(params: ESObject): void {
- this.blogItem = params.blogItem;
- }
-
- aboutToRecycle(): void {
- this.blogItem.callback = undefined;
- this.blogItem.fetchUrl = ImageContent.EMPTY;
- }
-
- build() {
- Column({ space: 12 }) {
- HeaderComponent({ blogItem: this.blogItem })
- if (this.blogItem?.content.length > 0) {
- // cache component.
- PartReuse({
- type: 'AdaptiveTextComponent',
- builder: wrapBuilder(AdaptiveTextComponentContainer),
- data: { blogItem: this.blogItem }
- })
- }
- if (this.blogItem?.images && this.blogItem.images.length > 0) {
- PartReuse({
- type: 'GridImageViewContainer',
- builder: wrapBuilder(GridImageViewContainer),
- data: { blogItem: this.blogItem }
- })
- }
-
- BottomContent({ blogItem: this.blogItem })
- }
- .padding(12)
- .backgroundColor(Color.White)
- .borderRadius(12)
- }
- }
- @Builder
- export function AdaptiveTextComponentContainer($$: Params) {
- AdaptiveTextComponent({ blogItem: $$.blogItem })
- }
- @Builder
- function GridImageViewContainer($$: Params) {
- GridImageView({ blogItem: $$.blogItem })
- }
If the FlowItem structures differ significantly in terms of layout, number of differential components, and component types, you can define multiple reuse templates.
- class MyWaterFlowManager extends WaterFlowManager {
- onWillCreateItem(index: number, data: BlogData) {
- let node: NodeItem<Params>
- if (index % 2 === 0) {
- node = this.dequeueReusableNodeByType('ImageContainer');
- } else {
- node = this.dequeueReusableNodeByType('TextContainer');
- }
- node?.setData({ blogItem: data });
- return node;
- }
- }
- @Entry
- @Component
- struct MultiFlowItemPage {
- waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
- defaultNodeItem: 'ImageContainer',
- context: this.getUIContext()
- });
- scroller: Scroller = new Scroller();
- @State dataArray: BlogData[] = [] // Bind data source for data iteration
-
- aboutToAppear(): void {
- this.initView();
- taskpool.execute(generateRandomBlogData).then((data: ESObject) => {
- this.dataArray = data;
- this.waterFlowView.setDataSource(data);
- })
-
- this.waterFlowView.registerNodeItem('ImageContainer', wrapBuilder(ImageContainer));
- this.waterFlowView.registerNodeItem('TextContainer', wrapBuilder(TextContainer));
-
- // ...
- }
-
- initView() {
- this.waterFlowView.setViewStyle({ scroller: this.scroller })
- // ...
- }
-
- build() {
- Column() {
- RecyclerView({
- viewManager: this.waterFlowView
- })
- }
- .height(CommonConstants.FULL_HEIGHT)
- .backgroundColor($r('app.color.home_background_color'))
- }
- }
- // Reusable Image Component Template.
- @Builder
- function ImageContainer($$: Params) {
- ImageContainerView({ blogItem: $$.blogItem })
- }
- // Reusable Text Component Template.
- @Builder
- function TextContainer($$: Params) {
- TextContainerView({ blogItem: $$.blogItem })
- }
WaterFlow may be reused on multiple pages, for example, tabs. ScrollComponents allows you to reuse WaterFlow globally.

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.
- import { RecycledPool } from '@hadss/scroll_components';
-
- export class Utils {
- // ...
- private static utils_: Utils;
- nodePool: RecycledPool | null = null;
- // ...
- }
WaterFlowManager provides the getRecyclePool() method to obtain the RecyclePool object and store it in the global singleton.
- if (Utils.getInstance().nodePool) {
- // Registration Reuse Pool
- this.waterFlowView.registerRecyclePool(Utils.getInstance().nodePool!);
- } else {
- Utils.getInstance().nodePool = this.waterFlowView.getRecyclePool();
- }
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.
- @Component
- export struct SharedPoolSecondPage {
- waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({
- defaultNodeItem: 'TestBlogItemContainer',
- context: this.getUIContext()
- });
- // ...
-
- aboutToAppear(): void {
- // ...
- if (Utils.getInstance().nodePool) {
- // Registration Reuse Pool.
- this.waterFlowView.registerRecyclePool(Utils.getInstance().nodePool!);
- } else {
- Utils.getInstance().nodePool = this.waterFlowView.getRecyclePool();
- }
- // ...
- }
-
- // ...
- }
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.

Use the preCreate() method to pre-create the reuse template. The core code is as follows:
- aboutToAppear(): void {
- // ...
- // register components.
- this.waterFlowView.registerNodeItem('BlogItemContainer', wrapBuilder(BlogItemContainer));
- this.waterFlowView.registerNodeItem('AdaptiveTextComponent', wrapBuilder(AdaptiveTextComponentContainer));
- this.waterFlowView.registerNodeItem('GridImageViewContainer', wrapBuilder(GridImageViewContainer));
-
- this.waterFlowView.preCreate('BlogItemContainer', 30);
- this.waterFlowView.preCreate('AdaptiveTextComponent', 30);
- this.waterFlowView.preCreate('GridImageViewContainer', 30);
- // ...
- }
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.

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.

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.
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.

- // Registers the callback that prefetcher invokes when a data referenced by a data source item needs to be fetched.
- this.waterFlowView.registerFetchCallback(this.fetchCallback);
- /**
- * Registers the callback that prefetcher invokes when a specific fetch should be
- * canceled to avoid wasting system resources, such as network bandwidth.
- */
- this.waterFlowView.registerCancelCallback(this.cancelCallback);
- taskpool.execute(generateRandomBlogData).then((data: ESObject) => {
- this.data = data;
- this.waterFlowView.setDataSource(data);
- }).catch((err: Error) => {
- const error = err as BusinessError
- Logger.error(`generateRandomBlogData failed, code = ${error.code} message = ${error.message}`)
- });
- fetchCallback: (item: ESObject, fetchId: number) => Promise<void> = (item: ESObject, fetchId: number) => {
- let data = item as BlogData;
- if (data.images.length == 0) {
- return Promise.resolve();
- }
- let url = data.images[0];
- if (this.imageCaches.has(url)) {
- // If cached, skip re-downloading and load the data directly.
- if (data.callback) {
- data.callback(this.imageCaches.get(url));
- } else {
- data.fetchUrl = this.imageCaches.get(url) as string
- }
- return Promise.resolve();
- }
- this.fetches.set(fetchId, data);
- this.fetchAgent.postMessageWithSharedSendable({
- type: 'fetch',
- cachePath: this.cachePath,
- url: data.images[0],
- fetchId: fetchId
- });
- return Promise.resolve();
- }
- cancelCallback: (fetchId: number) => void = (fetchId: number) => {
- this.fetches.delete(fetchId);
- this.fetchAgent.postMessageWithSharedSendable({ type: 'cancel', fetchId: fetchId });
- }
- this.waterFlowView.setViewStyle({
- scroller: this.scroller
- })
- // ...
- .onScrollIndex((start: number, end: number) => {
- if (end > 0) {
- /**
- * Call this method when the visible area boundaries change.
- * The prefetcher will start prefetching after the first call to this method
- * in all cases where the autoStart option is not set to false.
- */
- this.waterFlowView.visibleAreaChanged(start, end);
- }
- // ...
- })
- .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean) => {
- /**
- * By default, the prefetcher begins invoking user code to fetch data with the first call to the visibleAreaChanged method.
- * Sometimes, this can waste resources because, in practice, onScrollIndex triggers the callback even when the component is not actually visible to the user.
- * To avoid this, subscribe to the onVisibleAreaChange event.
- */
- if (isVisible) {
- // Call this method to start prefetching.
- this.waterFlowView.nodeAdapter.prefetcher?.start();
- } else {
- // Call this method to stop prefetching. For instance, this should be done if a related component becomes invisible.
- this.waterFlowView.nodeAdapter.prefetcher?.stop();
- }
- })
- // Preload images using prefetch.
- Image(this.fetchUrl)
- .sourceSize({ width: 100, height: 100 })
- .width(CommonConstants.FULL_WIDTH)
- .aspectRatio(1)
- .objectFit(ImageFit.Cover)
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.
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 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.

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:
- generateRandomBlogData().then((data: ESObject) => {
- this.data = data;
- this.waterFlowView.setDataSource(data);
- });
- @Builder
- getWaterFlow() {
- RecyclerView({
- viewManager: this.waterFlowView
- })
- }
- PullToRefresh({
- data: $data,
- scroller: this.scroller,
- customList: () => {
- this.getWaterFlow()
- },
- // ...
- onLoadMore: () => {
- return new Promise<string>((resolve) => {
- resolve('');
- generateRandomBlogData().then((data: ESObject) => {
- this.waterFlowView.nodeAdapter.pushData(data);
- });
- })
- }
- })
- .layoutWeight(1)
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.

The core code is as follows:
- PullToRefresh({
- data: $data,
- scroller: this.scroller,
- customList: () => {
- this.getWaterFlow()
- },
- onRefresh: () => {
- return new Promise<string>((resolve) => {
- isChinese()? resolve ('Refresh succeeded') : resolve ('Refresh succeeded')
- generateRandomBlogData().then((data: ESObject) => {
- this.data = data;
- this.waterFlowView.setDataSource(data);
- });
- })
- },
- // ...
- })
- .layoutWeight(1)
Currently, footer and footerContent cannot be set when FrameNode creates WaterFlow.
When a user long presses an item, the delete button is displayed. The user can tap the button to delete the item.

- Stack() {
- Image(this.blogItem.images[0])
- // ...
- }
- // ...
- .priorityGesture(
- GestureGroup(GestureMode.Exclusive,
- LongPressGesture().onAction(() => {
- this.showMenu = true;
- }))
- )
- this.context.eventHub.on(CommonConstants.EVENT_REMOVE_ITEM, (blogItem: BlogData) => {
-
- let foundIndex =
- this.dataArray.findIndex((value: BlogData) => JSON.stringify(value) ===
- JSON.stringify(blogItem))
- if (foundIndex !== CommonConstants.NOT_FOUND_INDEX) {
- this.getUIContext().animateTo({ duration: 200 }, () => {
- this.waterFlowView.nodeAdapter.deleteData(foundIndex)
- })
- }
- })
- @Builder
- popUpBuilder() {
- Row({ space: 2 }) {
- Text($r('app.string.not_interested_button_text'))
- }
- .width(100)
- .height(50)
- .padding(5)
- .justifyContent(FlexAlign.Center)
- .onClick(() => {
- this.context.eventHub.emit(CommonConstants.EVENT_REMOVE_ITEM, this.blogItem)
- this.showMenu = false;
- })
- }
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.

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:
- @State sections: WaterFlowSections = new WaterFlowSections();
- oneColumnSection: SectionOptions = {
- itemsCount: 3,
- crossCount: 1,
- columnsGap: 5,
- rowsGap: 10,
- margin: {
- top: 8,
- left: 0,
- bottom: 8,
- right: 0
- },
- onGetItemMainSizeByIndex: (index: number) => {
- if (index === 1) {
- return 100;
- } else {
- return 200;
- }
- }
- };
- twoColumnSection: SectionOptions = {
- itemsCount: 2,
- crossCount: 2,
- onGetItemMainSizeByIndex: () => {
- return 250;
- }
- };
- initView() {
- let sectionOptions: SectionOptions[] = [];
- let count = 0;
- let oneOrTwo = 0;
- while (count < this.dataCount) {
- if (oneOrTwo++ % 2 == 0) {
- sectionOptions.push(this.oneColumnSection);
- count += this.oneColumnSection.itemsCount;
- } else {
- sectionOptions.push(this.twoColumnSection);
- count += this.twoColumnSection.itemsCount;
- }
- }
- this.sections.splice(-1, 0, sectionOptions);
- this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
- // ...
-
- // ...
- }
- this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
- // ...
- .onScrollIndex((_first: number, last: number) => {
- if (last + 20 >= this.waterFlowView.nodeAdapter.totalNodeCount) {
- let dataArray: number[] = [];
- for (let i = 0; i < 100; i++) {
- dataArray.push(i)
- }
- // update data when the page is scrolling.
- this.waterFlowView.nodeAdapter.pushData(dataArray)
- let newSection: SectionOptions = {
- itemsCount: 100,
- crossCount: 2,
- onGetItemMainSizeByIndex: () => {
- return 100;
- }
- }
- // update section
- this.sections.push(newSection);
- this.waterFlowView.setViewStyle({
- scroller: this.scroller, // it's very important to do initialize that makes section update.
- sections: this.sections,
- })
- }
- })
- // ...
When the horizontal list component is swiped up, it will be pinned to the top and the list below continues scrolling.

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.
- @State sections: WaterFlowSections = new WaterFlowSections();
- oneColumnSection: SectionOptions = {
- itemsCount: 3,
- crossCount: 1,
- columnsGap: 5,
- rowsGap: 10,
- margin: {
- top: 8,
- left: 0,
- bottom: 8,
- right: 0
- },
- onGetItemMainSizeByIndex: (index: number) => {
- if (index === 1) {
- return 100;
- } else {
- return 200;
- }
- }
- };
- twoColumnSection: SectionOptions = {
- itemsCount: 2,
- crossCount: 2,
- onGetItemMainSizeByIndex: () => {
- return 250;
- }
- };
- initView() {
- let sectionOptions: SectionOptions[] = [];
- let count = 0;
- let oneOrTwo = 0;
- while (count < this.dataCount) {
- if (oneOrTwo++ % 2 == 0) {
- sectionOptions.push(this.oneColumnSection);
- count += this.oneColumnSection.itemsCount;
- } else {
- sectionOptions.push(this.twoColumnSection);
- count += this.twoColumnSection.itemsCount;
- }
- }
- this.sections.splice(-1, 0, sectionOptions);
- this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
- // ...
-
- // ...
- }
- this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })
- // ...
- .onWillScroll((offset: number) => {
- // Dynamically get the offset position of a waterfall flow
- this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
- })
- build() {
- Stack({ alignContent: Alignment.TopStart }) {
- RecyclerView({
- viewManager: this.waterFlowView
- })
- Stack() {
- // ...
- }
- .width(CommonConstants.FULL_WIDTH)
- .height(100)
- .padding({ left: CommonConstants.PADDING, right: CommonConstants.PADDING })
- .backgroundColor(Color.White)
- .hitTestBehavior(HitTestMode.Transparent)
- // Set the sticky component's offset.
- .position({ x: 0, y: this.scrollOffset >= 220 ? 0 : 220 - this.scrollOffset })
- }
- }
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:
- aboutToAppear(): void {
- let orientation = window.Orientation.AUTO_ROTATION;
- this.windowClass.setPreferredOrientation(orientation, (err: BusinessError) => {
- const errCode: number = err.code;
- if (errCode) {
- Logger.error('Failed to set window orientation. Cause:' + JSON.stringify(err));
- return;
- }
- Logger.info('Succeed to setting window orientation');
- })
- this.windowClass.on('windowSizeChange', (size) => {
- let viewWidth = size.width;
- let viewHeight = size.height;
-
- if (viewWidth > viewHeight) {
- this.waterFlowView.setViewStyle().columnsTemplate('1fr 1fr 1fr');
- } else {
- this.waterFlowView.setViewStyle().columnsTemplate('1fr 1fr');
- }
- })
- this.initView();
- // ...
- }
- initView() {
- this.waterFlowView.setViewStyle({ scroller: this.scroller })
- .height(CommonConstants.FULL_HEIGHT)
- .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE)
- .columnsGap(CommonConstants.COLUMNS_GAP)
- .rowsGap(CommonConstants.ROWS_GAP)
- .padding({
- top: CommonConstants.PADDING,
- left: CommonConstants.PADDING,
- right: CommonConstants.PADDING
- })
- .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) })
- }
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.
- this.waterFlowView.setViewStyle({ scroller: this.scroller })
- .height(CommonConstants.FULL_HEIGHT)
- .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE)
- .columnsGap(CommonConstants.COLUMNS_GAP)
- .rowsGap(CommonConstants.ROWS_GAP)
- .padding({
- top: CommonConstants.PADDING,
- left: CommonConstants.PADDING,
- right: CommonConstants.PADDING
- })
- .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) })
Add animateTo when deleting a component to implement a smooth transition effect.
- this.context.eventHub.on(CommonConstants.EVENT_REMOVE_ITEM, (blogItem: BlogData) => {
-
- let foundIndex =
- this.dataArray.findIndex((value: BlogData) => JSON.stringify(value) ===
- JSON.stringify(blogItem))
- if (foundIndex !== CommonConstants.NOT_FOUND_INDEX) {
- this.getUIContext().animateTo({ duration: 200 }, () => {
- this.waterFlowView.nodeAdapter.deleteData(foundIndex)
- })
- }
- })