开发经典的瀑布流
本案例主要介绍WaterFlow容器的使用。如图所示对高度不相等的子组件,实现如瀑布般的紧密布局。
1. 案例效果截图
2. 案例运用到的知识点
2.1. 核心知识点
- WaterFlow:瀑布流容器,由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。
- FlowItem:瀑布流容器的子组件。
- LazyForEach:LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当LazyForEach在滚动容器中使用了,框架会根据滚动容器可视区域按需创建组件,当组件划出可视区域外时,框架会进行组件销毁回收以降低内存占用。
2.2. 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Provider/@Consumer
- 自定义组件和组件生命周期
- 内置组件:Column/Button
- 日志管理类的编写
- 常量与资源分类的访问
- MVVM模式
3. 代码结构
├──entry/src/main/ets // 代码区
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 公共常量类
│ │ └──utils
│ │ └──Logger.ets // 日志打印类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ ├──pages
│ │ └──HomePage.ets // 主界面
│ ├──view
│ │ ├──ClassifyComponent.ets // 分类信息类
│ │ ├──FlowItemComponent.ets // 瀑布流Item组件类
│ │ ├──SearchComponent.ets // 查询组件类
│ │ ├──SwiperComponent.ets // banner组件类
│ │ └──WaterFlowComponent.ets // 自定义组件类
│ └──viewmodel
│ ├──HomeViewModel.ets // 瀑布流数据类
│ ├──ProductItem.ets // 产品信息类
│ └──WaterFlowDataSource.ets // 瀑布流数据类
└──entry/src/main/resources // 资源文件目录
4. 公共文件与资源
本案例涉及到的常量类和工具类代码如下:
- 通用常量类
// entry/src/main/ets/common/constants/CommonConstants.ets
export class CommonConstants {static readonly FULL_OPACITY: number = 1static readonly SIXTY_OPACITY: number = 0.6static readonly EIGHTY_OPACITY: number = 0.8static readonly FONT_WEIGHT_FIVE: number = 500static readonly WATER_FLOW_LAYOUT_WEIGHT: number = 1static readonly WATER_FLOW_COLUMNS_TEMPLATE: string = '1fr 1fr'static readonly FULL_WIDTH: string = '100%'static readonly FULL_HEIGHT: string = '100%'static readonly INVALID_INDEX: number = -1
}
- 日志类
// entry/src/main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'export default class Logger {private static domain: number = 0xFF00private static prefix: string = 'WaterFlow'private static format: string = '%{public}s, %{public}s'static debug(...args: string[]): void {hilog.debug(Logger.domain, Logger.prefix, Logger.format, args)}static info(...args: string[]): void {hilog.info(Logger.domain, Logger.prefix, Logger.format, args)}static warn(...args: string[]): void {hilog.warn(Logger.domain, Logger.prefix, Logger.format, args)}static error(...args: string[]): void {hilog.error(Logger.domain, Logger.prefix, Logger.format, args)}
}
本案例涉及到的资源文件如下:
- string.json
// entry/main/resources/base/element/string.json
{"string": [{"name": "module_desc","value": "模块描述"},{"name": "EntryAbility_desc","value": "description"},{"name": "EntryAbility_label","value": "WaterFlow组件的使用"},{"name": "title_bar_homepage","value": "首页"},{"name": "title_bar_phone","value": "手机"},{"name": "title_bar_computer","value": "电脑"},{"name": "title_bar_foods","value": "食品"},{"name": "title_bar_men_wear","value": "男装"},{"name": "title_bar_fresh","value": "生鲜"},{"name": "title_bar_furniture_kitchenware","value": "家具厨具"},{"name": "title_bar_classification","value": "分类"},{"name": "search_text","value": "儿童餐椅"},{"name": "footer_text","value": "已经到底了"}]
}
- float.json
// entry/main/resources/base/element/float.json
{"float": [{"name": "smaller_font_size","value": "12fp"},{"name": "small_font_size","value": "14fp"},{"name": "middle_font_size","value": "16fp"},{"name": "split_line_width","value": "1vp"},{"name": "split_line_height","value": "18vp"},{"name": "more_width","value": "16vp"},{"name": "more_height","value": "16vp"},{"name": "more_margin_left","value": "2vp"},{"name": "more_margin_right","value": "2vp"},{"name": "classify_title_margin","value": "12vp"},{"name": "search_width","value": "20vp"},{"name": "search_height","value": "20vp"},{"name": "search_radius","value": "20vp"},{"name": "search_margin_left","value": "12vp"},{"name": "search_margin_right","value": "8vp"},{"name": "search_swiper_height","value": "40vp"},{"name": "search_margin_top","value": "16vp"},{"name": "swiper_image_height","value": "150vp"},{"name": "swiper_radius","value": "16vp"},{"name": "swiper_margin_top","value": "12vp"},{"name": "swiper_margin_bottom","value": "12vp"},{"name": "image_background_height","value": "222vp"},{"name": "home_margin_left","value": "12vp"},{"name": "home_margin_right","value": "12vp"},{"name": "product_layout_radius","value": "8vp"},{"name": "product_layout_margin_left","value": "12vp"},{"name": "product_layout_margin_right","value": "12vp"},{"name": "product_layout_margin_bottom","value": "12vp"},{"name": "product_image_size","value": "132vp"},{"name": "water_flow_image_top","value": "12vp"},{"name": "water_flow_image_bottom","value": "8vp"},{"name": "discount_text_bottom","value": "4vp"},{"name": "piece_line_height","value": "19vp"},{"name": "promotion_font_size","value": "10fp"},{"name": "promotion_radius","value": "4vp"},{"name": "promotion_text_height","value": "16vp"},{"name": "promotion_padding_left","value": "4vp"},{"name": "promotion_padding_right","value": "4vp"},{"name": "promotion_margin_top","value": "6vp"},{"name": "promotion_margin_right","value": "8vp"},{"name": "bonus_points_radius_width","value": "0.5vp"},{"name": "bonus_points_margin_top","value": "6vp"},{"name": "bonus_points_padding_left","value": "8vp"},{"name": "bonus_points_padding_right","value": "8vp"},{"name": "water_flow_columns_gap","value": "8vp"},{"name": "water_flow_row_gap","value": "8vp"},{"name": "footer_text_size","value": "10vp"},{"name": "footer_text_height","value": "20vp"}]
}
- color.json
// entry/main/resources/base/element/color.json
{"color": [{"name": "start_window_background","value": "#FFFFFF"},{"name": "indicator_select","value": "#F74E42"},{"name": "focus_color","value": "#E92F4F"},{"name": "home_background_color","value": "#F1F3F5"}]
}
其他资源请到源码中获取。
5. 首页
// entry/src/main/est/pages/HomePage.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import ClassifyComponent from '../view/ClassifyComponent'
import SearchComponent from '../view/SearchComponent'
import SwiperComponent from '../view/SwiperComponent'
import WaterFlowComponent from '../view/WaterFlowComponent'@Entry
@Component
struct HomePage {build() {Stack({ alignContent: Alignment.Top }) {Image($r('app.media.ic_app_background')).width(Const.FULL_WIDTH).height($r('app.float.image_background_height')).objectFit(ImageFit.Cover)Column() {SearchComponent()ClassifyComponent()SwiperComponent()WaterFlowComponent()}.padding({left: $r('app.float.home_margin_left'),right: $r('app.float.home_margin_right')})}.backgroundColor($r('app.color.home_background_color'))}
}
6. 搜索组件
// entry/src/main/est/view/SearchComponent.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'@Component
export default struct SearchComponent {build() {Row() {Image($r('app.media.ic_search')).width($r('app.float.search_width')).height($r('app.float.search_height')).margin({left: $r('app.float.search_margin_left'),right: $r('app.float.search_margin_right')})Text($r('app.string.search_text')).fontSize($r('app.float.small_font_size')).fontColor(Color.Black).opacity(Const.SIXTY_OPACITY).fontWeight(FontWeight.Normal)}.width(Const.FULL_WIDTH).height($r('app.float.search_swiper_height')).borderRadius($r('app.float.search_radius')).backgroundColor(Color.White).margin({ top: $r('app.float.search_margin_top') })}
}
7. 分类导航组件
// entry/src/main/view/ClassifyComponent.ets
import { classifyTitle } from '../viewmodel/HomeViewModel'
import { CommonConstants as Const } from '../common/constants/CommonConstants'@Component
export default struct ClassifyComponent {@State titleIndex: number = 0build() {Flex({ justifyContent: FlexAlign.SpaceBetween }) {ForEach(classifyTitle, (item: Resource, index: number | undefined) => {if (index !== undefined) {Text(item).fontSize($r('app.float.middle_font_size')).opacity(this.titleIndex === index ? Const.FULL_OPACITY : Const.EIGHTY_OPACITY).fontWeight(this.titleIndex === index ? Const.FONT_WEIGHT_FIVE : FontWeight.Normal).fontColor(Color.White).onClick(() => {this.titleIndex = index})}}, (item: Resource) => JSON.stringify(item))Row() {Image($r('app.media.ic_split_line')).width($r('app.float.split_line_width')).height($r('app.float.split_line_height'))Image($r('app.media.ic_more')).width($r('app.float.more_width')).height($r('app.float.more_height')).margin({left: $r('app.float.more_margin_left'),right: $r('app.float.more_margin_right')})Text($r('app.string.title_bar_classification')).fontSize($r('app.float.middle_font_size')).fontColor(Color.White).opacity(this.titleIndex === Const.INVALID_INDEX ? Const.FULL_OPACITY : Const.EIGHTY_OPACITY).fontWeight(this.titleIndex === Const.INVALID_INDEX ? Const.FONT_WEIGHT_FIVE : FontWeight.Normal)}.onClick(() => {this.titleIndex = Const.INVALID_INDEX})}.width(Const.FULL_WIDTH).margin({ top: $r('app.float.classify_title_margin') })}
}
// entry/src/main/ets/viewmodel/HomeViewModel.ets
const classifyTitle: Resource[] = [$r('app.string.title_bar_homepage'),$r('app.string.title_bar_phone'),$r('app.string.title_bar_computer'),$r('app.string.title_bar_foods'),$r('app.string.title_bar_men_wear'),$r('app.string.title_bar_fresh'),$r('app.string.title_bar_furniture_kitchenware')
]export { classifyTitle }
8. 轮播图组件
// entry/src/main/ets/view/SwiperComponent.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import { swiperImage } from '../viewmodel/HomeViewModel'@Component
export default struct SwiperComponent {private dotIndicator: DotIndicator = new DotIndicator()aboutToAppear(){this.dotIndicator.selectedColor($r('app.color.indicator_select'))}build() {Swiper() {ForEach(swiperImage, (item: Resource) => {Image(item).width(Const.FULL_WIDTH).height($r('app.float.swiper_image_height')).borderRadius($r('app.float.swiper_radius')).backgroundColor(Color.White)}, (item: Resource) => JSON.stringify(item))}.indicator(this.dotIndicator).autoPlay(true).itemSpace(0).width(Const.FULL_WIDTH).displayCount(1).margin({top: $r('app.float.swiper_margin_top'),bottom: $r('app.float.swiper_margin_bottom')})}
}
// entry/src/main/ets/viewmodel/HomeViewModel.ets
const classifyTitle: Resource[] = [$r('app.string.title_bar_homepage'),$r('app.string.title_bar_phone'),$r('app.string.title_bar_computer'),$r('app.string.title_bar_foods'),$r('app.string.title_bar_men_wear'),$r('app.string.title_bar_fresh'),$r('app.string.title_bar_furniture_kitchenware')
]const swiperImage: Resource[] = [$r('app.media.ic_home_appliances_special'),$r('app.media.ic_coupons'),$r('app.media.ic_internal_purchase_price')
]export { classifyTitle, swiperImage }
9. 瀑布流组件
// entry/src/main/ets/view/WaterFlowComponent.ets
import ProductItem from '../viewmodel/ProductItem'
import { WaterFlowDataSource } from '../viewmodel/WaterFlowDataSource'
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import { waterFlowData } from '../viewmodel/HomeViewModel'
import FlowItemComponent from '../view/FlowItemComponent'@Component
export default struct WaterFlowComponent {private datasource: WaterFlowDataSource = new WaterFlowDataSource()aboutToAppear() {this.datasource.setDataArray(waterFlowData)}build() {WaterFlow({ footer: (): void => this.itemFoot() }) {LazyForEach(this.datasource, (item: ProductItem) => {FlowItem() {FlowItemComponent({ item: item })}}, (item: ProductItem) => JSON.stringify(item))}.layoutWeight(Const.WATER_FLOW_LAYOUT_WEIGHT).layoutDirection(FlexDirection.Column).columnsTemplate(Const.WATER_FLOW_COLUMNS_TEMPLATE).columnsGap($r('app.float.water_flow_columns_gap')).rowsGap($r('app.float.water_flow_row_gap'))}@BuilderitemFoot() {Column() {Text($r('app.string.footer_text')).fontColor(Color.Gray).fontSize($r('app.float.footer_text_size')).width(Const.FULL_WIDTH).height($r('app.float.footer_text_height')).textAlign(TextAlign.Center)}}
}
// entry/src/main/ets/viewmodel/ProductItem.ets
export interface IProductItem {image_url: Resourcename: stringdiscount: stringprice: stringpromotion: stringbonus_points: string
}export default class ProductItem implements IProductItem {image_url: Resourcename: stringdiscount: stringprice: stringpromotion: stringbonus_points: stringconstructor(props: ProductItem) {this.image_url = props.image_urlthis.name = props.namethis.discount = props.discountthis.price = props.pricethis.promotion = props.promotionthis.bonus_points = props.bonus_points}
}
// entry/src/main/ets/viewmodel/WaterFlowDataSource.ets
import ProductItem from './ProductItem'export class WaterFlowDataSource implements IDataSource {private dataArray: ProductItem[] = []private listeners: DataChangeListener[] = []public setDataArray(productDataArray: ProductItem[]): void {this.dataArray = productDataArray}public totalCount(): number {return this.dataArray.length}public getData(index: number): ProductItem {return this.dataArray[index]}registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener)}}unregisterDataChangeListener(listener: DataChangeListener): void {let pos = this.listeners.indexOf(listener)if (pos >= 0) {this.listeners.splice(pos, 1)}}
}
// entry/src/main/ets/viewmodel/HomeViewModel.ets
import { IProductItem } from './ProductItem'const classifyTitle: Resource[] = [$r('app.string.title_bar_homepage'),$r('app.string.title_bar_phone'),$r('app.string.title_bar_computer'),$r('app.string.title_bar_foods'),$r('app.string.title_bar_men_wear'),$r('app.string.title_bar_fresh'),$r('app.string.title_bar_furniture_kitchenware')
]const swiperImage: Resource[] = [$r('app.media.ic_home_appliances_special'),$r('app.media.ic_coupons'),$r('app.media.ic_internal_purchase_price')
]const waterFlowData: IProductItem[] = [{image_url: $r('app.media.ic_holder_50e'),name: 'XXX50E',discount: '',price: '¥4088',promotion: '',bonus_points: ''},{image_url: $r('app.media.ic_holder_xs2'),name: 'XXXPadMate Xs 2 \n8GB+256GB (雅黑)',discount: '',price: '¥9999',promotion: '限时',bonus_points: ''},{image_url: $r('app.media.ic_holder_computer'),name: 'XX设备 新品优惠!新品优惠!机不可失!失不再来!快来购买!快来购买!',discount: '限时省200',price: '¥10099',promotion: '商品',bonus_points: ''},{image_url: $r('app.media.ic_holder_mouse'),name: '送给亲人 快速购买!',discount: '限时省200',price: '¥199',promotion: '',bonus_points: ''},{image_url: $r('app.media.ic_holder_pad'),name: 'XXXPad Pro',discount: '',price: '¥3499',promotion: '',bonus_points: ''},{image_url: $r('app.media.ic_holder_mate50'),name: 'XXXMate 50 8GB+256GB',discount: '',price: '¥5499',promotion: '',bonus_points: ''},{image_url: $r('app.media.ic_holder_60pro'),name: 'XXX60 Pro 新品上市!\n你值得拥有!\n限时折扣!\n速速购买!',discount: '限时省200',price: '¥1299',promotion: '限时',bonus_points: ''},{image_url: $r('app.media.ic_holder_50e'),name: 'XXX50E',discount: '',price: '¥4088',promotion: '',bonus_points: ''},{image_url: $r('app.media.ic_holder_xs2'),name: 'XXXPadMate Xs 2 \n8GB+256GB (雅黑)',discount: '限时省200',price: '¥9999',promotion: '限时',bonus_points: ''},{image_url: $r('app.media.ic_holder_computer'),name: 'XX设备 新品优惠!新品优惠!',discount: '限时省200',price: '¥10099',promotion: '商品',bonus_points: ''},{image_url: $r('app.media.ic_holder_mouse'),name: '送给亲人 快速购买!',discount: '限时省200',price: '¥199',promotion: '限时',bonus_points: '赠送积分'},{image_url: $r('app.media.ic_holder_pad'),name: 'XXXPad Pro\n限时折扣!\n速速购买!\n机不可失!失不再来!',discount: '限时省200',price: '¥3499',promotion: '',bonus_points: '赠送积分'},{image_url: $r('app.media.ic_holder_mate50'),name: 'XXXMate 50 \n8GB+256GB',discount: '',price: '¥5499',promotion: '',bonus_points: ''},{image_url: $r('app.media.ic_holder_60pro'),name: 'XXX60 Pro',discount: '限时省200',price: '¥1299',promotion: '限时',bonus_points: '',}
];export { classifyTitle, swiperImage, waterFlowData }
// entry/src/main/ets/view/FlowItemComponent.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import ProductItem from '../viewmodel/ProductItem'
import { waterFlowData } from '../viewmodel/HomeViewModel'@Component
export default struct FlowItemComponent {item: ProductItem = waterFlowData[0]build() {Column() {Image(this.item?.image_url).width($r('app.float.product_image_size')).height($r('app.float.product_image_size')).objectFit(ImageFit.Contain).margin({top: $r('app.float.water_flow_image_top'),bottom: $r('app.float.water_flow_image_bottom')})Text(this.item?.name).fontSize($r('app.float.small_font_size')).fontColor(Color.Black).fontWeight(FontWeight.Normal).alignSelf(ItemAlign.Start)Text(this.item?.discount).fontSize($r('app.float.smaller_font_size')).fontColor(Color.Black).fontWeight(FontWeight.Normal).opacity(Const.SIXTY_OPACITY).alignSelf(ItemAlign.Start).margin({bottom: $r('app.float.discount_text_bottom')})Text(this.item?.price).fontSize($r('app.float.middle_font_size')).fontColor($r('app.color.focus_color')).fontWeight(FontWeight.Normal).alignSelf(ItemAlign.Start).lineHeight($r('app.float.piece_line_height'))Row() {if (this.item?.promotion) {Text(`${this.item?.promotion}`).height($r('app.float.promotion_text_height')).fontSize($r('app.float.promotion_font_size')).fontColor(Color.White).borderRadius($r('app.float.promotion_radius')).backgroundColor($r('app.color.focus_color')).padding({left: $r('app.float.promotion_padding_left'),right: $r('app.float.promotion_padding_right')}).margin({top: $r('app.float.promotion_margin_top'),right: $r('app.float.promotion_margin_right')})}if (this.item?.bonus_points) {Text(`${this.item?.bonus_points}`).height($r('app.float.promotion_text_height')).fontSize($r('app.float.promotion_font_size')).fontColor($r('app.color.focus_color')).borderRadius($r('app.float.promotion_radius')).borderWidth($r('app.float.bonus_points_radius_width')).borderColor($r('app.color.focus_color')).padding({left: $r('app.float.bonus_points_padding_left'),right: $r('app.float.bonus_points_padding_right')}).margin({ top: $r('app.float.bonus_points_margin_top') })}}.width(Const.FULL_WIDTH).justifyContent(FlexAlign.Start)}.borderRadius($r('app.float.product_layout_radius')).backgroundColor(Color.White).padding({left: $r('app.float.product_layout_margin_left'),right: $r('app.float.product_layout_margin_right'),bottom: $r('app.float.product_layout_margin_bottom')})}
}
10. 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-06-02.zip。
视频:《开发经典的瀑布流》。