rn觸摸手勢學習——PanResponder。
打造一個大圖瀏覽功能,實現:單擊事件、雙擊事件(雙擊縮放圖片)、長按事件、圖片滑動、雙指縮放圖片。
效果預覽:
20161220124610-2f36201a79.[gif-2-mp4.com].gif
下面來一步步實現。
1. PanResponder
PanResponder類可以將多點觸摸操作協調成一個手勢。它使得一個單點觸摸可以接受更多的觸摸操作,也可以用于識別簡單的多點觸摸手勢。
主要方法:
- onMoveShouldSetPanResponder: (e, gestureState) => {...}
- onStartShouldSetPanResponder: (e, gestureState) => {...}
- onPanResponderGrant: (e, gestureState) => {...}
- onPanResponderMove: (e, gestureState) => {...}
- onPanResponderRelease: (e, gestureState) => {...}
基本用法
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 現在會被設置為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} />
); },
詳細請看文檔 PanResponder
2. 實現圖片跟隨滑動
很明顯,我們需要在onPanResponderMove=(evt, gs)=>{...}中實現邏輯代碼。
直接貼代碼:
/**
*滑動距離大于5才會觸發滑動事件
*longPress 是長按事件標識
*this.isScale是雙擊縮放標識
*/
if((Math.abs(gs.dx)>5 || Math.abs(gs.dy)>5) && !longPress && !this.isScale) {
isSlide = true; //觸發滑動事件,標記滑動為真
this._clickTimeout && clearTimeout(this._clickTimeout);
this._longPressTimeout && clearTimeout(this._longPressTimeout);
}
if(!longPress) {
//this._offsetY、this._offsetX是上次移動距離,gs.dy、gs.dx當前移動距離
//算出x和y軸的偏移量dy,dx
let dy = gs.dy - this._offsetY;
let dx = gs.dx - this._offsetX;
this._offsetX = gs.dx;
this._offsetY = gs.dy;
//dy就是上下方向,這里限制如果比屏幕小,這上下方向不可移動
if(dy > 0) {
if(this.state.top <= 0 && this.state.top + dy > 0) {
this.setState({top: 0,});
dy = 0;
}else if(this.state.top > 0){
dy = 0;
}
}else {
if(this.state.viewHeight <= ScreenHeight - this.state.top) {
dy = 0;
}
}
//改變top和left,就可以看到圖片位置發生變化了
this.setState({
top: this.state.top + dy,
left: this.state.left + dx,
});
}
上面注釋已經很清晰了。
3. 雙擊縮放圖片
先來實現雙擊事件的監聽。
手指全部離開屏幕后,會觸發onPanResponderRelease: (evt, gs) => {...}
所以我們clickNum變量記錄點擊數,設定一個很短時間內連續觸摸,就當作雙擊事件被觸發了。超過那個時間就當是單擊(每個事件觸發后,重置clickNum)
代碼:
clickNum++; //記錄點擊數
if(!isSlide && !longPress) {//滑動和長按都沒有被觸發
if(clickNum == 1) {
//啟動一個200毫秒計時器,這個時間內沒有再次觸摸抬起的話,就是單擊事件,重置clickNum=0;
this._clickTimeout = setTimeout(
() => {
if(clickNum == 1 && !touchBegin){
// alert('單擊');
}
clickNum = 0;
},
200
);
}else if(clickNum == 2){//否則,觸發雙擊事件
// alert('雙擊'+gs.x0);
this._scale(1, 0, 0, this._x - ScreenWidth / 2, this._y - (ScreenHeight - 20) / 2);//縮放
this._clickTimeout && clearTimeout(this._clickTimeout);//取消點擊計時器
this._longPressTimeout && clearTimeout(this._longPressTimeout);//取消長按計時器
clickNum = 0;//重置點擊次數
}
這樣我們就能監聽到是否雙擊了。
下面實現this._scale函數
/**
*type:縮放類型,1雙擊縮放,2手勢縮放
*w:目標縮放寬度,雙擊為0
*h:目標縮放高度,雙擊為0
*offsetX:x中心軸偏移量
*offsetY:y中心軸偏移量
*/
_scale(type, w, h, offsetX, offsetY) {
if (type === 1) {
let sw = this.state.viewWidth;
let sh = this.state.viewHeight;
let pt = 0;
let pl = 0;
let offsetH = 0;
let offsetW = 0;
if(this.state.viewWidth <= ScreenWidth) {
if(this.state.viewWidth < MaxW) {
sw = MaxW;
sh = MaxH;
offsetH = offsetY*MaxH/this.state.viewHeight;
offsetW = offsetX*MaxW/this.state.viewWidth;
pt = (ScreenHeight - sh - 20) / 2 - offsetH;
pl = (ScreenWidth - sw) / 2 - offsetW;
if(MaxH < ScreenHeight) {
pt = (ScreenHeight - sh - 20) / 2;
}else {
if(pt > 0) {
pt = 0;
}else if(ScreenHeight - pt > sh) {
pt =ScreenHeight - sh;
}
}
if(pl > 0) {
pl = 0;
}else if(ScreenWidth - pl > sw) {
pl =ScreenWidth - sw;
}
}
}else {
sw = ScreenWidth;
sh = (MaxH*ScreenWidth)/MaxW;
pt = (ScreenHeight - sh - 20) / 2;
pl = (ScreenWidth - sw) / 2;
}
// this.setState({
// viewWidth: sw,
// viewHeight: sh,
// top: pt,
// left: pl,
// });
// alert(sw+', '+sh+', '+pt+', '+pl);
this._scaleAnimated(sw, sh, pt, pl,400);
this.interval && clearInterval(this.interval);
}else {
//兩手指縮放操作
if(w > ScreenWidth && w < MaxW) {
let sw = w;
let sh = h;
let offsetH = offsetY*sw/this.state.viewHeight;
let offsetW = offsetX*sw/this.state.viewWidth;
let pt = this.state.top + (this.state.viewWidth - sw)/2;
let pl = this.state.left + (this.state.viewHeight - sh)/2;
if(sh < ScreenHeight) {
pt = (ScreenHeight - sh - 20) / 2;
}else {
if(pt > 0) {
pt = 0;
}else if(ScreenHeight - pt > sh) {
pt =ScreenHeight - sh;
}
}
// alert(sw+', '+pl+', '+ScreenWidth+', '+offsetW+', '+offsetX);
if(pl > 0) {
pl = 0;
}else if(ScreenWidth - pl > sw) {
pl =ScreenWidth - sw;
}
this.setState({
viewWidth: sw,
viewHeight: sh,
top: pt,
left: pl,
});
// this._scaleAnimated(sw, sh, pt, pl,0);
// this.interval && clearInterval(this.interval);
}
}
}
直接看type=1里面的,有點麻煩,沒想到優化,將就著先
思路就是,確認縮放后的長寬,計算縮放后top和left的位置,然后就是執行this._scaleAnimated(sw, sh, pt, pl,400);執行動畫縮放
/**
sw: 縮放后寬度
sh: 縮放后高度
pt: 縮放后top
pl: 縮放后left
*/
_scaleAnimated(sw, sh, pt, pl,time) {
let vw = (sw - this.state.viewWidth)/ (time/60.0);
let vh = (sh - this.state.viewHeight) / (time/60.0);
let vt = (pt - this.state.top) / (time/60.0);
let vl = (pl - this.state.left) / (time/60.0);
// let time = 0.0;
let ss =sw+', '+sh+', '+pt+', '+pl;
this.interval2 = setInterval(()=>{
// time = time + (time/60.0);
if(Math.abs(this.state.viewWidth - sw) < Math.abs(vw)) {
vw = sw - this.state.viewWidth;
vh = sh - this.state.viewHeight;
vt = pt - this.state.top;
vl = pl - this.state.left;
this.interval2 && clearInterval(this.interval2);
}
// if(time >= 400.0) {
// this.interval2 && clearInterval(this.interval2);
// }
console.log(vw+', '+vh+', '+vt+', '+vl);
this.setState({
viewWidth: this.state.viewWidth + vw,
viewHeight: this.state.viewHeight + vh,
top: this.state.top + vt,
left: this.state.left + vl,
});
// alert(this.state.viewWidth+', '+this.state.viewHeight+', '+this.state.top+', '+this.state.left+'==='+ss);
}, 10);
}
這里用setInterval來實現動畫,性能問題沒考慮過,
嘗試用animated動畫來實現,但是那些位置我把控不了,嘗試很多遍還是放棄了,誰知道還望賜教。
4. 手勢縮放
if(gs.numberActiveTouches >= 2 ) {
this.isScale = true;
if(!longPress) {
this._longPressTimeout && clearTimeout(this._longPressTimeout);
if(this._touches[0].x <= 0) {
this._touches[0].x = evt.nativeEvent.changedTouches[0].pageX;
this._touches[0].y = evt.nativeEvent.changedTouches[0].pageY;
this._touches[1].x = evt.nativeEvent.changedTouches[1].pageX;
this._touches[1].y = evt.nativeEvent.changedTouches[1].pageY;
this._offsetXY = {};
this._offsetXY.x = (evt.nativeEvent.changedTouches[1].pageX + evt.nativeEvent.changedTouches[0].pageX)/2;
this._offsetXY.y = (evt.nativeEvent.changedTouches[1].pageY + evt.nativeEvent.changedTouches[0].pageY)/2;
}else {
//計算上次兩點距離
const distanceX = Math.abs(this._touches[1].x - this._touches[0].x);
const distanceY = Math.abs(this._touches[1].y - this._touches[0].y);
this._distance = Math.sqrt(distanceX*distanceX + distanceY*distanceY);
//計算本次兩點距離
const distanceX2 = Math.abs(evt.nativeEvent.changedTouches[1].pageX - evt.nativeEvent.changedTouches[0].pageX);
const distanceY2 = Math.abs(evt.nativeEvent.changedTouches[1].pageY - evt.nativeEvent.changedTouches[0].pageY);
this._distance2 = Math.sqrt(distanceX2*distanceX2 + distanceY2*distanceY2);
//縮放兩點中心的偏移量
const offsetXY2 = {};
offsetXY2.x = (evt.nativeEvent.changedTouches[1].pageX + evt.nativeEvent.changedTouches[0].pageX)/2;
offsetXY2.y = (evt.nativeEvent.changedTouches[1].pageY + evt.nativeEvent.changedTouches[0].pageY)/2;
const sw = this.state.viewWidth+((this._distance2-this._distance)/8);
const sh = this.state.viewHeight*sw/this.state.viewWidth;
this._scale(2,sw,sh,0,0);
this._clickTimeout && clearTimeout(this._clickTimeout);
clickNum = 0;
}
}
}
這里就不解釋了,也不注釋了,有耐心就看,就是計算兩次手指移動距離什么的。
代碼以后上傳。