# WaterFallEts **Repository Path**: xiongwg/water-fall-ets ## Basic Information - **Project Name**: WaterFallEts - **Description**: 鸿蒙ets 瀑布流布局 - **Primary Language**: TypeScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2021-10-28 - **Last Updated**: 2024-10-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## \#HarmonyOS挑战赛第四期#等宽不等高瀑布流布局 ### 前言 瀑布流,又被称作为瀑布流式布局,是一种比较流行的网站页面布局方式,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动 **瀑布流式布局的特征如下:** - 内容框宽度固定,高度不固定。 - 内容框从左到右排列,一行排满后,其余内容框就会按顺序排在短的一列后 - 需要在图片加载完成后,才知道该图片的原始宽高 ### 实现效果 图片宽度固定,高度自适应,不拉伸 ![](./img/ets-waterfall.gif) ### 实现过程 1、定义每一行展示的列数及间距,根据列数和宽度,计算出每个图片的容器宽度 ​ 初始化数据: ```typescript // imageListDataModel.ets export class ImageData { id: string src: Resource name: string } export function getImageList(): Array { let imageDataArray: Array = [ { "id": "0", "src": $r('app.media.hdc1'), "name": 'pic1' }, { "id": "1", "src": $r('app.media.hdc2'), "name": 'pic6' }, { "id": "2", "src": $r("app.media.program1024_1"), "name": 'pic2' }, { "id": "3", "src": $r("app.media.program1024_2"), "name": 'pic3' }, { "id": "4", "src": $r("app.media.program1024_3"), "name": 'pic4' }, { "id": "5", "src": $r("app.media.program1024_4"), "name": 'pic5' }, { "id": "6", "src": $r('app.media.hdc3'), "name": 'pic7' }, { "id": "7", "src": $r('app.media.hdc4'), "name": 'pic8' }, { "id": "8", "src": $r('app.media.hdc5'), "name": 'pic9' }, { "id": "9", "src": $r('app.media.hdc6'), "name": 'pic10' }, { "id": "10", "src": $r('app.media.hdc7'), "name": 'pic11' }, { "id": "11", "src": $r('app.media.hdc8'), "name": 'pic12' }, { "id": "12", "src": $r('app.media.hdc9'), "name": 'pic13' }, { "id": "13", "src": $r('app.media.hdc10'), "name": 'pic14' }, { "id": "14", "src": $r('app.media.hdc11'), "name": 'pic15' }, { "id": "15", "src": $r('app.media.hdc12'), "name": 'pic16' }, { "id": "16", "src": $r('app.media.hdc13'), "name": 'pic17' }, ] return imageDataArray } export class ItemData extends ImageData { left: number top: number id: string width: number height: number } ``` 初始化界面 ``` // index.ets import {ItemData, ImageData, getImageList} from '../model/imageListDataModel' @Entry @Component struct Index { @State itemDataList: ItemData[] = getImageList().map((imageData: ImageData) => { let itemData: ItemData = new ItemData() itemData.id = imageData.id itemData.name = imageData.name itemData.src = imageData.src return itemData }) // 一行展示的列数 private columnsCount: number = 2 // 每张图片间的间距 private gap: number = 5 // 每张图片的宽度 private imageWidth = (360 - (this.columnsCount + 1) * this.gap) / this.columnsCount build() { Scroll() { Stack({ alignContent: Alignment.Top }) { ForEach(this.itemDataList, (item: ItemData) => { Stack() { Image(item.src) .width('100%') .objectFit(ImageFit.Contain) .onComplete((msg: { width: number, height: number, componentWidth: number, componentHeight: number }) => { }).border({ width: 1, color: Color.Pink, radius: 5 }) Text(item.name).fontSize(20).margin({ bottom: 10 }).fontColor(Color.Red) } .width(this.imageWidth) .height(item.height) .position({ x: item.left, y: item.top }) }, item => item.id) }.width('100%') }.width('100%') .backgroundColor(Color.White) } } ``` 2、获取所有图片元素的原始宽高 我们在Image的onComplete方法中获取图片的原始宽高 根据图片的原始宽高比、图片实际占用宽度,计算出每张图片实际占用的高度 ```typescript // 图片宽高比 let picRatio: number = msg.width / msg.height // 屏幕密度比 let densityRatio = msg.componentWidth / this.imageWidth // 图片占用的宽度 item.width = msg.componentWidth // 图片应该占用的高度 item.height = msg.componentWidth / picRatio / densityRatio ``` 3、计算各个item的位置 遍历所有容器,开始判断 - 如果当前处于第一行时: 直接设置图片位置【 即 top为间距的大小,left为(当前图片的宽度+间距) * 当前图片的值+间距大小 】,并保存当前元素高度。 - 如果当前不处于第一行时:进行高度对比,通过遍历循环,拿到最小高度和相对应的索引,设置图片位置【 即 top为最小高度值+间距*2,left为 (当前图片的宽度+间距) * 索引 值+间距大小)】,并修改当前索引的高度为当前元素高度。 ```typescript let colHeightArry: number[] = []; for (let i = 0;i < this.itemDataList.length; i++) { if (i < this.columnsCount) { // 确定第一行 this.itemDataList[i].top = this.gap; this.itemDataList[i].left = (this.imageWidth + this.gap) * i + this.gap colHeightArry.push(this.itemDataList[i].height + this.gap); } else { // 其它行 先找到数组中的最小高度以及它的索引 let minHeight = colHeightArry[0]; // 定义最小的高度 let index = 0; // 定义最小高度的下标 for (let j = 0;j < this.columnsCount; j++) { if (minHeight > colHeightArry[j]) { minHeight = colHeightArry[j]; index = j; } } // 设置下一行的第一个盒子的位置 // top的值就是最小列的高度 + gap this.itemDataList[i].top = colHeightArry[index] + this.gap // left的值就是最小距离左边的距离 this.itemDataList[i].left = this.itemDataList[index].left // 修改最小列的高度 // 最小列的高度 = 当前的高度 + 拼接的高度 + 间隙的高度 colHeightArry[index] = colHeightArry[index] + this.itemDataList[i].height + this.gap; } } ``` 完整index.ets代码如下: ```typescript import {ItemData, ImageData, getImageList} from '../model/imageListDataModel' @Entry @Component struct Index { @State itemDataList: ItemData[] = getImageList().map((imageData: ImageData) => { let itemData: ItemData = new ItemData() itemData.id = imageData.id itemData.name = imageData.name itemData.src = imageData.src return itemData }) // 一行展示的列数 private columnsCount: number = 2 // 每张图片间的间距 private gap: number = 5 // 每张图片的宽度 private imageWidth = (360 - (this.columnsCount + 1) * this.gap) / this.columnsCount // 已加载完毕图片的数量 private loadCount: number= 0 build() { Scroll() { Stack({ alignContent: Alignment.Top }) { ForEach(this.itemDataList, (item: ItemData) => { Stack() { Image(item.src) .width('100%') .objectFit(ImageFit.Contain) .onComplete((msg: { width: number, height: number, componentWidth: number, componentHeight: number }) => { this.loadCount++ // 图片宽高比 let picRatio: number = msg.width / msg.height // 屏幕密度比 let densityRatio = msg.componentWidth / this.imageWidth // 图片占用的宽度 item.width = msg.componentWidth // 图片应该占用的高度 item.height = msg.componentWidth / picRatio / densityRatio console.log('===> ' + densityRatio) console.log('===> ' + JSON.stringify(item)) this.calcPositionWhenLoadComplete() }).border({ width: 1, color: Color.Pink, radius: 5 }) Text(item.name).fontSize(20).margin({ bottom: 10 }).fontColor(Color.Red) } .width(this.imageWidth) .height(item.height) .position({ x: item.left, y: item.top }) .onClick(() => { }) }, item => item.id) }.width('100%') }.width('100%') .backgroundColor(Color.White) } calcPositionWhenLoadComplete(): void{ if (this.loadCount == this.itemDataList.length) { console.log('=========LoadCompleted===start=============') let colHeightArry: number[] = []; for (let i = 0;i < this.itemDataList.length; i++) { if (i < this.columnsCount) { // 确定第一行 this.itemDataList[i].top = this.gap; this.itemDataList[i].left = (this.imageWidth + this.gap) * i + this.gap colHeightArry.push(this.itemDataList[i].height + this.gap); } else { // 其它行 先找到数组中的最小高度以及它的索引 let minHeight = colHeightArry[0]; // 定义最小的高度 let index = 0; // 定义最小高度的下标 for (let j = 0;j < this.columnsCount; j++) { if (minHeight > colHeightArry[j]) { minHeight = colHeightArry[j]; index = j; } } // 设置下一行的第一个盒子的位置 // top的值就是最小列的高度 + gap this.itemDataList[i].top = colHeightArry[index] + this.gap // left的值就是最小距离左边的距离 this.itemDataList[i].left = this.itemDataList[index].left // 修改最小列的高度 // 最小列的高度 = 当前的高度 + 拼接的高度 + 间隙的高度 colHeightArry[index] = colHeightArry[index] + this.itemDataList[i].height + this.gap; } } let str = JSON.stringify(this.itemDataList) this.itemDataList = JSON.parse(str) console.log('==============end===========') } } } ``` ### 项目地址: ## **[WaterFallEts](https://gitee.com/xiongwg/water-fall-ets)**