React Native 自定義下拉刷新組件

React Native 自定義下拉刷新組件 PullToRefresh

針對(duì)猴急一些的同學(xué),可以先在這個(gè) Expo網(wǎng)站在線運(yùn)行下demo看看效果 。

完整的代碼,在 Github倉(cāng)庫(kù) 。

下拉刷新,是一個(gè)很常見的交互方式。React-Native(以下簡(jiǎn)稱RN)內(nèi)置的 FlatList 是支持下拉刷新組件的,通過設(shè)置 refreshControl 屬性即可。通常我們不僅僅需要定制下拉組件,還需要在下拉過程中,下拉組件執(zhí)行一些動(dòng)畫,比如在我們場(chǎng)景下,公司logo會(huì)隨著下拉的幅度,不同的筆畫還是顯現(xiàn)出顏色。這就需要我們的下拉組件,知道當(dāng)前下拉的幅度,以此來(lái)計(jì)算我們動(dòng)畫執(zhí)行的進(jìn)度。顯然,RN官方的 refreshControl并不能滿足我們的需求。

看到有兩個(gè)已經(jīng)存在的開源包 react-native-pull-refreshreact-native-ptr-control ,基本都有2年左右歷史了,而且我也確實(shí)沒看懂,為什么要用到 兩個(gè) ScrollView 嵌套來(lái)實(shí)現(xiàn)。

直觀上來(lái)看,我應(yīng)該只需要有一個(gè) ScrollView 就可以了,我監(jiān)聽下拉距離,重新render自定義的下拉組件。嗯,按照這個(gè)思路,嘗試擼一個(gè)試試。

一步步實(shí)現(xiàn)自定義下拉

首先,我們提供的是一個(gè)容器(命名為 PullToRefresh 吧 ),內(nèi)部是用戶通過 children 傳進(jìn)來(lái)的 FlatList,這樣也方便用戶修改,在需要自定義下拉刷新的場(chǎng)景下,用我們這個(gè)容器把已經(jīng)存在的 FlatList 包起來(lái)就可以了,改動(dòng)也挺小。當(dāng)然,因?yàn)槭亲远x下拉刷新header,肯定還需要用戶把自定義的下拉刷新header組件傳進(jìn)來(lái),就命名為 props.HeaderComponent 吧,到這一步,我們?nèi)萜鲀?nèi)render出來(lái)的DOM結(jié)構(gòu),大概是這樣的:

<View>
  <Animated.View><HeaderComponent /></Animated.View>
  <FlatList />
</View>

最外層的 View 的展示區(qū)域,和用戶自己的 FlatList 完全一樣。那么問題來(lái)了,我們的下拉刷新 HeaderComponent 在默認(rèn)情況下,應(yīng)該是不可見的,是在用戶下拉過程中,逐漸的從上到下進(jìn)入容器的可視區(qū)域。那就默認(rèn)把 HeaderComponent 絕對(duì)定位到容器可視區(qū)域的外邊吧,可是往上移動(dòng)多大呢,這就需要用戶告訴我們?nèi)萜饕粋€(gè)下拉組件的高度了,props.headerHeight ,到這一步,容器渲染出來(lái)的樣式大概如下:

<View>
  <Animated.View style={{position: 'absolute', top: - this.props.headerHeight}}>
    <HeaderComponent />
  </Animated.View>
  <FlatList />
</View>

完成了初始的DOM結(jié)構(gòu)樣式,接下來(lái)容器下拉時(shí)機(jī)的問題。

首先,什么時(shí)候用戶下拉,是觸發(fā)我們?nèi)萜鞯南吕僮?,而不是?nèi)部的 FlatList 的默認(rèn)下拉呢?這個(gè)好像比較直接,當(dāng)內(nèi)部的 FlatList 已經(jīng)下拉到頂部,不能再繼續(xù)下拉時(shí),用戶的下拉動(dòng)作,就應(yīng)該觸發(fā)容器的下拉。那么,我們就需要知道內(nèi)部的 FlatList 的當(dāng)前下拉位置,這可以通過 FlatList 的 onScroll 屬性來(lái)獲取當(dāng)前 FlatList 的滾動(dòng)距離。

什么時(shí)機(jī)觸發(fā)容器的下拉確定了,那在容器下拉過程中,我們需要更新哪些組件呢?1) 自定義header組件肯定要更新,將最新的下拉距離傳給header組件。2) 如果只是將header組件往下移動(dòng),我們的 FlatList 不動(dòng),那么自定義header會(huì)遮擋住 FlatList 的內(nèi)容,這不是我們想要的;因此,在容器下拉過程中,內(nèi)部的 FlatList 位置也需要響應(yīng)的往下移動(dòng)。

如果我們用容器的 state.containerTop 這個(gè) Animated.Value 來(lái)保存當(dāng)前容器下拉的距離,那么目前我們?nèi)萜鱮ender的DOM結(jié)果大概如下:

const headerStyle = {
  position: 'absolute',
  left: 0,
  width: '100%',
  top: -this.props.headerHeight,
  transform: [{ translateY: this.state.containerTop }],
};
<View>
  <Animated.View style={[{ flex: 1, transform: [{ translateY: this.state.containerTop }] }]}>
    <FlatList />
  </Animated.View>
  <Animated.View style={headerStyle}>
    <HeaderComponent />
  </Animated.View>
</View>

這樣,基本就完成了容器下拉過程中,自定義header和內(nèi)部的FlatList同步下拉了。

下拉動(dòng)作實(shí)現(xiàn)了,那下拉到什么位置,可以觸發(fā)刷新呢?這就需要用戶再傳遞一個(gè)觸發(fā)刷新的下拉距離,就叫 props.refreshTriggerHeight 吧,當(dāng)用戶松開時(shí),如果當(dāng)前下拉距離 >= props.refreshTriggerHeight ,就會(huì)調(diào)用用戶傳入的刷新函數(shù) props.onRefresh 。通常,用戶如果下拉的距離比較大,松開手指時(shí)觸發(fā)了刷新動(dòng)作,這時(shí)候會(huì)整個(gè)組件會(huì)先回跳到一個(gè)刷新中的位置,這個(gè)位置,用戶可以通過 props.refreshingHoldHeight 來(lái)指定。props.refreshTriggerHeightprops.refreshingHoldHeight 都是可選的,如果用戶不傳,默認(rèn)為 props.headerHeight。

One More Thing

上面其實(shí)還省略了一些工作,最重要的,就是在容器下拉過程中,怎么把下拉距離(下拉進(jìn)度)傳給用戶的自定義 HeaderComponent ?上面容器上的 state.containerTop 其實(shí)就是當(dāng)前容器下拉距離,只不過這是一個(gè) Animated.Value ,我們 不能 讀取到它當(dāng)前的值。因此,我在容器上添加了一個(gè) 實(shí)例屬性 this.containerTranslateY 來(lái)保存當(dāng)前容器下拉的距離,我們會(huì)監(jiān)聽 state.containerTop 值的變化,在回調(diào)函數(shù)里,修改 this.containerTranslateY。

等等?。?code>containerTranslateY為什么沒有放到容器的 state 上呢?不應(yīng)該是 this.state.containerTranslateY 么??嗯,剛開始我確實(shí)是放在 state 上的,然后在用戶下拉容器過程中,通過在容器上 setState,觸發(fā)容器重新render,然后把 containerTranslateY 傳遞過header。但是,這樣通過容器上 setState 觸發(fā)header更新的方式,在我測(cè)試中,發(fā)現(xiàn)頁(yè)面會(huì)比較卡頓。因此,在用戶下拉容器過程中,并沒有去修改容器的 state ,而是通過 方法調(diào)用 的命令方式,將用戶當(dāng)前下拉距離傳給了header組件。這里可能還可以怎么優(yōu)化一下吧。I'm not sure.

因此,用戶的自定義header組件,必須 暴露一個(gè)實(shí)例方法 setProgress 來(lái)接收容器下拉過程中的一些參數(shù),目前這個(gè)方法的簽名是這樣的:

// pullDistance 表示容器下拉的距離;percent 代表下拉的進(jìn)度,[0, 1]
setProgress({pullDistance, percent}){}

完整的 header 組件demo,請(qǐng)參考 expo上的運(yùn)行demo

The End

最后,聽說(shuō),微交互動(dòng)畫,使用 lottie 和 RN 更配哦。

本來(lái)想嘗試用 AE 做一個(gè)公司logo的 lottie 動(dòng)畫的,奈何沒hold住……

完整的代碼在github上:https://github.com/sophister/react-native-pull-to-refresh-custom 。

相關(guān)鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容