一文教你实现小红书响应式瀑布流
点击小卡片,参与粉丝专属福利!!前言瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有完整代码。
正文还是先来看看效果
原理:
?对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的height通过计算来确定item的transform值
?接下来从易到难来解析一下实现
初始化数据列表怎么可以没有数据,先来初始化一下数据
确定列数及列大小由于是响应式,我们要去监听列表容器的大小变化并记录容器宽度,这样才能做出相应的处理
code.png根据监听得到的容器大小信息,我们可以确定每行个数和每一个item的宽度
确定列表中item位置确定item的位置,那么我们只需要确定transform值就可以了,这也是整个实现的核心。我们还需要解决几个问题
对还不知道item的高度,怎么确定我们希望把新的item放置在最低高度的旧item下方,这样全部渲染完每一列的高度才不会相差很多。code.pngitem放置的原理图,放置在当前最低高度的下面
更新item高度当我们第一次运行的时候,每一个item的高度一定都是随机生成的,现在我们要确定item的实际高度。在这里我们还可优化一下,使用懒加载和底部加载,提升性能。这两个在这里就不讲了,不懂的可以去搜一下。
下面代码一共两个作用
记录容器滚动值,传递给每一个item,用于判断是否加载图片。判断是否请求添加数据根据滚动值判断是否加载图片,加载图片后触发父亲更新高度函数
父亲接受到新的高度并更新高度,然后去重新计算transform值和item高度
完整代码divstyle={{
display:'flex'
}}
div左侧导航栏/div
WaterfallFlow/
/div
//瀑布流
importReact,{useState,useEffect,useRef,useMemo,useCallback,Fragment}from'react'
importimg1from'../assets/imgs/1.jpg'
importimg2from'../assets/imgs/2.jpg'
importimg12from'../assets/imgs/12.png'
import'./WaterfallFlow.scss'
interfaceWaterfallFlowItemProps{
/**显示边界*/
showBorder:number
src:string,
title:string,
style:React.CSSProperties,
unitWidth:number,
index:number,
sizeChange?:(height:number,index:number)=void
}
constWaterfallFlowItem=(props:WaterfallFlowItemProps)={
let{src,title,style={},sizeChange=()={},unitWidth,index,showBorder}=props
letframeDom=useRef(null)
let[isLoading,setIsLoading]=useState(false)
let[imgInfo,setImgInfo]=useState{
height:number,
width:number
}({
height:1,
width:1
})
letimgDom=useRefany(null)
/**离父亲上边框的距离*/
lettop=useMemo(()={
lety=style.transform?Number(style.transform?.substring(style.transform.indexOf(',',0)+1,style.transform.length-3)):undefined
returny
},[style])
/**是否加载图片*/
letisImgShow=useMemo(()={
if(top===undefined){
returnfalse
}
if(top=showBorder){
returntrue
}else{
returnfalse
}
},[top,showBorder])
/**符合条件懒加载图片*/
useEffect(()={
if(imgDom.current===null||src===''||isImgShow===false){
return
}
letimg=newImage();
img.src=
img.onload=()={
setImgInfo({
height:img.height,
width:img.width,
})
setIsLoading(true)
}
imgDom.current.src=src
},[src,isImgShow])
useEffect(()={
//通过宽度比例获取图片高度
letheight=imgInfo.height*(unitWidth/imgInfo.width);
if(isLoading){
//加40是因为下方文字部分高度为40,可以自己设置
sizeChange(height+40,index)
}
},[imgInfo,index,unitWidth,isLoading,sizeChange])
return(
divclassName='WaterfallItem'style={{
...style,
}}ref={frameDom}
divclassName='WaterfallItem__img'
{
imgref={imgDom}style={{
visibility:isLoading?'visible':'hidden'
}}/
}
/div
divclassName='WaterfallItem__name'
{titletitle}
{
!isLoading&&
divclassName='WaterfallItem__name--placeholder'/div
}
/div
/div
)
}
exportdefaultfunctionWaterfallFlow(){
/**滚动的父元素*/
constscrollParent=useRef(null)
/**向上滚动的距离*/
const[scrollTop,setScrollTop]=useState(0)
/**数据列表*/
const[list,setList]=useStateWaterfallFlowItemProps[]
constwaterfallFlowDom=useRef(null)
/**样式列表*/
const[styleList,setStyleList]=useStateReact.CSSProperties[]([])
/**自定义骨架屏高度*/
letheightList=[170,230,300];
/**到达底部*/
letisLoadingData=useRef(false);
/**生成随机数*/
constcreateRandomNum=useCallback((min:number,max:number):number={
returnMath.floor(Math.random()*(max-min+1))+
},[])
letwaterfallFlowListInfo=useRef{
left:number,
top:number,
height:number,
}[]([])
/**当前容器信息*/
let[frameInfo,setFrameInfoInfo]=useState{
width:number,
}({width:0})
/**每行个数*/
letrowsNum=useMemo(()={
letwidth=frameInfo.width||0;
if(width=1200){
return6
}elseif(width=768width=1199){
return4
}else{
return2
}
},[frameInfo])
/**每一个的宽度*/
letunitWidth=useMemo(()={
return(frameInfo.width-(rowsNum-1)*10)/rowsNum;
},[rowsNum,frameInfo])
/**获取位置*/
constgetStyleList=useCallback(()={
lettemporaryStyleList:React.CSSProperties[]=styleList;
/**目前最下一行的index*/
letbottomItemIndex=
for(leti=0;ilist.length;i++){
//原本应对应的行数
letcurrentRow=Math.floor(i/rowsNum);
//
letremainder=i%rowsNum+1;
//最低item下标
letminHeightInd=0;
//最低高度
letminHeight=9999999999;
//寻找最低高度的下标
if(currentRow===0){
bottomItemIndex[i]=
}else{
for(letj=0;jbottomItemIndex.length;j++){
if(waterfallFlowListInfo.current[bottomItemIndex[j]].top+waterfallFlowListInfo.current[bottomItemIndex[j]].heightminHeight){
minHeightInd=
minHeight=waterfallFlowListInfo.current[bottomItemIndex[j]].top+waterfallFlowListInfo.current[bottomItemIndex[j]].height
}
}
bottomItemIndex[minHeightInd]=
}
if(waterfallFlowListInfo.current[i]===undefined){
waterfallFlowListInfo.current[i]={}as
}
//第一行特殊处理,一定是从左到右铺的
if(currentRow===0){
if(remainder===1){
waterfallFlowListInfo.current[i].left=0;
}else{
waterfallFlowListInfo.current[i].left=
waterfallFlowListInfo.current[i-1].left+unitWidth+10;
}
waterfallFlowListInfo.current[i].top=0;
}
//剩下的行数,铺在当前最低高度下面
else{
waterfallFlowListInfo.current[i].left=waterfallFlowListInfo.current[minHeightInd].left
waterfallFlowListInfo.current[i].top=minHeight+25;
}
//是否已经有高度,有高度使用已有高度,否则随机生成
waterfallFlowListInfo.current[i].height=waterfallFlowListInfo.current[i].height||heightList[createRandomNum(0,2)];
temporaryStyleList[i]={
transform:`translate(${waterfallFlowListInfo.current[i].left}px,${waterfallFlowListInfo.current[i].top}px)`,
width:`${unitWidth}px`,
height:waterfallFlowListInfo.current[i].height
}
}
return[...temporaryStyleList]
},[unitWidth,rowsNum,list])
/**图片加载完更新高度*/
constonSizeChange=useCallback((height:number,index:number)={
if(waterfallFlowListInfo.current[index]===undefined){
waterfallFlowListInfo.current[index]={}as
}
waterfallFlowListInfo.current[index].height=height;
setStyleList(getStyleList())
},[getStyleList])
/**大小、数量发生变化时触发*/
useEffect(()={
setStyleList(getStyleList())
},[unitWidth,rowsNum,list])
/**初始化请求数据*/
useEffect(()={
isLoadingData.current=true;
letdata:any=[]
//为了出现骨架屏
for(leti=0;i50;i++){
letitem;
item={
src:"",
title:""
}
data.push(item);
}
setList(data)
data=
for(leti=0;i50;i++){
letitem;
if(i%3==0){
item={
src:img1,
title:`第${i}个Item`
}
}elseif(i%3==1){
item={
src:img2,
title:`第${i}个Item`
}
}else{
item={
src:img12,
title:`第${i}个Item`
}
}
data.push(item);
}
//模拟请求
setTimeout(()={
setList(data)
isLoadingData.current=false;
},1200)
},[])
constonResize=useCallback(()={
if(waterfallFlowDom.current===null){
return
}
setFrameInfoInfo({
width:(waterfallFlowDom.currentasHTMLDivElement).getBoundingClientRect().width
})
},[])
/**监听列表容器大小变化*/
useEffect(()={
if(waterfallFlowDom.current===null){
return
}
constresizeObserver=newResizeObserver(entries={
onResize()
resizeObserver.observe(waterfallFlowDom.current);
return()={
resizeObserver.disconnect()
}
},[])
constonScroll=useCallback(()={
//记录滚动值
setScrollTop((scrollParent.currentasany).scrollTop)
lettop=(scrollParent.currentasany).scrollTop
letclientHeight=(scrollParent.currentasany).clientHeight
letscrollHeight=(scrollParent.currentasany).scrollHeight
//做底部加载
if(scrollHeight-clientHeight/3=top+clientHeightisLoadingData.current===false){
isLoadingData.current=true;
letdata:any=[]
for(leti=0;i50;i++){
letitem;
if(i%3==0){
item={
src:img1,
title:`第${i}个Item`
}
}elseif(i%3==1){
item={
src:img2,
title:`第${i}个Item`
}
}else{
item={
src:img12,
title:`第${i}个Item`
}
}
data.push(item);
}
//请求数据并加载
setTimeout(()={
isLoadingData.current=false
setList((lastData)={
return[...lastData,...data]
})
},1200)
}
},[])
/**监听滚动*/
useEffect(()={
(scrollParent.currentasany).addEventListener('scroll',onScroll);
return()={
(scrollParent.currentasany).removeEventListener('scroll',onScroll);
}
},[])
return(
divclassName='waterfallFlow'ref={scrollParent}
divclassName='waterfallFlow__title'响应式瀑布流/div
sectionref={waterfallFlowDom}className='waterfallFlow__content'
{
list.map((item,ind)={
return(
divkey={ind}
WaterfallFlowItem
showBorder={scrollTop+(scrollParent.currentasany).clientHeight}
src={item.src}
title={item.title}
style={styleList[ind]}
sizeChange={onSizeChange}
unitWidth={unitWidth}
index={ind}
/
/div
)
})
}
/section
/div
)
}
.waterfallFlow{
flex:1;
height:100vh;
overflow-y:auto;
&__title{
text-align:center;
}
&__content{
position:relative;
box-sizing:border-box;
background-color:
}
}
.WaterfallItem{
display:flex;
flex-direction:column;
position:absolute;
top:0px;
left:0px;
border-radius:20px;
background-color:#FFF;
box-sizing:border-box;
box-shadow:0px0px12px2pxrgba(0,0,0,0.1);
&__img{
flex:1;
overflow:hidden;
border-radius:20px;
background:#f7f7f7;
}
img{
display:inline-block;
height:100%;
width:100%;
object-fit:cover;
border-radius:20px;
}
&__name{
height:30px;
margin-top:10px;
padding:"0px5px";
box-sizing:border-box;
&--placeholder{
height:20px;
background:#fbfbfb
}
}
}
结语感兴趣的可以去试试
如果文章对你有帮助的话欢迎「关注+点赞+收藏」
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线