實現的效果如下
GIF.gif
主要結合了react-native的觸摸和動畫事件,可通過點擊和滑動進行操作。
組件結構
四個滑塊是由父組件map而來,因此只分析一個。以touch部分在左邊為標準,滑塊結構如下
<View style={styles.container}>
<Animated.View
style={[
styles.touch,
{
transform: [
{translateX: this._animatedValue.x}
],
}
]}
>
</Animated.View>
<View style={styles.card}>
</View>
</View>
實質上只是分成了左右結構,左邊的touch較為特殊,因為要實現動畫效果,由動畫組件代替。
想用動畫實現什么屬性進行變化可通過在style中對該屬性的值用Animated.Value()進行初始化。比如想讓touch的寬度用動畫進行變化, 便可初始化寬度為width: new Animated.Value(0)
.
開始
起初,沒有引入動畫,將touch定位設置為relative,在觸摸事件中監聽其onLayout,通過setState實時刷新位置,代碼實現見這一版。
為了性能,為了交互,也為了折騰,引入Animated與PanResponder,讓這兩個好基友一起做點什么。
關于Animated和PanResponder的詳細介紹可查看本文底部講得非常好的參考鏈接,下面說實現。
constructor
constructor(props) {
super(props);
this.state = {
isTouch: false, // 是否處于點擊狀態
blockInLeft: true, // touch是否在左側
}
this._containerWidth = null; //滑塊組件寬度,可在render內通過onLayout得到
this._touchBlockWidth = null; //touch寬度
this._touchTimeStamp = null; // 為不允許雙擊事件發生設置的一個當前點擊時間點
this._startAnimation = this._startAnimation.bind(this)
this._animatedDivisionValue = new Animated.Value(0); //初始化動畫值
}
觸摸事件注冊
componentWillMount() {
this._animatedValue = new Animated.ValueXY()
this._value = {x: 0}
// 這里為了監聽后面動畫事件中setValue的值
this._animatedValue.addListener((value) => this._value = value);
this._panResponder = PanResponder.create({
// 寫法基本是固定的
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder.bind(this),
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder.bind(this),
onPanResponderGrant: this._handlePanResponderGrant.bind(this),
onPanResponderMove: this._handlePanResponderMove.bind(this),
onPanResponderRelease: this._handlePanResponderEnd.bind(this),
onPanResponderTerminate: this._handlePanResponderEnd.bind(this),
});
}
與動畫的結合
_handleStartShouldSetPanResponder(e, gestureState){
// 避免雙擊,與上次點擊在500ms以內時不處理點擊事件
const tick = new Date().getTime();
if (tick - this._touchTimeStamp < 500) {
return false;
}
this._touchTimeStamp = tick;
return true;
}
_handleMoveShouldSetPanResponder(e, gestureState){
// 是否響應移動事件
return true;
}
_handlePanResponderGrant(e, gestureState){
// touch告訴事件處理器,有人把手放我身上了
this.setState({
isTouch: true
})
// 歸位
this._animatedValue.setOffset({x: this._value.x});
this._animatedValue.setValue({x: 0});
}
_handlePanResponderMove(e, gestureState) {
// 這個方法在手指移動過程中連續調用
// 計算滑塊組件減去touch部分剩余的寬度,可寫在外部
let canTouchLength = this._containerWidth - this._touchBlockWidth
// 在邊界處不可向己邊滑動。祥看下面endValue介紹
if ( (this.state.blockInLeft && gestureState.dx > 0 && gestureState.dx < canTouchLength) || (!this.state.blockInLeft && gestureState.dx < 0 && gestureState.dx > -canTouchLength) ) {
// 動畫跟隨觸摸移動的關鍵,觸摸動畫實現的核心所在。只有在符合上述條件下touch才進行移動。
this._animatedValue.setValue({x: gestureState.dx})
}
// 如果不需要邊界處理,也可用event代替setValue
// Animated.event([
// null, {dx: this._animatedValue.x}
// ])
}
_handlePanResponderEnd(e, gestureState){
// 這個方法在手指離開屏幕時調用
// 同上,代碼冗余,建議寫在外部
let canTouchLength = this._containerWidth - this._touchBlockWidth
// 偏移。moveDistance計算touch的偏移值,判斷其不等于0是為了處理點擊操作
// gestureState.moveX有移動才會有值,點擊的話值為0
let moveDistance = gestureState.moveX !== 0 ? gestureState.moveX - gestureState.x0 : 0;
// 確定移動方向。moveDistance大于0時代表touch向右移動,不管在左邊還是右邊
const toRight = moveDistance>0 ? true : false;
// 取移動距離
moveDistance = Math.abs(moveDistance)
// 設定個中間值決定滑塊最終移向哪邊。中間值為滑塊寬度減去touch寬度的一半
const middleValue = canTouchLength / 2
// endValue為以左邊為原點,最終移動位置相對于左邊的距離。
// 這里為了實現觸摸時如果沒有將touch移動到最大位置釋放操作,touch最終選擇移動到左邊還是右邊
// 所以,向右移動touch時,中點以前為0,過了中點endValue為最大值
// 再向左移動時,中點以前為0(即不移動),過了中點為最大值的反向
// 這里還有個問題,touch的偏移實現上,是有累加性的。
// 即比如先向右移動touch到最大值,0 + maxValue,實現這個操作后,滑塊所處的位置maxValue會重設為0
// 如果想移回來到左邊,就需要0 - maxValue,這便是偏移的累加性
let endValue = 0
// 防止touch會被鼠標拽出邊界,給第二個條件加上 this.state.blockInLeft 的判斷
if ( (this.state.blockInLeft && moveDistance === 0) || (toRight && this.state.blockInLeft && (moveDistance > middleValue)) ) {
// touch向右移動時過了中點,或者touch在左邊時,被單擊
endValue = canTouchLength
this.setState({
blockInLeft: false
})
} else if ( (!this.state.blockInLeft && moveDistance === 0) || (!toRight && !this.state.blockInLeft && (moveDistance > middleValue)) ) {
// touch向左移動時過了中點,或者touch在右邊時,被單擊
endValue = -canTouchLength
this.setState({
blockInLeft: true
})
}
// touch到邊界時會回彈的動畫開始
this._startAnimation(endValue);
this.setState({
// 這人把手從我身上拿開了
isTouch: false
})
}
_startAnimation(endValue) {
Animated.spring(this._animatedValue, {
toValue: endValue,
tension: 80
}).start(
() => {
// 這里本來想在動畫結束后做一些事情,但是發現回調有些問題
// 可能是回彈的動畫不一定會在touch移動的動畫結束后觸發
}
);
}
這是整個觸摸與動畫結合的實踐。對于touch移動后另一邊的信息也發生移動,可通過監聽touch的blockInLeft,用margin對另一邊信息進行定位,這是我試過最簡單而且沒有副作用的方法。
還想實現的一個功能是,隨著touch從一邊移動到另一邊,底部文字的透明度從1 -> 0 -> 1 這樣切換。
代碼可以精簡,性能還可以優化,先提供一個實現該功能的方法。歡迎拍磚指正,交流學習。