天天看點

div 左右排列 高度自适應_手把手教你如何實作大量圖檔的自适應圖檔頁面的排列...

div 左右排列 高度自适應_手把手教你如何實作大量圖檔的自适應圖檔頁面的排列...
前言

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

在付出了許多頭發的代價之後,終于完成了圖檔排列,并封裝成元件,最終效果如下:

div 左右排列 高度自适應_手把手教你如何實作大量圖檔的自适應圖檔頁面的排列...
1、設計思路

為了使結構清晰,我将圖檔清單處理成了二維數組,第一維為行,第二維為列

<
           

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

而這就需要算出圖檔等比縮放後的寬度 imgWidth,前提條件是知道圖檔的原始寬高和縮放後的高度 imgHeight,通過接口擷取到圖檔清單的時候,至少是有圖檔連結 url 的,通過 url 我們就能擷取到圖檔的寬高。

如果後端的同僚更貼心一點,直接就傳回了圖檔寬高,就相當優秀了。

擷取到圖檔的原始寬高之後,可以先預設一個圖檔高度 imgHeight 作為基準值,然後算出等比縮放之後的圖檔寬度

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

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

2、資料結構

整體的方案設計好了之後,就可以确定最終處理好的圖檔資料應該是這樣的:

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 函數,每次建立一個新執行個體後,會傳回一個全新副本資料對象,就不會有共同引用的問題。

3、向目前行追加圖檔

我設定了一個緩沖值,假如目前行的總寬度與容器寬度(每行的寬度上限)的內插補點在緩沖值之内,這一行就沒法再繼續添加圖檔,可以直接将目前行的狀态标記為“已完成”。

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);
  }
}
           
4、處理圖檔資料

大部分的準備工作已經完成,可以試着處理圖檔資料了。

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,
    };
  });
}
           
5、處理沒有原始寬高的圖檔

上面處理圖檔的主函數 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) });
      }
    })
  })
}
           
6、完整代碼
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。

繼續閱讀