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

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

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

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

功能實現(xiàn)
監(jiān)聽元素尺寸變化
工欲善其事必先利其器
在分析完后,我們發(fā)現(xiàn)有一個點。如果要支持react-grid-layout這類的伸縮功能,需要能夠監(jiān)聽到元素的動態(tài)變化。
那么,我們可以先將這部分邏輯抽離,封裝成一個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來監(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類來監(jiān)聽一個元素的尺寸變化。然后返回變化后的width
組件開發(fā)。
有了上面的分析,再實現(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);
// 處理容器寬度變化時,內(nèi)部元素的吸附效果
useEffect(() => {
if (
containerWidth > cache.current && // 當(dāng)容器可拖拽時,表示用戶正在向右拖拽
translateX < 0 && // 表示左側(cè)有內(nèi)容被隱藏
listWidth - Math.abs(translateX) - containerWidth <= 0 //表示右側(cè)已經(jīng)沒有被隱藏的內(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è)置了兩個屬性width和children
- width: 如果不傳,默認(rèn)
inherit,表示該容器是可伸縮的,那么組件內(nèi)部會自己計算;如果傳了固定寬度,則按照該固定寬度來設(shè)置 - children: 需要滾動的內(nèi)容
其中,主要是有5個點需要著重理解
- 在
useLayoutEffect中,需要通過傳入的children來判斷是否需要更新list的長度,防止計算不準(zhǔn)確 - 判斷按鈕何時顯示
- 按鈕點擊時需要處理的UI邏輯
- 如果容器是可伸縮的,需要通過
useRef的緩存來判斷用戶向哪個方向伸縮容器 - 使用
transform和transition讓動畫更流暢自然
對應(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));
}使用方式
我們可以通過下面的方式來使用
const list = [
{ key: "1", name: "列表項1" },
{ key: "2", name: "列表項2" },
{ key: "3", name: "列表項3" },
{ key: "4", name: "列表項4" },
{ key: "5", name: "列表項5" },
{ key: "6", name: "列表項6" },
{ key: "7", name: "列表項7" },
{ key: "8", name: "列表項8" },
{ key: "9", name: "列表項9" },
{ key: "10", name: "列表項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實現(xiàn)內(nèi)容滑動組件效果的文章就介紹到這了,更多相關(guān)React滑動組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?正確使用useCallback?useMemo的方式
這篇文章主要介紹了React?正確使用useCallback?useMemo的方式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-08-08
詳解在React.js中使用PureComponent的重要性和使用方式
這篇文章主要介紹了詳解在React.js中使用PureComponent的重要性和使用方式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07
關(guān)于React中setState同步或異步問題的理解
相信很多小伙伴們都一直在疑惑,setState 到底是同步還是異步。本文就詳細(xì)的介紹一下React中setState同步或異步問題,感興趣的可以了解一下2021-11-11
React+TypeScript項目中使用CodeMirror的步驟
CodeMirror被廣泛應(yīng)用于許多Web應(yīng)用程序和開發(fā)工具,之前做需求用到過codeMirror這個工具,覺得還不錯,功能很強(qiáng)大,所以記錄一下改工具的基礎(chǔ)用法,對React+TypeScript項目中使用CodeMirror的步驟感興趣的朋友跟隨小編一起看看吧2023-07-07
ReactNative支付密碼輸入框?qū)崿F(xiàn)詳解
這篇文章主要為大家介紹了ReactNative支付密碼輸入框?qū)崿F(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
如何將你的AngularJS1.x應(yīng)用遷移至React的方法
本篇文章主要介紹了如何將你的AngularJS1.x應(yīng)用遷移至React的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02

