Cat waterfall
As shown in the following dynamic picture, do the irregular pictures of cute cats arouse your girlish heart?
Waterfall flow, also known as waterfall flow layout, is a popular way of website page layout. There are many ways to implement waterfall flow, but the principle is the same. In this paper, let's introduce in detail how to implement cat waterfall flow.

Waterfall flow principle

As shown in the above figure: the first, second, third, fourth and fifth figures are arranged in the first row of the container, that is, near the top.
We will find that figure 6 is not below figure 1, but below figure 3.
In fact, this is the key point of waterfall flow. What is the arrangement of Picture 6?
In fact, it will be placed under the picture with the smallest distance from the bottom to the top in the current arrangement picture, so that the picture difference will not be very large. We can see that 3 is the picture with the smallest height, and then we will put the sixth picture under Figure 3.
Similarly, the seventh figure should be in the position shown in the figure below.

So do you know where the eighth picture should be placed? Let's leave a question for everyone to think about. We'll reveal the answer at the end of the article. If you can't wait, you can slide to the end of the article to see if you guess right.
Preload picture
We probably know the principle of realizing waterfall flow, so how to realize the specific technical implementation?
In fact, it is to set the offset value of the picture according to the width and height of the picture, that is, the top and left values.
This means that we must know the width height ratio of the picture, because the width of a column here needs to be consistent, that is, a fixed value can be set.
If we wait until the rendering is finished to obtain the height, and then set the top value and left value, the interface will flash.
Therefore, we need to preload the pictures and obtain the width and height at the beginning, but do not render. The time is ripe, that is, all the pictures are loaded, that is, the height of all the pictures is calculated and then rendered. It is very simple to say, but how should we operate the specific implementation?
1. Traverse the img array passed in
//imgsArr is a picture array passed in from the outside of the component, in which a src represents the path of the picture this.imgsArr.forEach((imgItem, imgIndex) => { //... //... }) Copy code
2.loadedCount records the loading quantity
//Declare the loadedCount variable to record the number of completed loads. In order to compare with the size of imgsArr, notify the completion of loading (including no graph, loading completed and loading failed) data(){ return { loadedCount: 0 } } Copy code
3. Without drawing
// When there is no diagram, record the height as 0 if (!imgItem[this.srcKey]) { this.imgsArr[imgIndex]._height = "0"; this.loadedCount++; // No graph mode is supported if (this.loadedCount == this.imgsArr.length) { this.$emit("preloaded"); } return; } Copy code
4.Image object
//Use the Image API to execute when the src attribute changes and the load is complete let oImg = new Image(); oImg.src = imgItem[this.srcKey]; oImg.onload = oImg.onerror = (e) => { //... } Copy code
5. After loading, calculate the height of the actual picture to be rendered
//Theoretically, the height of the preloaded picture / the width of the preloaded picture = the height of the picture to be rendered / the width of the picture this.imgsArr[imgIndex]._height = e.type == "load" ? Math.round(this.imgWidth_c * (oImg.height / oImg.width)) : this.imgWidth_c; Copy code
6. After loading fails, identify the failure flag
if (e.type == "error") { this.imgsArr[imgIndex]._error = true; this.$emit("imgError", this.imgsArr[imgIndex]); } Copy code
7. After all loads are completed, the event emit preloaded occurs
if (this.loadedCount === this.imgsArr.length) { this.$emit("preloaded"); } Copy code
Calculate the number of columns

calcuCols() { // You need to calculate how many columns of data to render let waterfallWidth = this.width ? this.width : window.innerWidth; //Render at least one column let cols = Math.max(parseInt(waterfallWidth / this.colWidth),1); //Max cannot exceed maxCols column return this.isMobile ? 2 : Math.min(cols,this.maxCols; } Copy code
Use on/on/on/emit to listen and load

//After loading, the page starts rendering imgsArr_c is the real rendering array this.$emit("preloaded"); this.$on("preloaded", () => { this.imgsArr_c = this.imgsArr.concat([]); // The preload is complete and the rendering starts // ... }); Copy code
Use $nextTick to find update opportunities

When a property in the data changes, this value is not immediately rendered to the page, but first placed on the watcher queue (asynchronous). Tasks on the watcher queue will be executed only when the current task is idle. As a result, there will be a certain delay in mounting the changed data to the dom, which leads to the fact that when we change the attribute value and immediately get the changed value through the dom, we find that the obtained value is not the changed value, but the previous value.
The data above corresponds to our imgsArr_c.
this.$nextTick is used to execute delayed callback after the end of the next DOM update cycle. Use this method immediately after modifying the data to get the updated dom.
this.$nextTick(() => { //Indicates the end of loading this.isPreloading = false; this.waterfall(); }); Copy code
Arrange (core) using the waterfall method

waterfall() { //Select all pictures this.imgBoxEls = this.$el.getElementsByClassName("img-box"); //If there is none, nothing can be arranged, so it returns directly if (!this.imgBoxEls) return; //Declare top, left, height and colwidth, that is, the width of the column let top, left, height, colWidth = this.isMobile ? this.imgBoxEls[0].offsetWidth : this.colWidth; //The coordinate size of the starting arrangement. If the arrangement starts from 0, set colsHeightArr to null. colsHeightArr is used to compare which of the currently arranged pictures is the smallest if (this.beginIndex == 0) this.colsHeightArr = []; //Arrange from 0 for (let i = this.beginIndex; i < this.imgsArr.length; i++) { if (!this.imgBoxEls[i]) return; //Gets the height of the rendered element height = this.imgBoxEls[i].offsetHeight; if (i < this.cols) { //If it is less than the number of columns, push all the elements in the first row into the array, set top to 0, and left to multiply the column coordinates by the column width this.colsHeightArr.push(height); top = 0; left = i * colWidth; } else { //When the first row is arranged, calculate the current minimum height let minHeight = Math.min.apply(null, this.colsHeightArr); // Minimum height //When the first row is arranged, calculate the current minimum index let minIndex = this.colsHeightArr.indexOf(minHeight); // Minimum height cable //The top value of the new element is the smallest value in the array top = minHeight; //The value on the left is the minimum index multiplied by the column width left = minIndex * colWidth; // Sets the location where the element is positioned // Update colsHeightArr this.colsHeightArr[minIndex] = minHeight + height; } //Set the left and top values of a single element this.imgBoxEls[i].style.left = left + "px"; this.imgBoxEls[i].style.top = top + "px"; } this.beginIndex = this.imgsArr.length; // After the arrangement, the new pictures are preloaded and arranged from this index } Copy code
Add responsive

window.addEventListener("resize", this.response); response: function () { let old = this.cols; //Recalculate the number of columns this.cols = this.calcuCols(); //If the number of columns does not change, you do not need to rearrange if (old === this.cols) return; // Exit directly when the number of columns remains unchanged this.beginIndex = 0; // The index of the element to begin the arrangement this.waterfall(); } Copy code
Add scroll bottom

this.scroll(); scroll() { this.$refs.scrollEl.addEventListener("scroll", this.scrollFn); } scrollFn() { let scrollEl = this.$refs.scrollEl; //If preloading if (this.isPreloading) return; let minHeight = Math.min.apply(null, this.colsHeightArr); if ( scrollEl.scrollTop + scrollEl.offsetHeight > minHeight - this.reachBottomDistance ) { this.isPreloading = true; this.$emit("scrollReachBottom"); } } Copy code
More details
For more details, the source code is here github Welcome to star!
Publish to npm for everyone to use
npm install @parrotjs/vue-waterfall -S Copy code
You can go to my github README.md for details
