Adaptive page arrangement of a bunch of pictures

Posted by coreycollins on Mon, 01 Jun 2020 05:48:42 +0200

Recently, we are developing a page to display pictures in batches. The adaptive arrangement of pictures is an unavoidable problem

After paying for a lot of hair, I finally finished the picture arrangement and encapsulated it into components. The final effect is as follows

 

 

1, Design ideas

In order to make the structure clear, I process the picture list into a two-dimensional array, the first dimension is row, the second dimension is column

render() {
    const { className } = this.props;
    // imgs is the processed image data, two-dimensional array
    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 ( // Render row
              <div key={`image-row-${i}`} className="w-image-row">
                {Array.isArray(row) &&
                  row.map((item, index) => {
                    return ( // Render columns
                      <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>
    );
  }

The total width of each row cannot exceed the width of the container itself. If the remaining width of the current row is enough, new pictures can be added

It is necessary to calculate the imgWidth of the image after scaling, provided that the original width and height of the image and the height after scaling imgweight are known

 

When we get the picture list through the interface, at least there is a picture link url. Through the url, we can Get the width and height of the picture

If the back-end colleagues are more considerate, they will directly return the width and height of the picture, and they want to be excellent

After obtaining the original width and height of the picture, you can preset a picture height imgHeight as the reference value, and then calculate the picture width after scaling

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

Then a single picture is put into each row in the form of recursion for verification. If the current row can be put, it will be put in the current row. Otherwise, the next row will be judged, or a new row will be opened directly

 

 

Two. Data structure

After the overall scheme is designed, it can be determined that the final processed image data should be as follows:

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},
  ]
]

However, in order to facilitate the calculation of the total width of each row and complete the arrangement of the current row in advance when the remaining width is insufficient, such a data structure is more appropriate in the calculation process:

const rows = [
  {
    img: [], // Picture information, only this field will be kept in the end
    total: 0, // Total width
    over: false, // Whether the current row is arranged
  },
  {
    img: [],
    total: 0,
    over: false,
  }
]

Finally, we just need to bring out the img in rows and generate a two-dimensional array list

After the basic data structure is clear, write a basic function to add default values to new rows

// Handle picture list defaults as a function
const defaultRow = () => ({
  img: [], // Picture information, only this field will be kept in the end
  total: 0, // Total width
  over: false, // Whether the current line is completed
});

Why add default values as functions? In fact, this is the same reason why Vue's data uses functions

If you directly define a pure object as the default value, all row data will share and reference the same data object

Through the defaultRow function, each time a new instance is created, a new copy data object will be returned, so there will be no common reference problem

 

 

3, Append picture to current line

I set a buffer value. If the difference between the total width of the current line and the container width (the upper limit of the width of each line) is within the buffer value, this line can't continue to add pictures. You can directly mark the status of the current line as "completed"

const BUFFER = 30; // Single line width buffer value

Then it is the function to put the picture in the row, which is divided into two parts: recursively determine whether to put the picture in which row, and add the picture to the corresponding row

/**
 * Append a picture to a line
 * @param {Array}  list list
 * @param {Object} img Picture data
 * @param {Number} row Current line index
 * @param {Number} max Maximum width of single line
 */
function addImgToRow(list, img, row, max) {
  if (!list[row]) {
    // Add a new line
    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 the gap in the current row is less than the buffer value, no more filling is needed
  if (max - innerList[row].total < BUFFER) {
    innerList[row].over = true;
  }
  return innerList;
}

/**
 * Add pictures recursively
 * @param {Array} list list
 * @param {Number} row Current line index
 * @param {Objcet} opt Supplementary parameters
 */
function pushImg(list, row, opt) {
  const { maxWidth, item } = opt;
  if (!list[row]) {
    list[row] = defaultRow();
  }
  const total = list[row].total; // Total width of the current row
  if (!list[row].over && item.width + total < maxWidth + BUFFER) {
    // When the width is enough, append the picture to the current line
    return addImgToRow(list, item, row, maxWidth);
  } else {
    // Insufficient width, judge next line
    return pushImg(list, row + 1, opt);
  }
}

 

 

4, Process picture data

Most of the preparation work has been completed. You can try to process the picture data

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');
  // In the constructor constructor Defined in this.containerRef = null;
  const imgs = this.calcWidth(list, this.containerRef.clientWidth, this.imgHeight);
  console.timeEnd('CalcWidth');
  this.setState({ imgs });
}

Main function for processing pictures

/**
 * Process data and generate 2D array based on picture width
 * @param {Array} list data set
 * @param {Number} maxWidth Maximum width of single line, usually container width
 * @param {Number} imgHeight The reference height of each line, according to which the width of the picture can be calculated. Finally, the picture is aligned, and the height will fluctuate
 * @param {Boolean} deal Whether to process abnormal data, default processing
 * @return {Array} 2D array, save picture width by line
 */
calcWidth(list, maxWidth, imgHeight, deal = true) {
  if (!Array.isArray(list) || !maxWidth) {
    return;
  }
  const innerList = JSON.parse(JSON.stringify(list));
  const remaindArr = []; // Compatible with data without width and height
  let allRow = [defaultRow()]; // Initialize first line

  for (const item of innerList) {

    // Processing data without width and height, unified delay processing
    if (!item.height || !item.width) {
      remaindArr.push(item);
      continue;
    }
    const imgWidth = Math.floor(item.width * imgHeight / item.height);
    item.width = imgWidth;
    item.height = imgHeight;
    // Single graph in rows
    if (imgWidth >= maxWidth) {
      allRow = addImgToRow(allRow, item, allRow.length, maxWidth);
      continue;
    }
    // Recursively process the current picture
    allRow = pushImg(allRow, 0, { maxWidth, item });
  }
  console.log('allRow======>', maxWidth, allRow);
  // Handling abnormal data
  deal && this.initRemaindImg(remaindArr);
  return buildImgList(allRow, maxWidth);
}

The last two lines of the main function, calcWidth, first deal with the abnormal data without the original width and height (the next part is detailed), and then deal with the image data with line information as a two-dimensional array

After recursion, the image data is saved in rows, but the total width of each row is different from the width of the actual container. If you directly use the current image width and height, each row will be uneven

So you need to use buildiimglist to organize pictures. There are two main functions. The first is to process picture data into the two-dimensional array function mentioned above

The second function is to recalculate the height and width of the image with the width of the container, so that the image can align with the container:

// Extract picture list
function buildImgList(list, max) {
  const res = [];
  Array.isArray(list) &&
    list.map(row => {
      res.push(alignImgRow(row.img, (max / row.total).toFixed(2)));
    });
  return res;
}

// Adjust row height to align left and right
function alignImgRow(arr, coeff) {
  if (!Array.isArray(arr)) {
    return arr;
  }
  const coe = +coeff; // Width height scaling factor
  return arr.map(x => {
    return {
      ...x,
      width: x.width * coe,
      height: x.height * coe,
    };
  });
}

 

 

5, Process pictures without original width and height

In the process of traversing the data, the main function of the above image processing, calcWidth, recorded the data without the original width and height separately and put it into the final processing

For this part of data, first of all, we need to get the width and height of the image according to the url of the image

// according to url Get picture width and height
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;
  });
}

It should be noted that this process is asynchronous, so I did not process this part of data together with the above picture data

Instead, when all images are found, the data will be processed additionally, and the results will be spliced behind the previous images

// Processing image data without width and height information
initRemaindImg(list) {
  const arr = []; // Get data after width and height
  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;
        // In order to prevent the data abnormality from causing a dead cycle, this time calcWidth No more processing error data
        const imgs2 = this.calcWidth(arr, this.containerRef.clientWidth - 10, this.imgHeight, false);
        this.setState({ imgs: imgs.concat(imgs2) });
      }
    })
  })
}

 

 

6, Full code

import React from 'react';

const BUFFER = 30; // Single line width buffer value

// Handle picture list defaults as a function
const defaultRow = () => ({
  img: [], // Picture information, only this field will be kept in the end
  total: 0, // Total width
  over: false, // Whether the current line is completed
});

/**
 * Append a picture to a line
 * @param {Array}  list list
 * @param {Object} img Picture data
 * @param {Number} row Current line index
 * @param {Number} max Maximum width of single line
 */
function addImgToRow(list, img, row, max) {
  if (!list[row]) {
    // Add a new line
    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 the gap of the current row is less than the buffer value, no more filling is needed
  if (max - innerList[row].total < BUFFER) {
    innerList[row].over = true;
  }
  return innerList;
}

/**
 * Add pictures recursively
 * @param {Array} list list
 * @param {Number} row Current line index
 * @param {Objcet} opt Supplementary parameters
 */
function pushImg(list, row, opt) {
  const { maxWidth, item } = opt;
  if (!list[row]) {
    list[row] = defaultRow();
  }
  const total = list[row].total; // Total width of the current row
  if (!list[row].over && item.width + total < maxWidth + BUFFER) {
    // When the width is enough, append the picture to the current line
    return addImgToRow(list, item, row, maxWidth);
  } else {
    // Insufficient width, judge next line
    return pushImg(list, row + 1, opt);
  }
}

// Extract picture list
function buildImgList(list, max) {
  const res = [];
  Array.isArray(list) &&
    list.map(row => {
      res.push(alignImgRow(row.img, (max / row.total).toFixed(2)));
    });
  return res;
}

// Adjust row height to align left and right
function alignImgRow(arr, coeff) {
  if (!Array.isArray(arr)) {
    return arr;
  }
  const coe = +coeff; // Width height scaling factor
  return arr.map(x => {
    return {
      ...x,
      width: x.width * coe,
      height: x.height * coe,
    };
  });
}

// according to url Get picture width and height
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');
  // In the constructor constructor Defined in this.containerRef = null;
  const imgs = this.calcWidth(list, this.containerRef.clientWidth, this.imgHeight);
  console.timeEnd('CalcWidth');
  this.setState({ imgs });
}

/**
 * Process data and generate 2D array based on picture width
 * @param {Array} list data set
 * @param {Number} maxWidth Maximum width of single line, usually container width
 * @param {Number} imgHeight The reference height of each line, according to which the width of the picture can be calculated. Finally, the picture is aligned, and the height will fluctuate
 * @param {Boolean} deal Whether to process abnormal data, default processing
 * @return {Array} 2D array, save picture width by line
 */
calcWidth(list, maxWidth, imgHeight, deal = true) {
  if (!Array.isArray(list) || !maxWidth) {
    return;
  }
  const innerList = JSON.parse(JSON.stringify(list));
  const remaindArr = []; // Compatible with data without width and height
  let allRow = [defaultRow()]; // Initialize first line

  for (const item of innerList) {

    // Processing data without width and height, unified delay processing
    if (!item.height || !item.width) {
      remaindArr.push(item);
      continue;
    }
    const imgWidth = Math.floor(item.width * imgHeight / item.height);
    item.width = imgWidth;
    item.height = imgHeight;
    // Single graph in rows
    if (imgWidth >= maxWidth) {
      allRow = addImgToRow(allRow, item, allRow.length, maxWidth);
      continue;
    }
    // Recursively process the current picture
    allRow = pushImg(allRow, 0, { maxWidth, item });
  }
  console.log('allRow======>', maxWidth, allRow);
  // Handling abnormal data
  deal && this.initRemaindImg(remaindArr);
  return buildImgList(allRow, maxWidth);
}

// Processing image data without width and height information
initRemaindImg(list) {
  const arr = []; // Get data after width and height
  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;
        // In order to prevent the data abnormality from causing a dead cycle, this time calcWidth No more processing error data
        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 For processed image data, two-dimensional array
    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 ( // Render row
              <div key={`image-row-${i}`} className="w-image-row">
                {Array.isArray(row) &&
                  row.map((item, index) => {
                    return ( // Render columns
                      <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: remember to add the style box sizing: border box to each picture item;

Topics: Javascript JSON React less Vue