rn触摸手势学习——PanResponder。
打造一个大图浏览功能,实现:单击事件、双击事件(双击缩放图片)、长按事件、图片滑动、双指缩放图片。
效果预览:
下面来一步步实现。
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;
}
}
}
这里就不解释了,也不注释了,有耐心就看,就是计算两次手指移动距离什么的。
代码以后上传。