使用React實(shí)現(xiàn)內(nèi)容滑動(dòng)組件效果

最近在做項(xiàng)目時(shí)遇到一個(gè)需求,需要讓一個(gè)列表能夠通過(guò)點(diǎn)擊按鈕進(jìn)行滾動(dòng),每次都是一屏的距離,不足則結(jié)束。并且,這個(gè)列表項(xiàng)是在react-grid-layout中的某一個(gè)模塊內(nèi)。所以包裹這個(gè)列表的容器會(huì)隨時(shí)發(fā)生變化。在完成這個(gè)組件后,通過(guò)這篇文章總結(jié)一下。
UI/原型分析
那么從上面的功能描述以及項(xiàng)目中的UI,我們可以分析得到這樣一個(gè)假想圖:

- 我們需要實(shí)現(xiàn)一個(gè)容器來(lái)作為我們的可視區(qū)域,并且這個(gè)容器是可以伸縮的。
- 列表內(nèi)容如果超出容器的可視區(qū)域,那么就會(huì)被隱藏。
- 需要左右都有按鈕,來(lái)支持用戶左右滑動(dòng)內(nèi)容來(lái)查看,每次滑動(dòng)距離為 容器的寬度,也就是
可視區(qū)域 - 當(dāng)在伸縮容器的時(shí)候,如果是向右側(cè)伸縮,并且可視區(qū)域已經(jīng)被拉伸的超出了列表被隱藏的右側(cè)內(nèi)容時(shí),右側(cè)的隱藏內(nèi)容需要有一個(gè)吸附效果,即跟著被伸縮的容器移動(dòng),直到左側(cè)隱藏內(nèi)容的偏移值為0。
話不多說(shuō),我們先上簡(jiǎn)單的最終效果圖
有固定寬度,不可伸縮

無(wú)固定寬度,可伸縮(這里直接用幣安的效果來(lái)展示,如果你有興趣,可以自己下載個(gè)
react-grid-layout或者其他庫(kù)來(lái)試一下效果)

功能實(shí)現(xiàn)
監(jiān)聽元素尺寸變化
工欲善其事必先利其器
在分析完后,我們發(fā)現(xiàn)有一個(gè)點(diǎn)。如果要支持react-grid-layout這類的伸縮功能,需要能夠監(jiān)聽到元素的動(dòng)態(tài)變化。
那么,我們可以先將這部分邏輯抽離,封裝成一個(gè)hook:
hooks/useResizeObserver.ts
import { useLayoutEffect, useState } from "react";
// 接收保存被監(jiān)聽dom的ref
const useResizeObserver = (ref: React.RefObject<HTMLElement>) => {
const [width, setWidth] = useState<number>(0);
useLayoutEffect(() => {
// 使用ResizeObserver來(lái)監(jiān)聽DOM的變化
const resizeObserver = new ResizeObserver(() => {
setWidth((ref.current as HTMLElement).clientWidth);
});
resizeObserver.observe(ref.current as HTMLElement);
return () => {
resizeObserver.disconnect();
};
}, [ref]);
return width;
};
export default useResizeObserver;其中核心的邏輯是使用ResizeObserver類來(lái)監(jiān)聽一個(gè)元素的尺寸變化。然后返回變化后的width
組件開發(fā)。
有了上面的分析,再實(shí)現(xiàn)代碼就是一步步走即可。所以我們直接貼代碼:
components/SliderContainer/index.tsx
import React, {
useMemo,
useState,
useRef,
useLayoutEffect,
useEffect,
} from "react";
import type { ReactElement } from "react";
import "./index.css";
import ArrowLeft from "@/assets/arrow-left.svg";
import ArrowRight from "@/assets/arrow-right.svg";
import useResizeObserver from "@/hooks/useResizeObserver";
export interface SliderContainerProps {
width: number;
children: ReactElement; // 需要包括的內(nèi)容
}
const LEFT = "left";
const RIGHT = "right";
export const SliderContainer: React.FC<SliderContainerProps> = ({
width = "inherit",
children,
}) => {
const listRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const containerWidth = useResizeObserver(containerRef);
const [listWidth, setListWidth] = useState(0);
const [translateX, setTranslateX] = useState(0);
// 緩存
const cache = useRef(containerWidth);
// 處理容器寬度變化時(shí),內(nèi)部元素的吸附效果
useEffect(() => {
if (
containerWidth > cache.current && // 當(dāng)容器可拖拽時(shí),表示用戶正在向右拖拽
translateX < 0 && // 表示左側(cè)有內(nèi)容被隱藏
listWidth - Math.abs(translateX) - containerWidth <= 0 //表示右側(cè)已經(jīng)沒(méi)有被隱藏的內(nèi)容了
) {
const distance = containerWidth - cache.current;
setTranslateX((cur) => cur + distance);
}
// 更新緩存
cache.current = containerWidth;
}, [containerWidth, translateX, listWidth]);
useLayoutEffect(() => {
setListWidth((listRef.current as HTMLDivElement).clientWidth);
}, [children]);
// 判斷按鈕是否可見
const [leftArrowVisible, rightArrowVisible] = useMemo(() => {
let leftArrowVisible,
rightArrowVisible = false;
// listWidth - Math.abs(translateX) - containerWidth 為右側(cè)隱藏內(nèi)容
if (listWidth - Math.abs(translateX) - containerWidth > 0) {
rightArrowVisible = true;
}
if (translateX < 0) {
leftArrowVisible = true;
}
return [leftArrowVisible, rightArrowVisible];
}, [listWidth, translateX, containerWidth]);
const handleArrowClick = (direction: string) => {
if (direction === LEFT) {
// 左側(cè)隱藏內(nèi)容
const leftSpaceWidth = Math.abs(translateX);
if (leftSpaceWidth > containerWidth) {
setTranslateX((cur) => cur + containerWidth);
} else {
setTranslateX((cur) => cur + leftSpaceWidth);
}
}
if (direction === RIGHT) {
// 右側(cè)隱藏內(nèi)容
const rightSpaceWidth = listWidth - Math.abs(translateX) - containerWidth;
if (rightSpaceWidth > containerWidth) {
setTranslateX((cur) => cur - containerWidth);
} else {
setTranslateX((cur) => cur - rightSpaceWidth);
}
}
};
return (
<div ref={containerRef} style={{ width: width }} className="container">
{leftArrowVisible && (
<>
<button
color="white"
className="leftArrow btn"
onClick={() => handleArrowClick(LEFT)}
>
<img src={ArrowLeft} alt="" />
</button>
<div className="linerGrid leftGradient"></div>
</>
)}
<div
ref={listRef}
className="list"
style={{
transform: `translateX(${translateX}px)`,
transition: "all 0.3s linear",
}}
>
{children}
</div>
{rightArrowVisible && (
<>
<div className="linerGrid rightGradient"></div>
<button
color="white"
className="rightArrow btn"
onClick={() => handleArrowClick(RIGHT)}
>
<img src={ArrowRight} alt="" />
</button>
</>
)}
</div>
);
};組件目前設(shè)置了兩個(gè)屬性width和children
- width: 如果不傳,默認(rèn)
inherit,表示該容器是可伸縮的,那么組件內(nèi)部會(huì)自己計(jì)算;如果傳了固定寬度,則按照該固定寬度來(lái)設(shè)置 - children: 需要滾動(dòng)的內(nèi)容
其中,主要是有5個(gè)點(diǎn)需要著重理解
- 在
useLayoutEffect中,需要通過(guò)傳入的children來(lái)判斷是否需要更新list的長(zhǎng)度,防止計(jì)算不準(zhǔn)確 - 判斷按鈕何時(shí)顯示
- 按鈕點(diǎn)擊時(shí)需要處理的UI邏輯
- 如果容器是可伸縮的,需要通過(guò)
useRef的緩存來(lái)判斷用戶向哪個(gè)方向伸縮容器 - 使用
transform和transition讓動(dòng)畫更流暢自然
對(duì)應(yīng)的css樣式為:
components/SliderContainer/index.css
.container {
display: flex;
overflow: hidden;
position: relative;
height: 100%;
}
.list {
display: flex;
align-items: center;
}
.btn {
width: 40px;
height: 100%;
position: absolute;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
}
.leftArrow {
left: 0;
}
.rightArrow {
right: 0;
z-index: 0;
}
.linerGrid {
position: absolute;
width: 20px;
height: 100%;
}
.leftGradient {
left: 40px;
z-index: 1;
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0.2));
}
.rightGradient {
right: 40px;
background: linear-gradient(269.21deg, #ffffff, rgba(255, 255, 255, 0.2));
}使用方式
我們可以通過(guò)下面的方式來(lái)使用
const list = [
{ key: "1", name: "列表項(xiàng)1" },
{ key: "2", name: "列表項(xiàng)2" },
{ key: "3", name: "列表項(xiàng)3" },
{ key: "4", name: "列表項(xiàng)4" },
{ key: "5", name: "列表項(xiàng)5" },
{ key: "6", name: "列表項(xiàng)6" },
{ key: "7", name: "列表項(xiàng)7" },
{ key: "8", name: "列表項(xiàng)8" },
{ key: "9", name: "列表項(xiàng)9" },
{ key: "10", name: "列表項(xiàng)10" },
];
function App() {
return (
<div className="App">
<SliderContainer width={300}>
<>
{list.map((item) => (
<div style={{ width: 100 }} key={item.key}>
{item.name}
</div>
))}
</>
</SliderContainer>
</div>
);
}到此這篇關(guān)于使用React實(shí)現(xiàn)內(nèi)容滑動(dòng)組件效果的文章就介紹到這了,更多相關(guān)React滑動(dòng)組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解析React?ref?命令代替父子組件的數(shù)據(jù)傳遞問(wèn)題
這篇文章主要介紹了React?-?ref?命令為什么代替父子組件的數(shù)據(jù)傳遞,使用?ref?之后,我們不需要再進(jìn)行頻繁的父子傳遞了,子組件也可以有自己的私有狀態(tài)并且不會(huì)影響信息的正常需求,這是為什么呢?因?yàn)槲覀兪褂昧?ref?命令的話,ref是可以進(jìn)行狀態(tài)的傳輸2022-08-08
React報(bào)錯(cuò)之Object?is?possibly?null的問(wèn)題及解決方法
這篇文章主要介紹了React報(bào)錯(cuò)之Object?is?possibly?null的問(wèn)題,造成 "Object is possibly null"的錯(cuò)誤是因?yàn)閡seRef()鉤子可以傳遞一個(gè)初始值作為參數(shù),而我們傳遞null作為初始值,本文給大家分享詳細(xì)解決方法,需要的朋友可以參考下2022-07-07
ReactDOM.render在react源碼中執(zhí)行原理
這篇文章主要為大家介紹了ReactDOM.render在react源碼中執(zhí)行原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
React?Fiber構(gòu)建completeWork源碼解析
這篇文章主要為大家介紹了React?Fiber構(gòu)建completeWork源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
React中的setState使用細(xì)節(jié)和原理解析(最新推薦)
這篇文章主要介紹了React中的setState使用細(xì)節(jié)和原理解析(最新推薦),前面我們有使用過(guò)setState的基本使用, 接下來(lái)我們對(duì)setState使用進(jìn)行詳細(xì)的介紹,需要的朋友可以參考下2022-12-12

