最近在做一個小程序項目,在 UI 上借鑒了一下其他 App 設計,其中有一個圖片橫向布局鋪滿的 UI 感覺挺好看的,類似于傳統的瀑布流布局橫過來一樣。于是就自己實現了一下,并且將原本的橫向兩張圖的方法擴展了下,改成了可以自定義顯示張數的方法。下面是基本的顯示效果:
下面先說說寫這個方法時的思路:
效果分析
可以看到在上圖中,單行不管顯示幾張圖片,都幾乎能夠保證圖片被完整顯示出來,并且每一行的高度都不同——這是為了保證每張圖都能幾乎完整顯示,所以必須要根據實際要顯示的圖片來動態地調整行高。
由于像素渲染必須取整,所以計算圖片的寬高方面會存在 1~2px 的誤差。這個誤差可以直接忽略,并不會導致圖片在視覺上產生拉伸。
分析完效果后,就有了下面幾個問題:
- 如何保證每一行都能完整顯示里面的圖片?要知道每張圖片的寬高都是不同的
- 如何動態計算每一行的高度?
- 在最后圖片剩余數量不滿足單行顯示的圖片數的情況下,如何對最后一行的圖片進行布局?
- ……
問題分析
先來看第一個問題:如何保證單行的每一張圖片都能完整顯示。
首先我們可以確定單行的圖片顯示數量,這個是預先設置好的,比如上面的單行 5 張圖 numberInLine = 5
。而同一行中的每張圖片高度都相同,這樣就可以根據圖片寬度與所有圖片的總寬度的比值,計算出這張圖片實際渲染時占單行的寬度,公式如下:
imageRenderWidth = (imageWidth / imagesTotalWidth) * lineWidth
雖然圖片的實際寬高各不相同,但是由于單行圖片的高度都相同,我們就可以通過先假設一個標準高度
stdHeight
,通過這個標準高度把每張圖片的寬度都進行比例縮放,這樣就可以順利計算出單張圖片寬度在所有圖片總寬度中的比值
如何計算每一行的高度
在能夠確定圖片寬度的前提下,要確定每一行的高度相對就非常簡單了。以每行第一張圖片為基準,先計算出第一張圖片的渲染寬度,然后計算出這張圖片的渲染高度,并以此作為行高,之后的每張圖片都通過行高計算出各自的渲染寬度。但是需要注意的是,為了填滿單行,最后一張圖片需要通過總寬度-之前所有圖片寬度之和的方式計算出,否則就會有空白,表達公式如下:
// 第一張圖片渲染寬度
firstImageRenderWidth = (firstImageWidth / imagesTotalWidth) * lineWidth
// 第一張圖片渲染高度,即行高,即該行所有圖片高度
lineHeight = imagesRenderHeight = firstImageRenderWidth / (firstImageWidth / firstImageHeight)
// 中間圖片渲染寬度
middleImageRenderWidth = lineHeight * (middleImageWidth / middleImageHeight)
// 最后一張圖片渲染寬度
lastImageRenderWidth = lineWidth - otherImagesTotalRenderWidth
當剩余圖片數量不足單行數量時如何布局?
這個問題需要分兩種情況來考慮:
- 當單行需要 5 張,而剩余數量不足 5 張但大于 1 張時(如 4 張圖片):該行可按照剩余圖片的數量布局,算法依然如上。所以對于這點,需要讓代碼具有可復用性;
- 只剩下 1 張圖片時,有下面幾種處理方法:
- 可以將這張圖片占滿全部行寬并且完整顯示,但是如果這張圖片是高度很高的圖片,就會嚴重影響布局的美觀性
- 依然將圖片占滿行寬,但是給定一個最大高度,當高度不及最大高度時完整顯示,當超過時只顯示部分,這樣能保證布局美觀性,但是最后一張圖片的顯示存在瑕疵
- 取前一行的行高作為最后一行的行高,這樣可以在保證整體布局一致性的情況下,依然可以完整顯示圖片,但是這樣做最后一行會留有大量空白位置
對于上面三種處理方式,作者采用的是第二種。感興趣的小伙伴可以自己嘗試其他兩種方式。或者如果你有更好的布局方式,也可以在評論里告訴作者哦!
不知道上面三個問題的解釋小伙伴們有沒有理解了呢?不理解也沒事,可以直接通過代碼來了解是如何解決這些問題的。
代碼實現
/* imagesLayout.js */
/*
* 圖片橫向瀑布流布局 最大限度保證每張圖片完整顯示 可以獲取最后計算出來的圖片布局寬高信息 最后的瀑布流效果需要配合 css 實現(作者通過浮動布局實現)當然你也可以對代碼進行修改 讓其能夠直接返回一段已經布局完成的 html 結構
* 需要先提供每張圖片的寬高 如果沒有圖片的寬高數據 則可以在代碼中添加處理方法從而獲取到圖片寬高數據后再布局 但是并不推薦這樣做
* 盡量保證圖片總數能被單行顯示的數量整除 避免最后一行單張顯示 否則會影響美觀
* 每張圖由于寬高取整返回的寬高存在0-2px的誤差 可以通過 css 保證視覺效果
*/
/*
* @param images {Object} 圖片對象列表,每一個對象有 src width height 三個屬性
* @param containerWidth {Integer} 容器寬度
* @param numberInLine {Integer} 單行顯示圖片數量
* @param limit {Integer} 限制需要進行布局的圖片的數量 如果傳入的圖片列表有100張 但只需要對前20張進行布局 后面的圖片忽略 則可以使用此參數限制 如果不傳則默認0(不限制)
* @param stdRatio {Float} 圖片標準寬高比
*/
class ImagesLayout {
constructor(images, containerWidth, numberInLine = 10, limit = 0, stdRatio = 1.5) {
// 圖片列表
this.images = images
// 布局完畢的圖片列表 通過該屬性可以獲得圖片布局的寬高信息
this.completedImages = []
// 容器寬度
this.containerWidth = containerWidth
// 單行顯示的圖片數量
this.numberInLine = numberInLine
// 限制布局的數量 如果傳入的圖片列表有100張 但只需要對前20張進行布局 后面的圖片忽略 則可以使用此參數限制 如果不傳則默認0(不限制)
this.limit = limit
// 圖片標準寬高比(當最后一行只剩一張圖片時 為了不讓布局看上去很奇怪 所以要有一個標準寬高比 當圖片實際寬高比大于標準寬高比時會發生截取 小于時按照實際高度占滿整行顯示)
this.stdRatio = stdRatio
// 圖片撐滿整行時的標準高度
this.stdHeight = this.containerWidth / this.stdRatio
this.chunkAndLayout()
}
// 將圖片列表根據單行數量分塊并開始計算布局
chunkAndLayout () {
// 當圖片只有一張時,完整顯示這張圖片
if (this.images.length === 1) {
this.layoutFullImage(this.images[0])
return
}
let temp = []
for (let i = 0; i < this.images.length; i++) {
if (this.limit && i >= this.limit) return
temp.push(this.images[i])
// 當單行圖片數量達到限制數量時
// 當已經是最后一張圖片時
// 當已經達到需要布局的最大數量時
if (i % this.numberInLine === this.numberInLine - 1 || i === this.images.length - 1 || i === this.limit - 1) {
this.computedImagesLayout(temp)
temp = []
}
}
}
// 完整顯示圖片
layoutFullImage (image) {
let ratio = image.width / image.height
image.width = this.containerWidth
image.height = parseInt(this.containerWidth / ratio)
this.completedImages.push(image)
}
// 根據分塊計算圖片布局信息
computedImagesLayout(images) {
if (images.length === 1) {
// 當前分組只有一張圖片時
this.layoutWithSingleImage(images[0])
} else {
// 當前分組有多張圖片時
this.layoutWithMultipleImages(images)
}
}
// 分組中只有一張圖片 該張圖片會單獨占滿整行的布局 如果圖片高度過大則以標準寬高比為標準 其余部分剪裁 否則完整顯示
layoutWithSingleImage (image) {
let ratio = image.width / image.height
image.width = this.containerWidth
// 如果是長圖,則布局時按照標準寬高比顯示中間部分
if (ratio < this.stdRatio) {
image.height = parseInt(this.stdHeight)
} else {
image.height = parseInt(this.containerWidth / ratio)
}
this.completedImages.push(image)
}
// 分組中有多張圖片時的布局
// 以相對圖寬為標準,根據每張圖的相對寬度計算占據容器的寬度
layoutWithMultipleImages(images) {
let widths = [] // 保存每張圖的相對寬度
let ratios = [] // 保存每張圖的寬高比
images.forEach(item => {
// 計算每張圖的寬高比
let ratio = item.width / item.height
// 根據標準高度計算相對圖寬
let relateWidth = this.stdHeight * ratio
widths.push(relateWidth)
ratios.push(ratio)
})
// 計算每張圖片相對寬度的總和
let totalWidth = widths.reduce((sum, item) => sum + item, 0)
let lineHeight = 0 // 行高
let leftWidth = this.containerWidth // 容器剩余寬度 還未開始布局時的剩余寬度等于容器寬度
images.forEach((item, i) => {
if (i === 0) {
// 第一張圖片
// 通過圖片相對寬度與相對總寬度的比值計算出在容器中占據的寬度與高度
item.width = parseInt(this.containerWidth * (widths[i] / totalWidth))
item.height = lineHeight = parseInt(item.width / ratios[i])
// 第一張圖片布局后的剩余容器寬度
leftWidth = leftWidth - item.width
} else if (i === images.length - 1) {
// 最后一張圖片
// 寬度為剩余容器寬度
item.width = leftWidth
item.height = lineHeight
} else {
// 中間圖片
// 以行高為標準 計算出圖片在容器中的寬度
item.height = lineHeight
item.width = parseInt(item.height * ratios[i])
// 圖片布局后剩余的容器寬度
leftWidth = leftWidth - item.width
}
this.completedImages.push(item)
})
}
}
<!-- imagesLayout.html -->
<!-- css 布局通過浮動實現 -->
<style>
* {
box-sizing: border-box;
}
#horizontal-waterfull {
width: 300px;
}
#horizontal-waterfull:before, #horizontal-waterfull:after {
content: '';
display: table;
clear: both;
}
img {
display: block;
width: 100%;
height: 100%;
}
.image-box {
float: left;
padding: 1px;
overflow: hidden;
}
</style>
// 水平布局瀑布流容器
<div id="horizontal-waterfull"></div>
<script src="./imagesLayout.js"></script>
<script>
// 待布局圖片列表
const images = [{
src: 'https://static.cxstore.top/images/lake.jpg',
width: 4000,
height: 6000
}, {
src: 'https://static.cxstore.top/images/japan.jpg',
width: 1500,
height: 1125
}, {
src: 'https://static.cxstore.top/images/girl.jpg',
width: 5616,
height: 3266
}, {
src: 'https://static.cxstore.top/images/flower.jpg',
width: 4864,
height: 3648
}, {
src: 'https://static.cxstore.top/images/lake.jpg',
width: 4000,
height: 6000
}, {
src: 'https://static.cxstore.top/images/japan.jpg',
width: 1500,
height: 1125
}, {
src: 'https://static.cxstore.top/images/grass.jpg',
width: 5184,
height: 2916
}]
// 獲取布局容器
const $box = document.getElementById('horizontal-waterfull')
// 創建一個布局實例
const layout = new ImagesLayout(images, $box.clientWidth, 4)
// 通過 layout 的 completedImages 獲取所有圖片的布局信息
layout.completedImages.forEach(item => {
let $imageBox = document.createElement('div')
$imageBox.setAttribute('class', 'image-box')
$imageBox.style.width = item.width + 'px'
$imageBox.style.height = item.height + 'px'
let $image = document.createElement('img')
$image.setAttribute('src', item.src)
$imageBox.appendChild($image)
$box.appendChild($imageBox)
})
也你可以直接到 github 上看 demo 的源碼:Demo on github