React Native 實現 Slider 元件

hezhii發表於2018-12-27

最近在一個 React Native 專案中需要實現類似 iPhone 中調節亮度和聲音的滑塊元件。React Native 自帶的 Slider 雖然支援一定的定製化,但是仍無法滿足需求。在 GitHub 上搜尋無果後,打算自己實現。最終實現的效果如下圖所示。

React Native 實現 Slider 元件

這篇文章記錄了實現的思路,原始碼見 GitHub,元件也釋出到了 npm,通過 npm i react-native-column-slider 安裝之後就可以在專案中使用了。

基本思路

使用兩個 View,一個作為底部容器,一個用來顯示滑塊值。頂部顯示值的 View 的高度根據滑塊的總高度、滑塊的最大、最小值和滑塊當前值計算得出:(value - min) * height / (max - min)

監聽滑塊上的 move 事件,根據垂直方向上的移動距離佔總高度的比值計算出值的變化,進而在滑動的過程中動態的修改滑塊值。

實現過程

這裡主要介紹核心的功能是如何實現的,一些比較容易的功能(例如顯示當前值)則不作說明。

UI

UI 部分主要是兩個 View

<View style={styles.outer}>
    <View style={styles.inner}/>
</View

const styles = StyleSheet.create({
  outer: {
    backgroundColor: '#ddd',
    height: 200,
    width: 80,
    borderRadius: 20,
    overflow: 'hidden',
  },
  inner: {
    height: 30,
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: '#fff',
  },
});
複製程式碼

給外部的 View 新增圓角和 overflow: 'hidden',內部的 View 則絕對定位到下方。

React Native 實現 Slider 元件

新增陰影

當頁面顏色和滑塊底色相同時,滑塊則會不易辨識,因此我們需要給滑塊外部新增陰影。這裡有一個問題,就是在設定了 overflow: 'hidden' 之後,直接設定陰影無法顯示(參考 issue)。

因此需要在外面在套一個 View 並給它新增上陰影。

處理事件

處理滑動事件,主要依靠 React Native 的手勢響應系統。手勢響應系統不算複雜,我理解下來主要是通過一個問詢機制,當使用者開始觸控和觸控點開始移動時,會“詢問”一個 View 是否願意成為響應者。當成為了響應者之後,後續的手勢操作會回撥相應的函式。

這裡主要依賴於兩個函式:

  • onStartShouldSetPanResponder:在使用者開始觸控的時候(手指剛剛接觸螢幕的瞬間),是否願意成為響應者。
  • onPanResponderMove:使用者正在螢幕上移動手指時(沒有停下也沒有離開螢幕)觸發。

我們需要新增 onStartShouldSetPanResponder 函式並返回 true,即願意成為事件的響應者;然後在 onPanResponderMove 中根據垂直方向上移動的距離,計算出滑塊的值。

我們可以通過 PanResponder.create 方法來給 View 新增手勢響應回撥函式:

constructor(props) {
  super(props);
  this._panResponder = PanResponder.create({
    onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
    onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
    onPanResponderGrant: this._handlePanResponderGrant,
    onPanResponderMove: this._handlePanResponderMove,
    onPanResponderRelease: this._handlePanResponderEnd,
    onPanResponderTerminationRequest: this._handlePanResponderRequestEnd,
    onPanResponderTerminate: this._handlePanResponderEnd,
  });
}

...

<View style={styles.outer} {...this._panResponder.panHandlers}>
  <View style={styles.inner} />
</View>
複製程式碼

一些計算

元件並不是受阻的,需要在元件的 state 中新增 value 屬性用於儲存當前滑塊的值,通過公式 (value - min) * height / (max - min) 計算內部 View 的高度。

滑塊的值不支援點選,每次進行滑動操作時,都是基於當前滑塊的值進行。也就是說,在拖拽的過程中,滑塊的值等於拖拽開始時的值加上拖動的值,所以在拖動開始時,我們需要記錄當前的值:

_handlePanResponderGrant = () => {
  /*
   * 拖動開始時,記錄滑塊當前值。
   */
  this._moveStartValue = this._getCurrentValue();
};
複製程式碼

在拖動的過程中,通過 gestureState.dy 可以獲取垂直方向上拖動的距離,這裡需要注意向上拖是負值,向下是正值。根據垂直拖動的距離佔高度的比值、值區間和當前值就可以計算出拖動後的值:

const ratio = (-gestureState.dy) / height;
const diff = max - min;

const value = this._moveStartValue + ratio * diff
this.setState({
  value,
});
複製程式碼

到這一步,基本功能已經可以實現,效果如下圖。

React Native 實現 Slider 元件

其他細節

考慮最大值和最小值的情況。計算滑塊值的時候,不能超過最大、最小值:

const value = Math.max(
        min,
        Math.min(max, this._moveStartValue + ratio * diff),
    );
複製程式碼

處理步長。當設定了步長時,每次拖動的值應該是步長的整數倍(四捨五入):

const value = Math.max(
    min,
    Math.min(
        max,
        this._moveStartValue + Math.round(ratio * diff / step) * step,
    ),
);
複製程式碼

支援通過 value 屬性設滑塊值。在 state 中記錄上一次屬性中的 value,然後實現 getDerivedStateFromProps 函式,當此次屬性中的 value 不等於上次屬性中的 value 時,更新 state

this.state = {
  value: props.value,
  prevValue: props.value,
};

...

static getDerivedStateFromProps(props, state) {
  if (props.value !== state.prevValue) {
    return ({
      value: props.value,
      prevValue: props.value,
    });
  }
  return null;
}
複製程式碼

相關文章