최근에 소규모 프로그램 프로젝트를 진행하고 있는데 다른 앱의 UI 디자인을 일부 빌려왔는데 그 중에는 기존의 폭포 흐름 레이아웃과 비슷하게 꽤 보기 좋은 그림의 가로 레이아웃이 있는 UI가 있습니다. . 그래서 제가 직접 구현해 보았고, 기존의 가로 사진 2장 방식을 표시되는 사진 수를 맞춤 설정할 수 있는 방식으로 확장했습니다. 기본 표시 효과는 다음과 같습니다.
한 줄에 두 장의 그림 표시
한 줄에 다섯 장의 그림 표시
이 방법을 작성할 때 아이디어에 대해 먼저 이야기해 보겠습니다.
효과 분석
위 그림에서 볼 수 있듯이 한 행에 몇 장의 사진이 표시되더라도 그림이 완전히 표시되는 것을 거의 보장할 수 있으며 각 행의 높이가 다릅니다. 각 사진은 거의 완벽하게 표시될 수 있으므로 표시되는 이미지는 행 높이를 동적으로 조정해야 합니다.
픽셀 렌더링은 반올림을 해야 하기 때문에 이미지의 너비와 높이를 계산할 때 1~2px의 오차가 발생합니다. 이 오류는 직접 무시할 수 있으며 이미지가 시각적으로 늘어나지 않습니다.
효과를 분석한 후 다음과 같은 질문이 생겼습니다.
1. 각 행이 내부 사진을 완전히 표시할 수 있는지 확인하는 방법은 무엇입니까? 각 그림의 너비와 높이가 다르다는 것을 알아야 합니다
2. 각 행의 높이를 동적으로 계산하는 방법은 무엇입니까?
3. 결국 남은 사진 수가 한 줄에 표시된 사진 수를 충족하지 못하는 경우 마지막 행에 사진을 배치하는 방법은 무엇입니까?
4.…
문제 분석
먼저 첫 번째 질문을 살펴보겠습니다. 한 줄의 모든 그림이 완전히 표시될 수 있는지 확인하는 방법입니다.
먼저 한 줄에 표시되는 사진 수를 결정할 수 있습니다. 이는 위의 한 줄에 있는 5개의 사진에 대해 numberInLine = 5와 같이 미리 설정되어 있습니다. 같은 행에 있는 각 그림의 높이는 동일하므로 그림의 너비와 모든 그림의 전체 너비의 비율을 기반으로 그림을 실제로 렌더링할 때 단일 행의 너비를 계산할 수 있습니다. :
imageRenderWidth = (imageWidth / imagesTotalWidth) * lineWidth
그림의 실제 너비는 너비와 높이가 다르지만 그림 한 줄의 높이는 동일하므로 먼저 표준 높이 stdHeight를 가정하고 다음을 통해 각 그림의 너비를 비례적으로 조정할 수 있습니다. 한 장의 사진을 원활하게 계산할 수 있도록 이 표준 높이를 사용합니다. 모든 이미지의 전체 너비에 대한 너비의 비율
각 행의 높이를 계산하는 방법
이미지의 너비가 각 행의 상대적인 높이를 결정하는 것은 매우 간단합니다. 각 행의 첫 번째 그림을 기반으로 먼저 첫 번째 그림의 렌더링 너비를 계산한 다음 이 그림의 렌더링 높이를 계산하고 이를 행 높이로 사용합니다. 렌더링 폭. 그러나 한 줄을 채우려면 마지막 그림을 전체 너비(이전 모든 그림의 너비의 합)로 계산해야 합니다. 그렇지 않으면 표현 공식은 다음과 같습니다.
// 第一张图片渲染宽度 firstImageRenderWidth = (firstImageWidth / imagesTotalWidth) * lineWidth // 第一张图片渲染高度,即行高,即该行所有图片高度 lineHeight = imagesRenderHeight = firstImageRenderWidth / (firstImageWidth / firstImageHeight) // 中间图片渲染宽度 middleImageRenderWidth = lineHeight * (middleImageWidth / middleImageHeight) // 最后一张图片渲染宽度 lastImageRenderWidth = lineWidth - otherImagesTotalRenderWidth
남은 사진 수가 부족할 때 한 줄 수량이 있을 때 레이아웃은 어떻게 하나요?
이 문제는 두 가지 상황에서 고려해야 합니다.
1. 단일 행에 5개의 사진이 필요하고 나머지 수가 5개 미만이고 1개보다 큰 경우(예: 4개 사진): 행을 배치할 수 있습니다. 남은 사진 수에 따라 알고리즘은 여전히 위와 동일합니다. 따라서 이 시점에서는 코드를 재사용할 수 있어야 합니다.
2. 사진이 1개만 남은 경우 다음과 같은 여러 가지 처리 방법이 있습니다.
이 사진은 전체 선 너비를 채우고 완전히 표시할 수 있지만, 이 사진이 높이가 매우 높은 그림은 레이아웃의 아름다움에 심각한 영향을 미칩니다. 그림은 여전히 선 너비를 채우지만 최대 높이가 주어지면 높이가 최대 높이보다 작을 때 완전히 표시되고 다음 경우에만 표시됩니다. 부분적으로는 레이아웃의 아름다움을 보장할 수 있지만 마지막 사진의 표시에 결함이 있습니다.
이전 행의 행 높이를 마지막 행의 행 높이로 사용합니다. 전체 레이아웃의 일관성을 유지하면서 그림을 완전히 표시할 수 있지만 그렇게 하면 마지막 줄에 많은 공백이 남게 됩니다.
위의 세 가지 처리 방법에 대해 저자는 두 번째 방법을 사용합니다. 관심 있는 친구들은 다른 두 가지 방법을 스스로 시도해 볼 수 있습니다. 아니면 더 나은 레이아웃 방법이 있다면 댓글로 작성자에게 알려주세요!
위 세 가지 질문에 대한 설명이 이해가 되셨는지 모르겠네요? 이해하지 못해도 괜찮습니다. 이러한 문제를 해결하는 방법을 코드를 통해 직접 배울 수 있습니다.
코드 구현위 내용은 JavaScript에서 수평 폭포 흐름을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!/* 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)
})