將一堆圖片自適應頁面排列


最近在開發一個批量展示圖片的頁面,圖片的自適應排列是一個無法避免的問題

在付出了許多頭發的代價之后,終於完成了圖片排列,並封裝成組件,最終效果如下

 

 

一、設計思路

為了使結構清晰,我將圖片列表處理成了二維數組,第一維為行,第二維為列

render() { const { className } = this.props; // imgs 為處理后的圖片數據,二維數組 const { imgs } = this.state; return ( <div ref={ref => (this.containerRef = ref)} className={className ? `w-image-list ${className}` : 'w-image-list'} > {Array.isArray(imgs) && imgs.map((row, i) => { return ( // 渲染行 <div key={`image-row-${i}`} className="w-image-row"> {Array.isArray(row) && row.map((item, index) => { return ( // 渲染列 <div key={`image-${i}-${index}`} className="w-image-item" style={{ height: `${item.height}px`, width: `${item.width}px`, }} onClick={() => { this.handleSelect(item); }} > <img src={item.url} alt={item.title} />
                      </div> ); })} </div> ); })} </div> ); }

每一行的總寬度不能超過容器本身的寬度,當前行如果剩余寬度足夠,就可以追加新圖片

而這就需要算出圖片等比縮放后的寬度 imgWidth,前提條件是知道圖片的原始寬高縮放后的高度 imgHeight

 

通過接口獲取到圖片列表的時候,至少是有圖片鏈接 url 的,通過 url 我們就能獲取到圖片的寬高

如果后端的同事更貼心一點,直接就返回了圖片寬高,就想當優秀了

獲取到圖片的原始寬高之后,可以先預設一個圖片高度 imgHeight 作為基准值,然后算出等比縮放之后的圖片寬度

const imgWidth = Math.floor(item.width * imgHeight / item.height);

然后將單個圖片通過遞歸的形式放到每一行進行校驗,如果當前行能放得下,就放在當前行,否則判斷下一行,或者直接開啟新的一行

 

 

二、數據結構

整體的方案設計好了之后,就可以確定最終處理好的圖片數據應該是這樣的:

const list = [ [ {id: String, width: Number, height: Number, title: String, url: String}, {id: String, width: Number, height: Number, title: String, url: String}, ],[ {id: String, width: Number, height: Number, title: String, url: String}, {id: String, width: Number, height: Number, title: String, url: String}, ] ]

不過為了方便計算每一行的總寬度,並在剩余寬度不足時提前完成當前行的排列,所以在計算的過程中,這樣的數據結構更合適:

const rows = [ { img: [], // 圖片信息,最終只保留該字段
    total: 0, // 總寬度
    over: false, // 當前行是否完成排列
  }, { img: [], total: 0, over: false, } ]

最后只需要將 rows 中的 img 提出來,生成二維數組 list 即可 

基礎數據結構明確了之后,接下來先寫一個給新增行添加默認值的基礎函數

// 以函數的形式處理圖片列表默認值
const defaultRow = () => ({ img: [], // 圖片信息,最終只保留該字段
  total: 0, // 總寬度
  over: false, // 當前行是否完成
});

為什么會采用函數的形式添加默認值呢?其實這和 Vue 的 data 為什么會采用函數是一個道理

如果直接定義一個純粹的對象作為默認值,會讓所有的行數據都共享引用同一個數據對象

而通過 defaultRow 函數,每次創建一個新實例后,會返回一個全新副本數據對象,就不會有共同引用的問題

 

 

三、向當前行追加圖片

我設置了一個緩沖值,假如當前行的總寬度與容器寬度(每行的寬度上限)的差值在緩沖值之內,這一行就沒法再繼續添加圖片,可以直接將當前行的狀態標記為“已完成”

const BUFFER = 30; // 單行寬度緩沖值

然后是將圖片放到行里面的函數,分為兩部分:遞歸判斷是否將圖片放到哪一行,將圖片添加到對應行

/** * 向某一行追加圖片 * @param {Array} list 列表 * @param {Object} img 圖片數據 * @param {Number} row 當前行 index * @param {Number} max 單行最大寬度 */
function addImgToRow(list, img, row, max) { if (!list[row]) { // 新增一行
    list[row] = defaultRow(); } const total = list[row].total; const innerList = JSON.parse(JSON.stringify(list)); innerList[row].img.push(img); innerList[row].total = total + img.width; // 當前行若空隙小於緩沖值,則不再補圖
  if (max - innerList[row].total < BUFFER) { innerList[row].over = true; } return innerList; } /** * 遞歸添加圖片 * @param {Array} list 列表 * @param {Number} row 當前行 index * @param {Objcet} opt 補充參數 */
function pushImg(list, row, opt) { const { maxWidth, item } = opt; if (!list[row]) { list[row] = defaultRow(); } const total = list[row].total; // 當前行的總寬度
  if (!list[row].over && item.width + total < maxWidth + BUFFER) { // 寬度足夠時,向當前行追加圖片
    return addImgToRow(list, item, row, maxWidth); } else { // 寬度不足,判斷下一行
    return pushImg(list, row + 1, opt); } }

 

 

四、處理圖片數據

大部分的准備工作已經完成,可以試着處理圖片數據了

constructor(props) { super(props); this.containerRef = null; this.imgHeight = this.props.imgHeight || 200; this.state = { imgs: null, }; } componentDidMount() { const { list = mock } = this.props; console.time('CalcWidth'); // 在構造函數 constructor 中定義 this.containerRef = null;
  const imgs = this.calcWidth(list, this.containerRef.clientWidth, this.imgHeight); console.timeEnd('CalcWidth'); this.setState({ imgs }); }

處理圖片的主函數

/** * 處理數據,根據圖片寬度生成二維數組 * @param {Array} list 數據集 * @param {Number} maxWidth 單行最大寬度,通常為容器寬度 * @param {Number} imgHeight 每行的基准高度,根據這個高度算出圖片寬度,最終為對齊圖片,高度會有浮動 * @param {Boolean} deal 是否處理異常數據,默認處理 * @return {Array} 二維數組,按行保存圖片寬度 */ calcWidth(list, maxWidth, imgHeight, deal = true) { if (!Array.isArray(list) || !maxWidth) { return; } const innerList = JSON.parse(JSON.stringify(list)); const remaindArr = []; // 兼容不含寬高的數據
  let allRow = [defaultRow()]; // 初始化第一行

  for (const item of innerList) { // 處理不含寬高的數據,統一延后處理
    if (!item.height || !item.width) { remaindArr.push(item); continue; } const imgWidth = Math.floor(item.width * imgHeight / item.height); item.width = imgWidth; item.height = imgHeight; // 單圖成行
    if (imgWidth >= maxWidth) { allRow = addImgToRow(allRow, item, allRow.length, maxWidth); continue; } // 遞歸處理當前圖片
    allRow = pushImg(allRow, 0, { maxWidth, item }); } console.log('allRow======>', maxWidth, allRow); // 處理異常數據
  deal && this.initRemaindImg(remaindArr); return buildImgList(allRow, maxWidth); }

主函數 calcWidth 的最后兩行,首先處理了沒有原始寬高的異常數據(下一部分細講),然后將帶有行信息的圖片數據處理為二維數組

遞歸之后的圖片數據按行保存,但每一行的總寬度都和實際容器的寬度有出入,如果直接使用當前的圖片寬高,會導致每一行參差不齊

所以需要使用 buildImgList 來整理圖片,主要作用有兩個,第一個作用是將圖片數據處理為上面提到的二維數組函數

第二個作用則是用容器的寬度來重新計算圖片高寬,讓圖片能夠對齊容器:

// 提取圖片列表
function buildImgList(list, max) { const res = []; Array.isArray(list) && list.map(row => { res.push(alignImgRow(row.img, (max / row.total).toFixed(2))); }); return res; } // 調整單行高度以左右對齊
function alignImgRow(arr, coeff) { if (!Array.isArray(arr)) { return arr; } const coe = +coeff; // 寬高縮放系數
  return arr.map(x => { return { ...x, width: x.width * coe, height: x.height * coe, }; }); }

 

 

五、處理沒有原始寬高的圖片

上面處理圖片的主函數 calcWidth 在遍歷數據的過程中,將沒有原始寬高的數據單獨記錄了下來,放到最后處理

對於這一部分數據,首先需要根據圖片的 url 獲取到圖片的寬高

// 根據 url 獲取圖片寬高
function checkImgWidth(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = function() { const res = { width: this.width, height: this.height, }; resolve(res); }; img.src = url; }); }

需要注意的是,這個過程是異步的,所以我沒有將這部分數據和上面的圖片數據一起處理

而是當所有圖片寬高都查詢到之后,額外處理這部分數據,並將結果拼接到之前的圖片后面

// 處理沒有寬高信息的圖片數據
initRemaindImg(list) { const arr = []; // 獲取到寬高之后的數據
  let count = 0; list && list.map(x => { checkImgWidth(x.url).then(res => { count++; arr.push({ ...x, ...res }) if (count === list.length) { const { imgs } = this.state; // 為防止數據異常導致死循環,本次 calcWidth 不再處理錯誤數據
        const imgs2 = this.calcWidth(arr, this.containerRef.clientWidth - 10, this.imgHeight, false); this.setState({ imgs: imgs.concat(imgs2) }); } }) }) }

 

 

六、完整代碼

import React from 'react'; const BUFFER = 30; // 單行寬度緩沖值

// 以函數的形式處理圖片列表默認值
const defaultRow = () => ({ img: [], // 圖片信息,最終只保留該字段
  total: 0, // 總寬度
  over: false, // 當前行是否完成
}); /** * 向某一行追加圖片 * @param {Array} list 列表 * @param {Object} img 圖片數據 * @param {Number} row 當前行 index * @param {Number} max 單行最大寬度 */
function addImgToRow(list, img, row, max) { if (!list[row]) { // 新增一行
    list[row] = defaultRow(); } const total = list[row].total; const innerList = JSON.parse(JSON.stringify(list)); innerList[row].img.push(img); innerList[row].total = total + img.width; // 當前行若空隙小於緩沖值,則不再補圖
  if (max - innerList[row].total < BUFFER) { innerList[row].over = true; } return innerList; } /** * 遞歸添加圖片 * @param {Array} list 列表 * @param {Number} row 當前行 index * @param {Objcet} opt 補充參數 */
function pushImg(list, row, opt) { const { maxWidth, item } = opt; if (!list[row]) { list[row] = defaultRow(); } const total = list[row].total; // 當前行的總寬度
  if (!list[row].over && item.width + total < maxWidth + BUFFER) { // 寬度足夠時,向當前行追加圖片
    return addImgToRow(list, item, row, maxWidth); } else { // 寬度不足,判斷下一行
    return pushImg(list, row + 1, opt); } } // 提取圖片列表
function buildImgList(list, max) { const res = []; Array.isArray(list) && list.map(row => { res.push(alignImgRow(row.img, (max / row.total).toFixed(2))); }); return res; } // 調整單行高度以左右對齊
function alignImgRow(arr, coeff) { if (!Array.isArray(arr)) { return arr; } const coe = +coeff; // 寬高縮放系數
  return arr.map(x => { return { ...x, width: x.width * coe, height: x.height * coe, }; }); } // 根據 url 獲取圖片寬高
function checkImgWidth(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = function() { const res = { width: this.width, height: this.height, }; resolve(res); }; img.src = url; }); } export default class ImageList extends React.Component { constructor(props) { super(props); this.containerRef = null; this.imgHeight = this.props.imgHeight || 200; this.state = { imgs: null, }; } componentDidMount() { const { list } = this.props; console.time('CalcWidth'); // 在構造函數 constructor 中定義 this.containerRef = null;
  const imgs = this.calcWidth(list, this.containerRef.clientWidth, this.imgHeight); console.timeEnd('CalcWidth'); this.setState({ imgs }); } /** * 處理數據,根據圖片寬度生成二維數組 * @param {Array} list 數據集 * @param {Number} maxWidth 單行最大寬度,通常為容器寬度 * @param {Number} imgHeight 每行的基准高度,根據這個高度算出圖片寬度,最終為對齊圖片,高度會有浮動 * @param {Boolean} deal 是否處理異常數據,默認處理 * @return {Array} 二維數組,按行保存圖片寬度 */ calcWidth(list, maxWidth, imgHeight, deal = true) { if (!Array.isArray(list) || !maxWidth) { return; } const innerList = JSON.parse(JSON.stringify(list)); const remaindArr = []; // 兼容不含寬高的數據
  let allRow = [defaultRow()]; // 初始化第一行

  for (const item of innerList) { // 處理不含寬高的數據,統一延后處理
    if (!item.height || !item.width) { remaindArr.push(item); continue; } const imgWidth = Math.floor(item.width * imgHeight / item.height); item.width = imgWidth; item.height = imgHeight; // 單圖成行
    if (imgWidth >= maxWidth) { allRow = addImgToRow(allRow, item, allRow.length, maxWidth); continue; } // 遞歸處理當前圖片
    allRow = pushImg(allRow, 0, { maxWidth, item }); } console.log('allRow======>', maxWidth, allRow); // 處理異常數據
  deal && this.initRemaindImg(remaindArr); return buildImgList(allRow, maxWidth); } // 處理沒有寬高信息的圖片數據
initRemaindImg(list) { const arr = []; // 獲取到寬高之后的數據
  let count = 0; list && list.map(x => { checkImgWidth(x.url).then(res => { count++; arr.push({ ...x, ...res }) if (count === list.length) { const { imgs } = this.state; // 為防止數據異常導致死循環,本次 calcWidth 不再處理錯誤數據
        const imgs2 = this.calcWidth(arr, this.containerRef.clientWidth - 10, this.imgHeight, false); this.setState({ imgs: imgs.concat(imgs2) }); } }) }) } handleSelect = item => { console.log('handleSelect', item); }; render() { const { className } = this.props; // imgs 為處理后的圖片數據,二維數組
    const { imgs } = this.state; return ( <div ref={ref => (this.containerRef = ref)} className={className ? `w-image-list ${className}` : 'w-image-list'} > {Array.isArray(imgs) && imgs.map((row, i) => { return ( // 渲染行
              <div key={`image-row-${i}`} className="w-image-row"> {Array.isArray(row) && row.map((item, index) => { return ( // 渲染列
                      <div key={`image-${i}-${index}`} className="w-image-item" style={{ height: `${item.height}px`, width: `${item.width}px`, }} onClick={() => { this.handleSelect(item); }} >
                        <img src={item.url} alt={item.title} />
                      </div>
 ); })} </div>
 ); })} </div>
 ); } }

PS: 記得給每個圖片 item 添加樣式 box-sizing: border-box;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM