Taro商城实现瀑布流布局实践

·  阅读 2182
Taro商城实现瀑布流布局实践

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

image.png

一、什么是瀑布流

案例

下两张图分别是京东首页、淘宝首页

image.png image.png

可以看到都使用了瀑布流式布局,图片等宽不等高

定义

瀑布流又称瀑布流式布局,是一种比较流行的页面布局方式,专业的英文名称为[Masonry Layouts]。与传统的分页显示不同,视觉表现为参差不齐的多栏布局

特点

技术角度

  • 多栏或者说多列(为了保持统一,下面都说
  • 图片等宽不等高,根据图片原比例缩放直至宽度达到固定的要求
  • 图片依次放入高度最小的栏,比如,分为两栏,刚开始,两栏的高度都是0,第一张图片进来,高度400,放在第一栏,第二张图片(高度300),放在第二栏,第三张图片(高度50),还是放在第二栏

体验角度

  • 节省空间,外表美观,更有艺术性
  • 用户浏览时的观赏和思维不容易被打断,留存更容易

适用场景

  • 内容以图片为主的时候,瀑布流是更好的选择。图片占用空间比较大,并且大脑理解的速度相比理解文字要快,短时间内可以扫过的内容很多,所以如果用分页显示的话用户务必会频繁的翻页,影响沉浸式的体验,而瀑布流可以解决这个问题
  • 用户目的性不强的时候,瀑布流是更好的选择。如果用户有特定需要查找的信息,分页查找定位更方便,而当目的性较弱的时候,瀑布流可以增加用户停留的时间和意想不到的收获

二、常用解决方案

主要总结在技术选型时,看到的各个方案的缺陷

1、多栏布局

multi-column实现瀑布流主要依赖以下几个属性:

  • column-count: 设置共有几列
  • column-width: 设置每列宽度,列数由总宽度与每列宽度计算得出
  • column-gap: 设置列与列之间的间距
  • break-inside: avoid; 设置元素不能中断
.container{
    column-count: 3;
    column-gap: 10px;

    &-item {
        break-inside: avoid;
    }
}
复制代码

优点是纯css,不需要知道图片的高度,但是有一个致命的缺陷,multi-column布局中子元素的排列顺序是先从上往下再从左至右,如下图

所以如果你的数据是需要先从左到右再从上到下展示,或者说有分页,那是不满足的

image.png

2、grid布局

缺点:需要知道图片的高度,通过图片的高度动态修改样式属性

具体实现可见博客

3、flex布局

一种是直接用flex布局,从上往下再从左至右,这种需要提前给容器设置一个高度,而这个高度是多少,很难计算

<View className="container">
    {list.map(item => (<View className="container-item" key={item.id}></View>))}
</View>
复制代码

还有一种是,容器里面分列,比如分为两列,这种的话与grid布局一致,需要需要知道图片的高度,通过图片的高度动态修改样式属性

<View className="container">
    <!-- 第一列 -->
    <div className="container-column">
        <div className="container-column__item"></div>
        <!-- more items-->
    </div>
    <!-- 第二列 -->
    <div className="container-column">
        <div className="container-column__item"></div>
        <!-- more items-->
    </div>
    <!-- 第三列 -->
    <div className="container-column">
        <div className="container-column__item"></div>
        <!-- more items-->
    </div>
</View>
复制代码

4、position:absolute

绝对定位,把子元素全部设置成绝对定位,监听图片加载,如果加载完就把子元素设置其对应的位置,逐个塞到父容器中,需要需要知道图片的高度

综上,因为场景是有分页,且顺序需要按照服务端返回的顺序展示,所以多栏布局满足不了,需要js计算,考虑到flex布局相对于grid布局,浏览器支持度、以及属性更简单,小程序商城就两列,相较positionflex布局计算更友好

三、Taro多端商城瀑布流实践

服务端返回的数据里面没有图片的宽高,首先想办法拿到图片的宽高,然后分为两列形成瀑布流

1、拿图片的高度

微信小程序图片Image上有一个bindload事件,如下图 image.png

对应到Taro的Image上是onLoad事件,如下图 image.png

需要注意的是,这个事件是在图片载入完毕的时候得到参数的,因此当我们从服务端拿到列表数据之后,这个列表并不是我们真正想呈现的效果,因此节点需要设置display: none

先解释下,存在分页,showList是当前页的数据,比如10条

在事件的e.detail上可以拿到图片的宽width、高height;然后根据定宽,获得图片的缩放比例,算出展示的高度imgHeight

const onImageLoad = (index) => {
    return (e) => {
        console.log(index)
        // 获取图片宽高
        const oImgW = e.detail.width;         //图片原始宽度
        const oImgH = e.detail.height;        //图片原始高度
        const imgWidth = 340;  //图片设置的宽度
        const scale = imgWidth / oImgW;        //比例计算
        const imgHeight = Math.round(oImgH * scale);      //自适应高度
        ...
    };
};

<View className="buyer-show__container-v">
    {
        showList.map((item, index) => {
            return <Image
                key={ showAllList.length + index }
                className="buyer-show__item-img"
                src={ item.picUrl }
                onLoad={ onImageLoad(showAllList.length - showList.length + index) }
            />;
        }
        )
    }
</View>
复制代码
.buyer-show__container-v{
    display: none
}
复制代码

2、图片调整排序

上面onImageLoad执行顺序需要注意下,我们想要的结果是按照数组的顺序执行,但真正在渲染的时候,规律是小图片会先执行、先载入完成,不会按照顺序

const onImageLoad = (index) => {
    return (e) => {
        console.log(index)
        // 获取图片宽高
        const oImgW = e.detail.width;         //图片原始宽度
        const oImgH = e.detail.height;        //图片原始高度
        const imgWidth = 340;  //图片设置的宽度
        const scale = imgWidth / oImgW;        //比例计算
        const imgHeight = Math.round(oImgH * scale);      //自适应高度
        const newShowLoadList = [...showLoadList, {
            id: Number(index),
            height: imgHeight,
        }];
        setShowLoadList(newShowLoadList);
        // 载入全部的图片进入showLoadList数组,若数量和showList中相同,进入图片排序函数
        if (newShowLoadList.length === showList.length) {
            handleImageLoad(newShowLoadList);
        }
    };
};
复制代码

如上代码,输出console.log(index),可以看到顺序如下 image.png

所以需要引入变量showLoadList,表示当前页(比如10条)已经载入完的数据数组,并且加上id属性代表索引,并将获得height后保存的数据放到showLoadList

当这页全部载入完,执行如下handleImageLoad,利用sort根据id属性排序

const handleImageLoad = (newShowLoadList) => {
    // 调整顺序
    const imageLoadList = newShowLoadList.sort((a, b) => a.id - b.id);
    ...
}
复制代码

3、形成瀑布流数据

编译数据,将height与初始数据合并到imageLoadList

定义左右已渲染数据高度变量,leftHeightrightHeight,将数据分为左右两个数组leftShowListrightShowList

遍历imageLoadList根据leftHeightrightHeight高度依次放入高度矮的那边

const [rightShowList, setRightShowList] = useState<showItemType[]>(rightList);
const [leftShowList, setLeftShowList] = useState<showItemType[]>(leftList);
const [leftHeight, setLeftHeight] = useState<number>(0);
const [rightHeight, setRightHeight] = useState<number>(0);

const handleImageLoad = (newShowLoadList) => {
    // 调整顺序
    const imageLoadList = newShowLoadList.sort((a, b) => a.id - b.id);

    for (let i = 0; i < showList.length; i++) {
        // 把原数组中的属性赋予imageLoadList数组
        imageLoadList[i] = {
            ...imageLoadList[i],
            ...showList[i],
            imgStyle: {
                height: pxTransform(imageLoadList[i].height)
            }
        };
    }

    // 对现在的列表进行操作
    let leftHeightCur = leftHeight;  // 左边列表的高度
    let rightHeightCur = rightHeight;
    const left: showItemType[] = leftShowList;  // 左边列表的数组
    const right: showItemType[] = rightShowList;
    // 遍历数组
    for (let i = 0; i < imageLoadList.length; i++) {
        if (leftHeightCur <= rightHeightCur) {
            left.push(imageLoadList[i]);
            leftHeightCur = leftHeightCur + imageLoadList[i].height + 322;
        } else {
            right.push(imageLoadList[i]);
            rightHeightCur = rightHeightCur + imageLoadList[i].height + 322;
        }
    }
    setRightShowList(right);
    setLeftShowList(left);
    setRightHeight(rightHeightCur);
    setLeftHeight(leftHeightCur);
    saveShowLoadList(left, right);
    setShowLoadList([]);
};
复制代码

4、render及页面分页

利用flex布局,左右分为两列

说下分页,因为有分页,数据会比比较多,因此每次在页面不显示的buyer-show__container-v节点,每次都渲染当前页即可,也就是showListshowAllList是整体数据 而想要所有数据的索引值是对的,可以将传入onImageLoad的ID设置为showAllList.length - showList.length + index

<View className="buyer-show">
    <View className="buyer-show__container-v">
    {
        showList.map((item, index) => (
            <Image
                key={ showAllList.length - showList.length + index }
                className="buyer-show__item-img"
                src={ item.picUrl }
                onLoad={ onImageLoad(showAllList.length - showList.length + index) }
            />
        ))
    }
    </View>

    <View className="buyer-show__container">
        <View className="buyer-show__container-left">
            {
                leftShowList.map(item => (
                    <View key={ item.id }>
                        <CartItem item={ item } />
                    </View>

                ))
            }
        </View>

        <View className="buyer-show__container-right">
            {
            rightShowList.map(item => (
                <View key={ item.id }>
                    <CartItem item={ item } />
                </View>
            ))
            }
        </View>
    </View>
</View>
复制代码
分类:
前端
收藏成功!
已添加到「」, 点击更改