diff --git a/README.md b/README.md index 890e2fc0ffb77e56f0cb1e1776a10897feb55eb8..0b55fa25a4d0324409eb3fe77457a676e136b922 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,177 @@ -# WaterFlowScrollComponent +# 基于ScrollComponents实现瀑布流页面 -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} +## 简介 +本示例展示了使用ScrollComponents构建瀑布流页面,覆盖场景包括 -#### 软件架构 -软件架构说明 +- 瀑布流Item组件复用,以及跨页面的组件复用场景; +- 瀑布流加速首屏渲染的场景; +- 瀑布流无限滑动的场景; +- 瀑布流数据处理场景:下拉刷新、上拉加载、长按删除; +- 瀑布流分组混合布局场景; +- 瀑布流滑动吸顶场景; +- 旋转屏幕瀑布流动态切换列数场景; +- 瀑布流动效场景:边缘渐隐效果、删除滑动错位效果; +## 效果预览 +图片名称 -#### 安装教程 +## 工程目录 +``` +├─entry/src/main/ets +├── model +| ├── mock.ets +| ├── ToolBarData.ets +| └── types.ets +└── pages + ├── Index.ets + ├── StandardWaterFlowPage.ets // 瀑布流列表项结构相同复用 + ├── CombineWaterFlowPage.ets // 瀑布流列表项内子组件可拆分组合复用 + ├── TabBarPage.ets // 瀑布流跨页面复用 + └── StickyWaterFlowPage.ets // 瀑布流滑动吸顶 +``` -1. xxxx -2. xxxx -3. xxxx +## 功能说明 +1. StandardWaterFlowPage.ets:实现了列表项结构相同的组件复用、瀑布流加速首屏渲染场景,瀑布流自适应屏幕旋转动态修改列数场景、瀑布流长按删除和删除滑动错位效果场景、瀑布流边缘渐隐效果场景; +2. CombineWaterFlowPage.ets:实现了列表项内子组件可拆分组合的复用、瀑布流加速首屏渲染场景、瀑布流无限滑动场景; +3. TabBarPage.ets:实现了SharedPoolPage.ets和SharedPoolSecondPage.ets之间的跨页面复用功能, 瀑布流下拉刷新和上拉加载场景; +4. StickyWaterFlowPage.ets:实现了瀑布流滑动吸顶场景、瀑布流分组混合布局场景; -#### 使用说明 -1. xxxx -2. xxxx -3. xxxx +## 实现说明 +> 下文介绍高效无限瀑布流页面渲染。 -#### 参与贡献 +### 1. 定义瀑布流视图管理类,注册item节点模板 +WaterFlowManager是视图管理器,开发者根据业务需要自定义视图管理器类。 +- 页面初始化时,开发者自定义子节点模板和组件绑定,即可实现复用能力 +- 如果使用预创建节点,需在预创建前注册节点模板 -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +```c +// src/main/ets/pages/StandardWaterFlowPage.ets +import { NodeItem, RecyclerView } from '@hadss/scroll_components'; -#### 特技 +/** + * item模板 + * */ +@Builder +function StandardGridImageContainer($$: ESObject) { + GridImageView({blogItem: $$.blogItem}) +} -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +// 1. 自定义视图管理器 +class MyWaterFlowManager extends WaterFlowManager { + + onWillCreateItem(index: number, data: BlogData) { + // 3. 根据唯一标识获取复用节点,传递节点数据 + let node: NodeItem | null = this.dequeueReusableNodeByType('StandardGridImageContainer'); + node?.setData({ blogItem: data }); + return node; + } +} + +@Entry +@Component +export struct StandardWaterFlowPage { + // 视图管理器实例化 + waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({ + defaultNodeItem: 'StandardGridImageContainer', + context: this.getUIContext() + }); + ... + + aboutToAppear(): void { + ... + this.initView(); + // 2. 注册节点模板 + this.waterFlowView.registerNodeItem('StandardGridImageContainer', wrapBuilder(StandardGridImageContainer)); + } + ... +} +``` + +### 2. WaterFlow组件初始化 + +```c +// src/main/ets/pages/StandardWaterFlowPage.ets + +initView() { + this.waterFlowView.setViewStyle({scroller: this.scroller}) + .width(CommonConstants.FULL_WIDTH) + .height(CommonConstants.FULL_HEIGHT) + .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE) + .columnsGap(CommonConstants.COLUMNS_GAP) + .rowsGap(CommonConstants.ROWS_GAP) + .padding(CommonConstants.PADDING) +} +``` + +### 3. 设置数据源渲染组件 +- ScrollComponents默认支持懒加载,使用setDataSource设置普通数组即可。开发者无需繁琐的定义类似lazyForEach的Datasource数据源 +- RecyclerView组件占位,绑定视图容器实例即可渲染瀑布流列表 +```c +// src/main/ets/pages/StandardWaterFlowPage.ets + +import { RecyclerView } from '@hadss/scroll_components'; + +@Observed +class BlogData{ + id: number=-1 + images: string[] =[]; + imagePixelMap: image.PixelMap | undefined; + ... +} + +@Entry +@Component +export struct StandardWaterFlowPage { + waterFlowView: MyWaterFlowManager = new MyWaterFlowManager({ + defaultNodeItem: 'StandardGridImageContainer', + context: this.getUIContext() + }); + scroller: Scroller = new Scroller(); + @State data: BlogData[] = []; + ... + + aboutToAppear(): void { + ... + this.initView(); + // 1. setDataSource设置瀑布流数据 + this.waterFlowView.setDataSource(data); + } + + build() { + Column() { + ... + // 2. 绑定视图容器实例 + RecyclerView({ + viewManager: this.waterFlowView + }) + } + } +} +``` + +# ScrollComponents简介 + +ScrollComponents 作为高性能滑动解决方案,可以帮助开发者在长列表、瀑布流等复杂页面场景下实现更好的滑动流畅度。 + +底座通过使用自定义复用池解决复杂场景下组件复用的效率问题,相比原生的Reusable组件复用效率更高。 + +开发者无需关注复杂的组件复用池管理和其它性能优化方案的交互细节,使用少量的代码即可快速实现高性能滑动的开发体验。 + +# 特性 +- 支持瀑布流页面的流畅滑动; +- 默认懒加载; +- 支持组件分帧预创建,帮助开发者减少组件单帧耗时; +- 支持跨页面共享复用池,减少页面打开后的滑动丢帧; +- 支持内容动态预加载,减少滑动过程中的白块问题; + +# FAQ +[查看详情](https://gitcode.com/openharmony-sig/scroll_components/blob/master/docs/FAQ.md) + +# 原理介绍 +[查看详情](https://gitcode.com/openharmony-sig/scroll_components/blob/master/README.md#%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D) + +# 开源协议 + +本项目基于 [Apache License 2.0](./LICENSE) ,请自由地享受和参与开源。 \ No newline at end of file diff --git a/entry/src/main/ets/pages/CombineWaterFlowPage.ets b/entry/src/main/ets/pages/CombineWaterFlowPage.ets index ada623272b2427cc83441068223208838f207ee0..217e89d2f6fe96a5a9949faf3ff3823eac384976 100644 --- a/entry/src/main/ets/pages/CombineWaterFlowPage.ets +++ b/entry/src/main/ets/pages/CombineWaterFlowPage.ets @@ -13,8 +13,9 @@ * limitations under the License. */ +// [Start quick_start_4_b] import { NodeItem, PartReuse, RecyclerView, WaterFlowManager } from '@hadss/scroll_components'; -import { PullToRefresh } from '@ohos/pulltorefresh'; +// [End quick_start_4_b] import { BlogData, Params } from '../model/types'; import { collections, JSON, MessageEvents, taskpool, util, worker } from '@kit.ArkTS'; import { CommonConstants } from '../common/constants/CommonConstants'; @@ -38,7 +39,6 @@ async function generateRandomBlogData(): Promise { class MyWaterFlowManager extends WaterFlowManager { onWillCreateItem(index: number, data: BlogData) { - let node: NodeItem | null = this.dequeueReusableNodeByType('BlogItemContainer'); node?.setData({ blogItem: data }); return node; @@ -101,25 +101,26 @@ struct WaterFlowPage { footerContent: ComponentContent = new ComponentContent(this.getUIContext(), wrapBuilder<[MyParams]>(buildText), new MyParams(this.footerState)); - @State isRefreshing: boolean = false; + @State isLoadMore: boolean = false; fetchAgent = new worker.ThreadWorker('entry/ets/workers/FetchAgent.ets', { name: 'fetchAgent' }); cachePath = (this.getUIContext().getHostContext() as common.Context).getApplicationContext().cacheDir; + imageCaches: Map = new Map(); // [Start PreCreate] aboutToAppear(): void { // [StartExclude PreCreate] this.initView(); // [Start Prefetch_1] + // 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); }) - // [end Prefetch_1] + // [End Prefetch_1] // [EndExclude PreCreate] - // register components this.waterFlowView.registerNodeItem('BlogItemContainer', wrapBuilder(BlogItemContainer)); this.waterFlowView.registerNodeItem('AdaptiveTextComponent', wrapBuilder(AdaptiveTextComponentContainer)); @@ -128,25 +129,26 @@ struct WaterFlowPage { this.waterFlowView.preCreate('BlogItemContainer', 30); this.waterFlowView.preCreate('AdaptiveTextComponent', 30); this.waterFlowView.preCreate('GridImageViewContainer', 30); - // [StartExclude PreCreate] Logger.info('WaterFlowPage==> nodePool : ' + JSON.stringify(util.getHash(this.waterFlowView.getRecyclePool()))); this.workerOnMessage(); // [EndExclude PreCreate] } - // [End PreCreate] aboutToDisappear() { this.fetchAgent.terminate(); + this.imageCaches.clear(); + this.fetches.clear(); } initView() { // [Start Prefetch_3] this.waterFlowView.setViewStyle({ scroller: this.scroller - })// [StartExclude Prefetch_3] + }) + // [StartExclude Prefetch_3] .width(CommonConstants.FULL_WIDTH) .height(CommonConstants.FULL_HEIGHT) .columnsTemplate(CommonConstants.WATER_FLOW_COLUMNS_TEMPLATE) @@ -159,13 +161,32 @@ struct WaterFlowPage { })// [EndExclude Prefetch_3] .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); } + // [StartExclude Prefetch_3] + if (end + 30 > this.waterFlowView.nodeAdapter.totalNodeCount) { + if (!this.isLoadMore) { + this.isLoadMore = true + taskpool.execute(generateRandomBlogData).then((data: ESObject) => { + this.waterFlowView.nodeAdapter.pushData(data); + this.isLoadMore = false; + }) + } + } + // [EndExclude Prefetch_3] }) .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(); } }) @@ -186,6 +207,7 @@ struct WaterFlowPage { } else { data.fetchUrl = e.data.fetchUrl } + this.imageCaches.set(data.images[0], e.data.fetchUrl) this.fetches.delete(e.data.fetchId) break; case 'FAIL': @@ -203,7 +225,16 @@ struct WaterFlowPage { if (data.images.length == 0) { return Promise.resolve(); } - + let url = data.images[0]; + if (this.imageCaches.has(url)) { + // 有缓存就不重新下载,直接加载数据 + 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', @@ -217,7 +248,6 @@ struct WaterFlowPage { this.fetches.delete(fetchId); this.fetchAgent.postMessageWithSharedSendable({ type: 'cancel', fetchId: fetchId }); } - // [End Prefetch_2] @Builder @@ -240,35 +270,8 @@ struct WaterFlowPage { .width(CommonConstants.FULL_WIDTH) .backgroundColor(Color.White) - // [Start Load_More] - PullToRefresh({ - data: $data, - scroller: this.scroller, - customList: () => { - this.getWaterFlow() - }, - // [StartExclude Load_More] - onRefresh: () => { - return new Promise((resolve) => { - resolve('刷新成功'); - generateRandomBlogData().then((data: ESObject) => { - this.data = data; - this.waterFlowView.setDataSource(data); - }); - }) - }, - // [EndExclude Load_More] - onLoadMore: () => { - return new Promise((resolve) => { - resolve(''); - generateRandomBlogData().then((data: ESObject) => { - this.waterFlowView.nodeAdapter.pushData(data); - }); - }) - } - }) - .layoutWeight(1) - // [End Load_More] + this.getWaterFlow() + } .height(CommonConstants.FULL_HEIGHT) .backgroundColor('#F5F5F5') @@ -280,10 +283,12 @@ function BlogItemContainer($$: Params) { BlogItem({ blogItem: $$.blogItem }) } +// [Start quick_start_4_b] @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; } @@ -297,13 +302,13 @@ struct BlogItem { 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', @@ -319,6 +324,7 @@ struct BlogItem { .borderRadius(12) } } +// [End quick_start_4_b] @Component @@ -345,17 +351,19 @@ export struct HeaderComponent { } } +// [Start quick_start_4_b] @Builder export function AdaptiveTextComponentContainer($$: Params) { AdaptiveTextComponent({ blogItem: $$.blogItem }) } - +// [End quick_start_4_b] @Component export struct AdaptiveTextComponent { @State private showFullText: boolean = false; @State originalText: string = '这里是一段非常长的示例文本...'; // 替换为实际文本 @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; } @@ -376,12 +384,12 @@ export struct AdaptiveTextComponent { .height('auto') } } - +// [Start quick_start_4_b] @Builder function GridImageViewContainer($$: Params) { GridImageView({ blogItem: $$.blogItem }) } - +// [End quick_start_4_b] @Component struct GridImageView { @State blogItem: BlogData = new BlogData() @@ -415,8 +423,8 @@ struct GridImageView { .width(CommonConstants.FULL_WIDTH) .aspectRatio(1) .objectFit(ImageFit.Cover) + // [End Prefetch_4] } - // [End Prefetch_4] } @Component @@ -427,29 +435,32 @@ export struct BottomContent { // Interactive toolbar Row({ space: 20 }) { Row() { - Image('https://img2.baidu.com/it/u=3148587158,2445495738&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500') - .width(20) - .height(20) + Image($r('app.media.icon_like')) + .width(14) + .height(14) Text(this.blogItem.likes.toString()) + .fontSize(14) .margin({ left: 4 }) } .backgroundColor(Color.Transparent) Row() { - Image('https://img.ixintu.com/download/jpg/20200805/8a9cb0d17145f80ea2cf8d060070276e_512_512.jpg%21con') - .width(20) - .height(20) + Image($r('app.media.icon_comment')) + .width(14) + .height(14) Text(this.blogItem.comments.toString()) + .fontSize(14) .margin({ left: 4 }) } .backgroundColor(Color.Transparent) Row() { - Image('https://gd-hbimg.huaban.com/f5159d11bab345671e2943a4cf792048eb18f07a3e34-6EOAyU_fw658') - .width(20) - .height(20) + Image($r('app.media.icon_share')) + .width(14) + .height(14) Text(this.blogItem.reposts.toString()) - .margin({ left: 4 }) + .fontSize(14) + .margin({ left: 2 }) } .backgroundColor(Color.Transparent) } diff --git a/entry/src/main/ets/pages/MultiFlowItemPage.ets b/entry/src/main/ets/pages/MultiFlowItemPage.ets index de96c481e9fab2a7075d4fe05929fc580d511ae0..057dd051c4a54ee50d7ed09e767ddb35f7cfb69b 100644 --- a/entry/src/main/ets/pages/MultiFlowItemPage.ets +++ b/entry/src/main/ets/pages/MultiFlowItemPage.ets @@ -107,6 +107,7 @@ struct MultiFlowItemPage { // [End quick_start_4_c] // [Start quick_start_4_c] +// Reusable Image Component Template @Builder function ImageContainer($$: Params) { ImageContainerView({ blogItem: $$.blogItem }) @@ -142,6 +143,7 @@ struct ImageContainerView { } // [Start quick_start_4_c] +// Reusable Text Component Template @Builder function TextContainer($$: Params) { TextContainerView({ blogItem: $$.blogItem }) diff --git a/entry/src/main/ets/pages/SharedPoolPage.ets b/entry/src/main/ets/pages/SharedPoolPage.ets index a059f829c830d4d0a323ebb54b2126e1a9ff998a..b39d46ebca623d8bb80dc56798325a139a799181 100644 --- a/entry/src/main/ets/pages/SharedPoolPage.ets +++ b/entry/src/main/ets/pages/SharedPoolPage.ets @@ -20,6 +20,7 @@ import { CommonConstants } from '../common/constants/CommonConstants'; import { Utils } from '../common/util/Utils'; import { BottomContent, HeaderComponent } from './CombineWaterFlowPage'; import { Logger } from '../common/util/Logger'; +import { PullToRefresh } from '@ohos/pulltorefresh'; @Concurrent async function generateRandomBlogData(): Promise { @@ -55,24 +56,20 @@ export struct SharedPoolPage { this.data = data; this.waterFlowView.setDataSource(data); }); - // [Start Share_Pool] if (Utils.getInstance().nodePool) { - // 注册复用池 + // Registration Reuse Pool this.waterFlowView.registerRecyclePool(Utils.getInstance().nodePool!); } else { Utils.getInstance().nodePool = this.waterFlowView.getRecyclePool(); } // [End Share_Pool] - - // 注册复用模板 + // Reusable Registration Template this.waterFlowView.registerNodeItem('TestBlogItemContainer', wrapBuilder(MyBlogItemContainer)); this.waterFlowView.registerNodeItem('GridImageViewContainer', wrapBuilder(GridImageViewContainer)); - - // 预创建 + // Pre-creation this.waterFlowView.preCreate('TestBlogItemContainer', 30); this.waterFlowView.preCreate('GridImageViewContainer', 30); - Logger.info('SharedPoolPage==> nodePool : ' + JSON.stringify(util.getHash(this.waterFlowView.getRecyclePool()))); } @@ -89,25 +86,16 @@ export struct SharedPoolPage { left: CommonConstants.PADDING, right: CommonConstants.PADDING, }) - .onScrollIndex((_start: number, end: number) => { - if (end > this.waterFlowView.nodeAdapter.totalNodeCount - 10) { - setTimeout(() => { - generateRandomBlogData().then((data: ESObject) => { - this.waterFlowView.nodeAdapter.pushData(data) - }) - }, 100) - } - }) } - // [Start Refresh_2] + // [Start Pull_Refresh_2] @Builder getWaterFlow() { RecyclerView({ viewManager: this.waterFlowView }) } - // [end Refresh_2] + // [End Pull_Refresh_2] build() { Column() { @@ -122,23 +110,41 @@ export struct SharedPoolPage { .padding(CommonConstants.PADDING) .width(CommonConstants.FULL_WIDTH) .backgroundColor(Color.White) - // [Start Refresh_2] - Refresh({ refreshing: $$this.isRefreshing }) { - this.getWaterFlow() - } - .layoutWeight(1) - .onRefreshing(() => { - // [Start Refresh_1] - // modify date - generateRandomBlogData().then((data: BlogData[]) => { - this.isRefreshing = false - this.data = data; - this.waterFlowView.setDataSource(data); - }) - // [End Refresh_1] + // [Start Load_More] + // [Start Pull_Refresh_2] + PullToRefresh({ + data: $data, + scroller: this.scroller, + customList: () => { + this.getWaterFlow() + }, + // [StartExclude Load_More] + onRefresh: () => { + return new Promise((resolve) => { + resolve('刷新成功'); + // [Start Pull_Refresh_1] + generateRandomBlogData().then((data: ESObject) => { + this.data = data; + this.waterFlowView.setDataSource(data); + }); + // [Start Pull_Refresh_2] + }) + }, + // [EndExclude Load_More] + // [StartExclude Pull_Refresh_2] + onLoadMore: () => { + return new Promise((resolve) => { + resolve(''); + generateRandomBlogData().then((data: ESObject) => { + this.waterFlowView.nodeAdapter.pushData(data); + }); + }) + } + // [EndExclude Pull_Refresh_2] }) - // [End Refresh_2] - + .layoutWeight(1) + // [End Pull_Refresh_2] + // [Start Load_More] } .height(CommonConstants.FULL_HEIGHT) .backgroundColor('#F5F5F5') @@ -154,6 +160,7 @@ function MyBlogItemContainer($$: Params) { struct MyBlogItem { @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; } @@ -187,6 +194,7 @@ export function GridImageViewContainer($$: Params) { struct GridImageView { @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; } diff --git a/entry/src/main/ets/pages/SharedPoolSecondPage.ets b/entry/src/main/ets/pages/SharedPoolSecondPage.ets index 44b72f3daa5f2a73037b9ed80d949bfc61c8500c..32784b1bcdc053f3d6aabc32be888c00996c243d 100644 --- a/entry/src/main/ets/pages/SharedPoolSecondPage.ets +++ b/entry/src/main/ets/pages/SharedPoolSecondPage.ets @@ -60,23 +60,19 @@ export struct SharedPoolSecondPage { this.waterFlowView.setDataSource(data); }); // [EndExclude Share_Pool] - if (Utils.getInstance().nodePool) { // Registration Reuse Pool this.waterFlowView.registerRecyclePool(Utils.getInstance().nodePool!); } else { Utils.getInstance().nodePool = this.waterFlowView.getRecyclePool(); } - // [StartExclude Share_Pool] // Reusable Registration Template this.waterFlowView.registerNodeItem('TestBlogItemContainer', wrapBuilder(MyBlogItemContainer)); this.waterFlowView.registerNodeItem('GridImageViewContainer', wrapBuilder(GridImageViewContainer)); - // Pre-initialization this.waterFlowView.preCreate('TestBlogItemContainer', 30); this.waterFlowView.preCreate('GridImageViewContainer', 30); - Logger.info('SharedPoolSecondPage==> nodePool : ' + JSON.stringify(util.getHash(this.waterFlowView.getRecyclePool()))); // [EndExclude Share_Pool] diff --git a/entry/src/main/ets/pages/StandardWaterFlowPage.ets b/entry/src/main/ets/pages/StandardWaterFlowPage.ets index df0f022289eaca35d828712aebc3f454fe87069c..b72a58f5dd1a6e5258e7afa3c2b9dea1bb71af8c 100644 --- a/entry/src/main/ets/pages/StandardWaterFlowPage.ets +++ b/entry/src/main/ets/pages/StandardWaterFlowPage.ets @@ -187,6 +187,7 @@ struct StandardWaterFlowPage { // [Start quick_start_4_a] // [Start quick_start_3] +// Define an item template @Builder function StandardGridImageContainer($$: Params) { GridImageView({ blogItem: $$.blogItem }) @@ -201,6 +202,7 @@ struct GridImageView { @State flowHeight: Length = 0; private context = this.getUIContext().getHostContext() as common.UIAbilityContext; + // In reusable components, you must use aboutToReuse to update data, just like native recycling. aboutToReuse(params: ESObject): void { this.blogItem = params.blogItem; } diff --git a/entry/src/main/ets/pages/StickyWaterFlowPage.ets b/entry/src/main/ets/pages/StickyWaterFlowPage.ets index 5969893af5437555c92e86de793501d885867845..487842a95f944122c6fc1e8f01c854213a88dc1a 100644 --- a/entry/src/main/ets/pages/StickyWaterFlowPage.ets +++ b/entry/src/main/ets/pages/StickyWaterFlowPage.ets @@ -34,6 +34,7 @@ class MyWaterFlowManager extends WaterFlowManager { struct StickyFlowItem { @State item: number = 0; + // In reusable components, you must use aboutToReuse to update data, just like native recycling. aboutToReuse(params: ESObject): void { this.item = params.item } @@ -199,8 +200,11 @@ struct StickyWaterFlowPage { } } this.sections.splice(-1, 0, sectionOptions); + // [Start Stick] // [Start Section_3] - this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections })// [StartExclude Section_3] + this.waterFlowView.setViewStyle({ scroller: this.scroller, sections: this.sections }) + // [StartExclude Stick] + // [StartExclude Section_3] // [StartExclude Section_2] .columnsTemplate('1fr 1fr') .columnsGap(CommonConstants.COLUMNS_GAP) @@ -209,7 +213,8 @@ struct StickyWaterFlowPage { .height(CommonConstants.FULL_HEIGHT) .layoutWeight(1) .scrollBar(BarState.Off) - .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])// [StartExclude Section_3] + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) + // [EndExclude Section_3] .onScrollIndex((_first: number, last: number) => { if (last + 20 >= this.waterFlowView.nodeAdapter.totalNodeCount) { let dataArray: number[] = []; @@ -233,12 +238,17 @@ struct StickyWaterFlowPage { sections: this.sections, }) } - })// [StartExclude Section_2] + }) + // [EndExclude Stick] + // [StartExclude Section_3] .onWillScroll((offset: number) => { + // Dynamically get the offset position of a waterfall flow this.scrollOffset = this.scroller.currentOffset().yOffset + offset; - }) // [EndExclude Section_2] - // [EndExclude Section_2] + }) + // [EndExclude Section_3] + // [EndExclude Section_2] // [End Section_3] + // [End Stick] // [StartExclude Section_2] this.waterFlowView.setItemViewStyle((flowItem, _index, item: number) => { @@ -249,7 +259,6 @@ struct StickyWaterFlowPage { // [EndExclude Section_2] } - // [End Section_2] aboutToDisappear(): void { @@ -257,15 +266,14 @@ struct StickyWaterFlowPage { display.off('foldStatusChange'); } } - + // [Start Stick] build() { - Stack({ alignContent: Alignment.TopStart }) { RecyclerView({ viewManager: this.waterFlowView }) - Stack() { + // [StartExclude Stick] Column() { Scroll() { Row({ space: 0 }) { @@ -381,13 +389,16 @@ struct StickyWaterFlowPage { .width(CommonConstants.FULL_WIDTH) .padding({ top: $r('app.float.sections_margin'), bottom: $r('app.float.sections_margin') }) }.alignItems(HorizontalAlign.Start) + // [EndExclude Stick] } .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 }) } } + // [End Stick] } \ No newline at end of file diff --git a/entry/src/main/resources/base/media/icon_comment.svg b/entry/src/main/resources/base/media/icon_comment.svg new file mode 100644 index 0000000000000000000000000000000000000000..2b1da05e6da914b6dc670b9fe72f104e209d2309 --- /dev/null +++ b/entry/src/main/resources/base/media/icon_comment.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/icon_like.svg b/entry/src/main/resources/base/media/icon_like.svg new file mode 100644 index 0000000000000000000000000000000000000000..c8ee4673a8472caa8bbf06dd0f29ef038bb1bb1c --- /dev/null +++ b/entry/src/main/resources/base/media/icon_like.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/icon_share.svg b/entry/src/main/resources/base/media/icon_share.svg new file mode 100644 index 0000000000000000000000000000000000000000..c16235b831e2886919a965ef058bd59c04b13929 --- /dev/null +++ b/entry/src/main/resources/base/media/icon_share.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/screenshots/img.gif b/screenshots/img.gif new file mode 100644 index 0000000000000000000000000000000000000000..85bfd264319dab94257111b18ce4ee2cfbb072ed Binary files /dev/null and b/screenshots/img.gif differ