用react實(shí)現(xiàn)一個(gè)簡單的scrollView組件
效果
我們先看一下效果,大概就是希望點(diǎn)擊左邊按鈕 或者右邊按鈕將元素以 每一個(gè) 子選項(xiàng)卡長度單位進(jìn)行精準(zhǔn)偏移。
設(shè)計(jì)考慮的問題
在這之前,大家不妨思考一下這個(gè)需求給到你,你應(yīng)該怎么去設(shè)計(jì)這個(gè)東西?
1 : 父盒子的長度多少 ? 如何控制多出的元素隱藏?
2 :我們應(yīng)該如何進(jìn)行偏移 ? 用定位
還是位移
? 他們有什么區(qū)別?如何保證偏移量一點(diǎn)不差?
3 :每次偏移的量是多少?如何處理邊界情況?
好,帶著以下幾個(gè)問題,我們一起來思考一下這個(gè)組件,應(yīng)該如何封裝。
1: 結(jié)構(gòu)
首先 , 結(jié)構(gòu)如下圖,我們有一個(gè) 父盒子(content-wapper-hidden
) ,還有一個(gè)子盒子內(nèi)嵌在 父盒子中,父盒子負(fù)責(zé)元素的溢出隱藏,固定寬度,子盒子負(fù)責(zé)渲染,和滾動(dòng)。
結(jié)構(gòu)搭建
scrollView.tsx
import { ReactNode, memo, useRef } from 'react' import { ScrollViewWapper } from './style' interface ScrollViewProps { children: ReactNode } const ScrollView = memo(( { children }: ScrollViewProps ) => { const contentRef = useRef<HTMLDivElement>(null) return ( <ScrollViewWapper> <div className='leftIcon'>左邊</div> <div className='content-wapper-hidden'> <div className='render-content' ref={contentRef}> {children} </div> </div> <div className='rightIcon'>右邊</div> </ScrollViewWapper> ) }) export default ScrollView
邏輯處理
1 : 接下來 我們分別給兩個(gè) 按鈕綁定事件 , 并且 在初始化的時(shí)候 去計(jì)算 最大可偏移的值,
import { ReactNode, memo, useEffect, useRef, useState } from 'react' import { ScrollViewWapper } from './style' interface ScrollViewProps { children: ReactNode } const ScrollView = memo(( { children }: ScrollViewProps ) => { const contentRef = useRef<HTMLDivElement>(null) const [maxoffset, setMaxOffset] = useState(0) // 兩者最大的偏移量 可滾動(dòng)距離 const [currentOffsetIndex, setCurrentOffsetIndex] = useState(0) // 當(dāng)前滾動(dòng)元素的索引 const handelIconClick = (isRoght: boolean) => { // 事件處理函數(shù) } function getScrollOffset() { const scrollWidth = contentRef.current!.scrollWidth const clientWidth = contentRef.current!.clientWidth setMaxOffset(scrollWidth - clientWidth) // 計(jì)算最大偏移量 } useEffect(() => { getScrollOffset() }, []) return ( <ScrollViewWapper> <div className='leftIcon' onClick={() => handelIconClick(false)}>左邊</div> <div className='content-wapper-hidden'> <div className='render-content' ref={contentRef}> {children} </div> </div> <div className='rightIcon' onClick={() => handelIconClick(true)}>右邊</div> </ScrollViewWapper> ) }) export default ScrollView
在這里我們通過 ref 綁定了 一個(gè) 內(nèi)容元素 ,然后 初始化的時(shí)候 ,我們?nèi)ビ?jì)算了元素最大可滾動(dòng)的距離, 然后當(dāng)我們點(diǎn)擊了 按鈕時(shí) 我們獲取了 contentRef
下所有 綁定 類名為 item的元素,點(diǎn)擊時(shí)判斷 是否為 右側(cè) 如果是 右側(cè) 點(diǎn)擊 則 索引 + 1 否則 索引 -1 ,然后做邊界處理,依次獲得item的 offsetLeft
,注意 offsetleft 是相對(duì)于 父級(jí)元素的距離,然后 將元素 contentRef 進(jìn)行 translate 位移
import { ReactNode, memo, useEffect, useRef, useState } from 'react' import { ScrollViewWapper } from './style' interface ScrollViewProps { children: ReactNode } const ScrollView = memo(( { children }: ScrollViewProps ) => { const contentRef = useRef<HTMLDivElement>(null) const [maxoffset, setMaxOffset] = useState(0) // 兩者最大的偏移量 可滾動(dòng)距離 const [currentOffsetIndex, setCurrentOffsetIndex] = useState(0) const [isContinueScroll, setisContinueScroll] = useState(true) const handelIconClick = (isRight: boolean) => { const newIndex = isRight ? currentOffsetIndex + 1 : currentOffsetIndex - 1 if (newIndex < 0 || (!isContinueScroll && isRight)) return // 邊界處理 const TabAllList = getAllElements('item', 'class') // 獲取conntentRef 下面的 所有 item 子節(jié)點(diǎn) const TabItem = TabAllList[newIndex] as HTMLDivElement // 獲取下一個(gè) 準(zhǔn)備滾動(dòng)元素 const TabItemOffsetLeft = TabItem.offsetLeft contentRef.current!.style.transform = `translateX(${-TabItemOffsetLeft}px)` setisContinueScroll(maxoffset > TabItemOffsetLeft) // 是否能繼續(xù)滾動(dòng) 如果 你的 offsetLeft 都 // 比我可滾動(dòng)距離大了 , 則肯定是不能滾動(dòng)的 setCurrentOffsetIndex(newIndex) } const getAllElements = (querySelectorName: string, type: 'id' | 'class' | 'el' = 'class') => { let seletorName = null if (type === 'id') { // 對(duì)選擇器 做不同類型處理 seletorName = `#${querySelectorName.replace(/\^#/, '')}` } else if (type == 'class') { seletorName = `.${querySelectorName.replace(/\^./, '')}` } else { seletorName = `${querySelectorName.replace(/\^(.|#)/, '')}` } return contentRef.current!.querySelectorAll(seletorName) } function getScrollOffset() { const scrollWidth = contentRef.current!.scrollWidth const clientWidth = contentRef.current!.clientWidth setMaxOffset(scrollWidth - clientWidth) } useEffect(() => { getScrollOffset() }, []) return ( <ScrollViewWapper> <div className='leftIcon' onClick={() => handelIconClick(false)}>左邊</div> <div className='content-wapper-hidden'> <div className='render-content' ref={contentRef}> {children} </div> </div> <div className='rightIcon' onClick={() => handelIconClick(true)}>右邊</div> </ScrollViewWapper> ) }) export default ScrollView
外部使用ScrollView 組件
import classNames from 'classnames'; import { memo, useState } from 'react'; import ScrollView from './components'; const Login = memo(() => { const [list, setList] = useState(['YYDS', '易烊千璽', '李易峰', '雞哥', '古巨基', '羅志祥', '肖站', '彭于晏']) const [currentIndex, setCurrentIndex] = useState(0) return ( <div id="danmu-container"> <ScrollView> { list.map((item, index) => { return ( <div className='item' key={item} onClick={() => setCurrentIndex(index)}> <div className={classNames('tab-item', currentIndex === index ? 'active' : '')}>{item}</div> </div> ) }) } </ScrollView> </div> ) }) export default Login
ok 到這里 scrollView 組件就簡單的封裝完成了 , 當(dāng)然你還可以集成 點(diǎn)擊某個(gè)選項(xiàng)時(shí) 再進(jìn)行偏移也是可以的,額可以拓展一下。
問題
為什么 用 tranform 而不是定位 ?
答案很簡單 : 定位移動(dòng)的回引發(fā)視圖重繪,而transform 不會(huì)觸發(fā)重回,出于這一點(diǎn)可以在性能上做優(yōu)化,當(dāng)然 掘友在上一張 彈幕的文章中也講到 這一點(diǎn),謝謝大家
總結(jié)
總結(jié)下來,這個(gè)組件 我們主要做的就是它的transform ,當(dāng)然還有更多的功能大家可以拓展一下,如果你覺得還有哪些可以補(bǔ)充的,歡迎評(píng)論區(qū)留言。
以上就是用react實(shí)現(xiàn)一個(gè)簡單的scrollView組件的詳細(xì)內(nèi)容,更多關(guān)于react實(shí)現(xiàn)scrollView組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何不使用eject修改create-react-app的配置
許多剛開始接觸create-react-app框架的同學(xué),不免都會(huì)有個(gè)疑問:如何在不執(zhí)行eject操作的同時(shí),修改create-react-app的配置。2021-04-04concent漸進(jìn)式重構(gòu)react應(yīng)用使用詳解
這篇文章主要為大家介紹了concent漸進(jìn)式重構(gòu)react應(yīng)用的使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11react hooks實(shí)現(xiàn)防抖節(jié)流的方法小結(jié)
這篇文章主要介紹了react hooks實(shí)現(xiàn)防抖節(jié)流的幾種方法,文中通過代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-04-04淺析react里面如何封裝一個(gè)通用的Ellipsis組件
這篇文章主要為大家詳細(xì)介紹了在react里面如何封裝一個(gè)通用的Ellipsis組件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-12-12React使用xlsx和js-export-excel實(shí)現(xiàn)前端導(dǎo)出
這篇文章主要為大家詳細(xì)介紹了React如何分別使用xlsx和js-export-excel實(shí)現(xiàn)前端導(dǎo)出功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2024-02-02react 報(bào)錯(cuò)Module build failed: Browserslis
這篇文章主要介紹了react 報(bào)錯(cuò)Module build failed: BrowserslistError: Unknown browser query `dead`問題的解決方法,需要的朋友可以參考下2023-06-06