本案例主要介绍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 = 1
static readonly SIXTY_OPACITY: number = 0.6
static readonly EIGHTY_OPACITY: number = 0.8
static readonly FONT_WEIGHT_FIVE: number = 500
static readonly WATER_FLOW_LAYOUT_WEIGHT: number = 1
static 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 = 0xFF00
private 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 = 0
build() {
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'))
}
@Builder
itemFoot() {
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: Resource
name: string
discount: string
price: string
promotion: string
bonus_points: string
}
export default class ProductItem implements IProductItem {
image_url: Resource
name: string
discount: string
price: string
promotion: string
bonus_points: string
constructor(props: ProductItem) {
this.image_url = props.image_url
this.name = props.name
this.discount = props.discount
this.price = props.price
this.promotion = props.promotion
this.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。
视频:《开发经典的瀑布流》。