使用React實(shí)現(xiàn)內(nèi)容滑動(dòng)組件效果
最近在做項(xiàng)目時(shí)遇到一個(gè)需求,需要讓一個(gè)列表能夠通過點(diǎn)擊按鈕進(jìn)行滾動(dòng),每次都是一屏的距離,不足則結(jié)束。并且,這個(gè)列表項(xiàng)是在react-grid-layout中的某一個(gè)模塊內(nèi)。所以包裹這個(gè)列表的容器會(huì)隨時(shí)發(fā)生變化。在完成這個(gè)組件后,通過這篇文章總結(jié)一下。
UI/原型分析
那么從上面的功能描述以及項(xiàng)目中的UI,我們可以分析得到這樣一個(gè)假想圖:
- 我們需要實(shí)現(xiàn)一個(gè)容器來作為我們的可視區(qū)域,并且這個(gè)容器是可以伸縮的。
- 列表內(nèi)容如果超出容器的可視區(qū)域,那么就會(huì)被隱藏。
- 需要左右都有按鈕,來支持用戶左右滑動(dòng)內(nè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。
話不多說,我們先上簡單的最終效果圖
有固定寬度,不可伸縮
無固定寬度,可伸縮(這里直接用幣安的效果來展示,如果你有興趣,可以自己下載個(gè)
react-grid-layout
或者其他庫來試一下效果)
功能實(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來監(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)聽一個(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)沒有被隱藏的內(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ì)算;如果傳了固定寬度,則按照該固定寬度來設(shè)置 - children: 需要滾動(dòng)的內(nèi)容
其中,主要是有5個(gè)點(diǎn)需要著重理解
- 在
useLayoutEffect
中,需要通過傳入的children
來判斷是否需要更新list
的長度,防止計(jì)算不準(zhǔn)確 - 判斷按鈕何時(shí)顯示
- 按鈕點(diǎn)擊時(shí)需要處理的UI邏輯
- 如果容器是可伸縮的,需要通過
useRef
的緩存來判斷用戶向哪個(gè)方向伸縮容器 - 使用
transform
和transition
讓動(dòng)畫更流暢自然
對應(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: "列表項(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?正確使用useCallback?useMemo的方式
這篇文章主要介紹了React?正確使用useCallback?useMemo的方式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08詳解在React.js中使用PureComponent的重要性和使用方式
這篇文章主要介紹了詳解在React.js中使用PureComponent的重要性和使用方式,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07關(guān)于React中setState同步或異步問題的理解
相信很多小伙伴們都一直在疑惑,setState 到底是同步還是異步。本文就詳細(xì)的介紹一下React中setState同步或異步問題,感興趣的可以了解一下2021-11-11React+TypeScript項(xiàng)目中使用CodeMirror的步驟
CodeMirror被廣泛應(yīng)用于許多Web應(yīng)用程序和開發(fā)工具,之前做需求用到過codeMirror這個(gè)工具,覺得還不錯(cuò),功能很強(qiáng)大,所以記錄一下改工具的基礎(chǔ)用法,對React+TypeScript項(xiàng)目中使用CodeMirror的步驟感興趣的朋友跟隨小編一起看看吧2023-07-07ReactNative支付密碼輸入框?qū)崿F(xiàn)詳解
這篇文章主要為大家介紹了ReactNative支付密碼輸入框?qū)崿F(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11如何將你的AngularJS1.x應(yīng)用遷移至React的方法
本篇文章主要介紹了如何將你的AngularJS1.x應(yīng)用遷移至React的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02