支援通用的手勢縮放,手勢跟隨,多圖翻頁
手勢系統
通過 PanResponder.create
建立手勢響應者,分別在 onPanResponderMove
與 onPanResponderRelease
階段進行處理實現上述功能。
手勢階段
大體介紹整體設計,在每個手勢階段需要做哪些事。
開始
onPanResponderGrant
1 2 3 4 5 |
// 開始手勢操作 this.lastPositionX = null this.lastPositionY = null this.zoomLastDistance = null this.lastTouchStartTime = new Date().getTime() |
開始時非常簡單,初始化上一次的唯一、縮放距離、觸控時間,這些中間量分別會在計算增量位移、增量縮放、使用者鬆手意圖時使用。
移動
onPanResponderMove
1 2 3 4 5 |
if (evt.nativeEvent.changedTouches.length <= 1) { // 單指移動 or 翻頁 } else { // 雙指縮放 } |
在移動中,先根據手指數量區分使用者操作意圖。
當單個手指時,可能是移動或者翻頁
先記錄增量位移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// x 位移 let diffX = gestureState.dx - this.lastPositionX if (this.lastPositionX === null) { diffX = 0 } // y 位移 let diffY = gestureState.dy - this.lastPositionY if (this.lastPositionY === null) { diffY = 0 } // 保留這一次位移作為下次的上一次位移 this.lastPositionX = gestureState.dx this.lastPositionY = gestureState.dy |
獲得了位移距離後,我們先不要移動圖片,因為橫向操作如果溢位了螢幕邊界,我們要觸發圖片切換(如果滑動方向還有圖),此時不能再增加圖片的偏移量,而是要將其偏移量記錄下來儲存到溢位量,當這個溢位量沒有用完時,只滑動整體容器,不移動圖片,用完時再移動圖片,就可以將移動圖片與整體滑動連貫起來了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// diffX > 0 表示手往右滑,圖往左移動,反之同理 // horizontalWholeOuterCounter > 0 表示溢位在左側,反之在右側,絕對值越大溢位越多 if (this.props.imageWidth * this.scale > this.props.cropWidth) { // 如果圖片寬度大圖盒子寬度, 可以橫向拖拽 // 沒有溢位偏移量或者這次位移完全收回了偏移量才能拖拽 if (this.horizontalWholeOuterCounter > 0) { // 溢位在右側 if (diffX < 0) { // 從右側收緊 if (this.horizontalWholeOuterCounter > Math.abs(diffX)) { // 偏移量還沒有用完 this.horizontalWholeOuterCounter += diffX diffX = 0 } else { // 溢位量置為0,偏移量減去剩餘溢位量,並且可以被拖動 diffX += this.horizontalWholeOuterCounter this.horizontalWholeOuterCounter = 0 this.props.horizontalOuterRangeOffset(0) } } else { // 向右側擴增 this.horizontalWholeOuterCounter += diffX } } else if (this.horizontalWholeOuterCounter < 0) { // 溢位在左側 if (diffX > 0) { // 從左側收緊 if (Math.abs(this.horizontalWholeOuterCounter) > diffX) { // 偏移量還沒有用完 this.horizontalWholeOuterCounter += diffX diffX = 0 } else { // 溢位量置為0,偏移量減去剩餘溢位量,並且可以被拖動 diffX += this.horizontalWholeOuterCounter this.horizontalWholeOuterCounter = 0 this.props.horizontalOuterRangeOffset(0) } } else { // 向左側擴增 this.horizontalWholeOuterCounter += diffX } } else { // 溢位偏移量為0,正常移動 } |
上述程式碼表示在溢位時,優先計算溢位量,並且當收縮時,用增量位移抵消溢位量,最後如果還有增量位移,就可以移動圖片了:
1 2 |
// 產生位移 this.positionX += diffX / this.scale |
還有橫向不能出現黑邊,因此移動到邊界時會把位移全部轉換為偏移量:
1 2 3 4 5 6 7 8 9 10 11 |
// 但是橫向不能出現黑邊 // 橫向能容忍的絕對值 const horizontalMax = (this.props.imageWidth * this.scale - this.props.cropWidth) / 2 / this.scale if (this.positionX < -horizontalMax) { // 超越了左邊臨界點,還在繼續向左移動 this.positionX = -horizontalMax this.horizontalWholeOuterCounter += diffX } else if (this.positionX > horizontalMax) { // 超越了右側臨界點,還在繼續向右移動 this.positionX = horizontalMax this.horizontalWholeOuterCounter += diffX } this.animatedPositionX.setValue(this.positionX) |
PS:如果圖片長寬沒有超過外部容器大小,那麼所有位移都算做溢位量,也就是圖片不能被移動,所有移動都會當做在切換圖片:
1 2 |
// 不能橫向拖拽,全部算做溢位偏移量 this.horizontalWholeOuterCounter += diffX |
我們在溢位量不為0的時候,執行切換圖片的邏輯即可,由於本文主要介紹手勢操作,切換圖片的邏輯不再細說。最後再給Y軸限定低於盒子高度不能縱向移動:
1 2 3 4 5 |
if (this.props.imageHeight * this.scale > this.props.cropHeight) { // 如果圖片高度大圖盒子高度, 可以縱向拖拽 this.positionY += diffY / this.scale this.animatedPositionY.setValue(this.positionY) } |
當兩個手指時,希望縮放
先找到兩手位置中 minX
minY
maxX
maxY
,由此計算縮放距離:
1 2 3 4 |
const widthDistance = maxX - minX const heightDistance = maxY - minY const diagonalDistance = Math.sqrt(widthDistance * widthDistance + heightDistance * heightDistance) this.zoomCurrentDistance = Number(diagonalDistance.toFixed(1)) |
開始縮放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let distanceDiff = (this.zoomCurrentDistance - this.zoomLastDistance) / 400 let zoom = this.scale + distanceDiff if (zoom < 0.6) { zoom = 0.6 } if (zoom > 10) { zoom = 10 } // 記錄之前縮放比例 const beforeScale = this.scale // 開始縮放 this.scale = zoom this.animatedScale.setValue(this.scale) |
此時需要注意的時,我們還要以雙手中心點為固定點,保持這個點在螢幕的相對位置不變,這樣才能放大到使用者想看的部分,我們需要對圖片進行位移:
1 2 3 4 5 6 7 8 9 10 11 |
// 圖片要慢慢往兩個手指的中心點移動 // 縮放 diff const diffScale = this.scale - beforeScale // 找到兩手中心點距離頁面中心的位移 const centerDiffX = (evt.nativeEvent.changedTouches[0].pageX + evt.nativeEvent.changedTouches[1].pageX) / 2 - this.props.cropWidth / 2 const centerDiffY = (evt.nativeEvent.changedTouches[0].pageY + evt.nativeEvent.changedTouches[1].pageY) / 2 - this.props.cropHeight / 2 // 移動位置 this.positionX -= centerDiffX * diffScale this.positionY -= centerDiffY * diffScale this.animatedPositionX.setValue(this.positionX) this.animatedPositionY.setValue(this.positionY) |
其實是計算了這次的縮放增量,再計算出雙手中心點距離螢幕正中心的距離,用這個距離乘以縮放增量就是這次縮放造成的中心點位移值,我們再反向移動這個位移抵消掉,就會產生這個點的相對位置不變的效果。
結束
結束時主要做一些重置操作,和判斷是否翻到下一頁或者關閉看大圖。比如圖片被移出邊界需要彈回來,縮放的過小需要恢復原大小。
1 2 3 4 5 6 7 8 9 |
// 手勢完成,如果是單個手指、距離上次按住只有預設秒、滑動距離小於預設值,認為是退出 const stayTime = new Date().getTime() - this.lastTouchStartTime const moveDistance = Math.sqrt(gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy) if (evt.nativeEvent.changedTouches.length <= 1 && stayTime < this.props.leaveStayTime && moveDistance < this.props.leaveDistance) { this.props.onCancle() return } else { this.props.responderRelease(gestureState.vx) } |
當單手指結束,並且移動距離小於某個值,並且移動時間過短,就會認為是退出,否則手勢結束,再判斷是否要切換圖片,切換圖片部分不再展開說明,下面羅列出結束時需要注意重置的要點,粗看即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
if (this.scale < 1) { // 如果縮放小於1,強制重置為 1 this.scale = 1 Animated.timing(this.animatedScale, { toValue: this.scale, duration: 100, }).start() } if (this.props.imageWidth * this.scale <= this.props.cropWidth) { // 如果圖片寬度小於盒子寬度,橫向位置重置 this.positionX = 0 Animated.timing(this.animatedPositionX, { toValue: this.positionX, duration: 100, }).start() } if (this.props.imageHeight * this.scale <= this.props.cropHeight) { // 如果圖片高度小於盒子高度,縱向位置重置 this.positionY = 0 Animated.timing(this.animatedPositionY, { toValue: this.positionY, duration: 100, }).start() } // 橫向肯定不會超出範圍,由拖拽時控制 // 如果圖片高度大於盒子高度,縱向不能出現黑邊 if (this.props.imageHeight * this.scale > this.props.cropHeight) { // 縱向能容忍的絕對值 const verticalMax = (this.props.imageHeight * this.scale - this.props.cropHeight) / 2 / this.scale if (this.positionY < -verticalMax) { this.positionY = -verticalMax } else if (this.positionY > verticalMax) { this.positionY = verticalMax } Animated.timing(this.animatedPositionY, { toValue: this.positionY, duration: 100, }).start() } // 拖拽正常結束後,如果沒有縮放,直接回到0,0點 if (this.scale === 1) { this.positionX = 0 this.positionY = 0 Animated.timing(this.animatedPositionX, { toValue: this.positionX, duration: 100, }).start() Animated.timing(this.animatedPositionY, { toValue: this.positionY, duration: 100, }).start() } |
如果結束時速度超過某個閾值,也要切換圖片,這個判斷就很方便了:
1 2 3 4 5 6 7 8 9 10 |
if (gestureState.vx > 0.7) { // 上一張 this.goBack.call(this) } else if (gestureState.vx < -0.7) { // 下一張 this.goNext.call(this) } // 水平溢位量置空 this.horizontalWholeOuterCounter = 0 |
最後重置水平溢位量,完成整套手勢操作,可以進行周而復始的迴圈了。
一種實現
應大家要求,加上上述理論實現的程式碼倉庫地址:
https://github.com/ascoders/react-native-image-viewer
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式