本文做者:李磊
Web 應用若是要更新列表數據,通常會選擇點擊左上角刷新按鈕,或使用快捷鍵 Ctrl+F5,進行頁面資源和數據的全量更新。若是頁面提供了刷新按鈕或是翻頁按鈕,也能夠點擊只作數據更新。html
但移動客戶端屏幕寸土寸金,不管是加上一個刷新按鈕,仍是配合愈來愈少的手機按鍵來作刷新操做,都不是十分便捷的方案。前端
因而,在這方寸之間,各類各樣的滑動方案和手勢方案來觸發事件,成了移動客戶端的廣泛趨勢。在刷新數據方面,移動端最經常使用的方案就是下拉刷新的機制。react
下拉刷新的機制最先是由 Loren Brichter 在 Tweetie 2 中實現。Tweetie 是 Twitter 的第三方客戶端,後來被 Twitter 收購,Loren Brichter 也成爲 Twitter 員工(現已離開)。android
Loren Brichter 在 2010 年 4 月 8 日爲下拉刷新申請了專利,並得到受權United States Patent: 8448084。但他很願意看到這個機制被其餘 app 採用,也曾經說過申請是防護性的。git
咱們看下專利保護範圍最大的主權項是:github
簡單來講,下拉加載的機制包含三個狀態:react-native
在那以後,不少以 news feed 爲主的移動客戶端都相繼採用了這個設計。數組
React Native 提供了 RefreshControl 組件,能夠用在 ScrollView 或 FlatList 內部,爲其添加下拉刷新的功能。app
RefreshControl 內部實現是分別封裝了 iOS 環境下的 UIRefreshControl
和安卓環境下的 AndroidSwipeRefreshLayout
,兩個都是移動端的原生組件。ide
因爲適配的原生方案不一樣,RefreshControl 不支持自定義,只支持一些簡單的參數修改,如:刷新指示器顏色、刷新指示器下方字體。而且已有參數還受不一樣平臺的限制。
最多見的需求會要求下拉加載指示器有本身特點的 loading 動畫,個別的需求方還會加上操做的文字說明和上次加載的時間。只支持修改顏色的 RefreshControl 確定是沒法知足的。
那想要自定義下拉刷新要怎麼作呢?
ScrollView 是官方提供的一個封裝了平臺 ScrollView (滾動視圖)的組件,經常使用於顯示滾動區域。同時還集成了觸摸的「手勢響應者」系統。
手勢響應系統用來判斷用戶的一次觸摸操做的真實意圖是什麼。一般用戶的一次觸摸須要通過幾個階段才能判斷。好比開始是點擊,以後變成了滑動。隨着持續時間的不一樣,這些操做會轉化。
另外,手勢響應系統也能夠提供給其餘組件,可使組件在不關心父組件或子組件的前提下自行處理觸摸交互。PanResponder
類提供了一個對觸摸響應系統的可預測的包裝。它能夠將多點觸摸操做協調成一個手勢。它使得一個單點觸摸能夠接受更多的觸摸操做,也能夠用於識別簡單的多點觸摸手勢。
它在原生事件外提供了一個新的 gestureState
對象:
onPanResponderMove: (nativeEvent, gestureState) => {}
nativeEvent 原生事件對象包含如下字段:
gestureState 對象爲了描繪手勢操做,有以下的字段:
能夠看下 PanResponder
的基本用法:
componentWillMount: function() { this._panResponder = PanResponder.create({ // 要求成爲響應者: onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => true, onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, onPanResponderGrant: (evt, gestureState) => { // 開始手勢操做。給用戶一些視覺反饋,讓他們知道發生了什麼事情! // gestureState.{x,y} 如今會被設置爲0 }, onPanResponderMove: (evt, gestureState) => { // 最近一次的移動距離爲gestureState.move{X,Y} // 從成爲響應者開始時的累計手勢移動距離爲gestureState.d{x,y} }, onPanResponderTerminationRequest: (evt, gestureState) => true, onPanResponderRelease: (evt, gestureState) => { // 用戶放開了全部的觸摸點,且此時視圖已經成爲了響應者。 // 通常來講這意味着一個手勢操做已經成功完成。 }, onPanResponderTerminate: (evt, gestureState) => { // 另外一個組件已經成爲了新的響應者,因此當前手勢將被取消。 }, onShouldBlockNativeResponder: (evt, gestureState) => { // 返回一個布爾值,決定當前組件是否應該阻止原生組件成爲JS響應者 // 默認返回true。目前暫時只支持android。 return true; }, }); }, render: function() { return ( <View {...this._panResponder.panHandlers} /> ); },
結合上面狀態分析,看到 onPanResponderMove
和 onPanResponderRelease
這兩個參數,基本是能夠知足下拉刷新機制的操做流程的。
onPanResponderMove
處理滑動過程。
onPanResponderMove(event, gestureState) { // 最近一次的移動距離爲 gestureState.move{X,Y} // 從成爲響應者開始時的累計手勢移動距離爲 gestureState.d{x,y} if (gestureState.dy >= 0) { if (gestureState.dy < 120) { this.state.containerTop.setValue(gestureState.dy); } } else { this.state.containerTop.setValue(0); if (this.scrollRef) { if (typeof this.scrollRef.scrollToOffset === 'function') { // inner is FlatList this.scrollRef.scrollToOffset({ offset: -gestureState.dy, animated: true, }); } else if(typeof this.scrollRef.scrollTo === 'function') { // inner is ScrollView this.scrollRef.scrollTo({ y: -gestureState.dy, animated: true, }); } } } }
onPanResponderRelease
處理釋放時的操做。
onPanResponderRelease(event, gestureState) { // 用戶放開了全部的觸摸點,且此時視圖已經成爲了響應者。 // 通常來講這意味着一個手勢操做已經成功完成。 // 判斷是否達到了觸發刷新的條件 const threshold = this.props.refreshTriggerHeight || this.props.headerHeight; if (this.containerTranslateY >= threshold) { // 觸發刷新 this.props.onRefresh(); } else { // 沒到刷新的位置,回退到頂部 this._resetContainerPosition(); } // 檢查 scrollEnabled 開關 this._checkScroll(); }
剩下的就是如何區分容器的滑動,和下拉刷新的觸發。
當 ScrollView 的 scrollEnabled
屬性設置爲 false 時,能夠禁止用戶滾動。所以,能夠將 ScrollView 做爲內容容器。當滾動到容器頂部的時候,關閉 ScrollView 的 scrollEnabled
屬性,經過設置 Animated.View 的 translateY
,顯示自定義加載器。
<Animated.View style={[{ flex: 1, transform: [{ translateY: this.state.containerTop }] }]}> {child} </Animated.View>
通過試用,發現這個方案有如下幾個致命性問題:
另外還有 ScrollView 的滑動和模擬的下拉過程滑動配合不夠默契的問題。
ScrollView 在 iOS 設備下有個特性,若是內容範圍比滾動視圖自己大,在到達內容末尾的時候,能夠彈性地拉動一截。能夠將加載指示器放在頁面的上邊緣,彈性滾動時露出。這樣既不須要利用到手勢影響渲染速度,又能夠將滾動和下拉過程很好的融合。
所以,只要處理好滾動操做的各階段事件就好。
onScroll = (event) => { // console.log('onScroll()'); const { y } = event.nativeEvent.contentOffset this._offsetY = y if (this._dragFlag) { if (!this._isRefreshing) { const height = this.props.refreshViewHeight if (y <= -height) { this.setState({ refreshStatus: RefreshStatus.releaseToRefresh, refreshTitle: this.props.refreshableTitleRelease }) } else { this.setState({ refreshStatus: RefreshStatus.pullToRefresh, refreshTitle: this.props.refreshableTitlePull }) } } } if (this.props.onScroll) { this.props.onScroll(event) } } onScrollBeginDrag = (event) => { // console.log('onScrollBeginDrag()'); this._dragFlag = true this._offsetY = event.nativeEvent.contentOffset.y if (this.props.onScrollBeginDrag) { this.props.onScrollBeginDrag(event) } } onScrollEndDrag = (event) => { // console.log('onScrollEndDrag()', y); this._dragFlag = false const { y } = event.nativeEvent.contentOffset this._offsetY = y const height = this.props.refreshViewHeight if (!this._isRefreshing) { if (this.state.refreshStatus === RefreshStatus.releaseToRefresh) { this._isRefreshing = true this.setState({ refreshStatus: RefreshStatus.refreshing, refreshTitle: this.props.refreshableTitleRefreshing }) this._scrollview.scrollTo({ x: 0, y: -height, animated: true }); this.props.onRefresh() } } else if (y <= 0) { this._scrollview.scrollTo({ x: 0, y: -height, animated: true }) } if (this.props.onScrollEndDrag) { this.props.onScrollEndDrag(event) } }
惟一美中不足的就是,iOS 支持超過內容的滑動,安卓不支持,須要單獨適配下安卓。
將加載指示器放在頁面內,經過 scrollTo
方法控制頁面距頂部距離,來模擬下拉空間。(iOS 和安卓方案已在 expo pulltorefresh2 給出)
(demo 建議在移動設備查看,Web 端適配可嘗試將 onScrollBeginDrag onScrollEndDrag
更換爲 onTouchStart onTouchEnd
)
本文主要介紹了在 React Native 開發過程當中,下拉刷新組件的技術調研和實現過程。 Expo demo 包含了兩個方案的主要實現邏輯,讀者可根據自身業務需求作定製,有問題歡迎溝通。
本文發佈自 網易雲音樂前端團隊,可自由轉載,轉載請在標題標明轉載並在顯著位置保留出處。咱們一直在招人,若是你剛好準備換工做,又剛好喜歡雲音樂,那就 加入咱們!