前言
前面介绍了前面介绍了嵌套滑动、自定义Behavior等高级UI实现原理,今天介绍一种比较简单的高级UI用法——手势处理。
你可以点开自己的手机相册,点开任意一张图片,双击图片会有放大效果,再次双击会缩小为原来大小。并且双指操作可以放大缩小图片。今天,小编就来实现一下这样的PhotoView效果。但是我的效果处理肯定不是很完美,毕竟追求代码简洁。
效果展示图:
手势处理API
首先,先介绍一个Android处理单指手势的API GestureDetector
,在创建一个GestureDetector对象时需要传入一个监听者OnGestureListener
,所以我们需要实现OnGestureListener,通常实现SimpleOnGestureListener这个接口里面的功能就足够我们使用了,所以实现它即可。
SimpleOnGestureListener的方法介绍
class PhotoScaleGestureDetector extends GestureDetector.SimpleOnGestureListener{ @Override public boolean onDown(@NonNull MotionEvent e) { return super.onDown(e); } @Override public void onShowPress(@NonNull MotionEvent e) { super.onShowPress(e); } @Override public boolean onSingleTapUp(@NonNull MotionEvent e) { return super.onSingleTapUp(e); }
@Override public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { return super.onScroll(e1, e2, distanceX, distanceY); } @Override public void onLongPress(@NonNull MotionEvent e) { super.onLongPress(e); }
@Override public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { return super.onFling(e1, e2, velocityX, velocityY); }
@Override public boolean onSingleTapConfirmed(@NonNull MotionEvent e) { return super.onSingleTapConfirmed(e); }
@Override public boolean onDoubleTap(@NonNull MotionEvent e) { return super.onDoubleTap(e); }
@Override public boolean onDoubleTapEvent(@NonNull MotionEvent e) { return super.onDoubleTapEvent(e); }
}
|
下图是三个类或接口的时序关系,PhotoView为小编的自定义View。
从时序图可以看出,我们只需要实例化SimpleOnGestureListener并且在对应的方法中做自己的处理,在onTouchEvent触摸事件中回调gestureDetector.onTouchEvent(event)就能将自定义的触摸事件设置完成。
注:onDown必须返回true,否则就不会处理点击事件了。
另外需要注意的一个点是,如果GestureDetector是在子线程中创建的,还必须得调用Looper.prepare(),因为如果我们不手动传入一个handler实例,系统就会帮我们new一个,而Handler是会和所在线程进行一个绑定,必须进行prepare,而这个handler就是来处理触摸事件的,下面通过一张图来进行描述它们之间的关系
ScaleGestureDetector.OnScaleGestureListener的方法
下面的代码是小编demo中的
scaleGestureDetector.getScaleFactor()为缩放因子
- onScaleBegin:scale变化前的处理
- onScale:scale变化时,也就是双指缩放图片scale处理的位置
- onScaleEnd:scale变化后的处理
注:onScaleBegin方法中必须返回true,否则双指操作无效
class PhotoScaleGestureDetector implements ScaleGestureDetector.OnScaleGestureListener {
float initScale; @Override public boolean onScale(@NonNull ScaleGestureDetector scaleGestureDetector) { if ((currentScale>smallScale && !isEnlarge)||(currentScale==smallScale && !isEnlarge)){ isEnlarge = !isEnlarge; } currentScale = initScale * scaleGestureDetector.getScaleFactor(); invalidate(); return false; }
@Override public boolean onScaleBegin(@NonNull ScaleGestureDetector scaleGestureDetector) { initScale = currentScale; return true; }
@Override public void onScaleEnd(@NonNull ScaleGestureDetector scaleGestureDetector) {
} }
|
图片缩放效果的实现
class PhotoGestureDetector extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(@NonNull MotionEvent e) { return true; }
@Override public boolean onDoubleTap(@NonNull MotionEvent e) { isEnlarge = !isEnlarge; if (isEnlarge){ offsetX = (e.getX()-getWidth()/2f)-(e.getX()-getWidth()/2f) * (bigScale/smallScale); offsetY = (e.getY()-getHeight()/2f)-(e.getY()-getHeight()/2f)* (bigScale/smallScale); fixOffsets(); currentScale = bigScale; getScaleAnim(smallScale,bigScale).start(); }else { currentScale = smallScale; getScaleAnim(smallScale,bigScale).reverse(); } return super.onDoubleTap(e); }
@Override public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { if (isEnlarge){ offsetX -= distanceX; offsetY -= distanceY; fixOffsets(); invalidate(); } return super.onScroll(e1, e2, distanceX, distanceY); }
@Override public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { if (isEnlarge){ overScroller.fling( (int)offsetX, (int)offsetY, (int) velocityX, (int) velocityY, -(int) (bitmap.getWidth() * bigScale - getWidth()) / 2, (int) (bitmap.getWidth() * bigScale - getWidth()) / 2, -(int) (bitmap.getHeight() * bigScale - getHeight()) / 2, (int) (bitmap.getHeight() * bigScale - getHeight()) / 2, 300, 300 ); postOnAnimation(new FlingRunner());
} return super.onFling(e1, e2, velocityX, velocityY); } }
|
这个算法用于限制图片偏移的范围
惯性事件处理
其他重要参数截图中都有介绍,下面主要介绍一下postAnimation,一次只能移动一小段距离(一帧),需要多次调度动画直到完成滑动偏移,这里采用递归的方式,实例化一个Runnable放入postOnAnimation()中
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh);
originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f; originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;
if ((float)bitmap.getWidth()/bitmap.getHeight()>(float) getWidth()/getHeight()){ smallScale = (float) getWidth()/ bitmap.getWidth(); bigScale = (float) getHeight()/bitmap.getHeight()*OVER_SCALE_FACTOR; }else { smallScale = (float) getHeight()/bitmap.getHeight(); bigScale = (float) getWidth()/bitmap.getWidth()*OVER_SCALE_FACTOR; } currentScale = smallScale; }
|
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas);
float scaleFaction = (currentScale-smallScale)/(bigScale-smallScale); canvas.translate(offsetX * scaleFaction,offsetY * scaleFaction);
canvas.scale(currentScale,currentScale,getWidth()/2f,getHeight()/2f); canvas.drawBitmap(bitmap,originalOffsetX,originalOffsetY,paint);
}
|
判断是单指处理还是双指处理
@Override public boolean onTouchEvent(MotionEvent event) { boolean result = scaleGestureDetector.onTouchEvent(event); if(!scaleGestureDetector.isInProgress()){ result = gestureDetector.onTouchEvent(event); } return result; }
|
关于多指触摸事件
注意:多指处理需要event.getActionMasked()
一次触摸事件中Down和Up都只触发一次
多指触摸事件是存储在一个键值(<id,index>)对数组中,从0开始,可以理解为一个SpareArray,需要一个currentPointId记录当前正在处理事件的手指。
该数组的规则:
- 当手指Up,对应的id和index会从数组中移除,移除后数组会偏移补全,比如0-4移除3,4会补全3的位置,并且index变为3,而id不变
- 再有新的手指点击,如果前面的id不连续,会补全,比如接着上方的例子原来的index3会变为index4,而新的点击事件为id3、index3,也就是说id不会变,index会变
lastOffsetX、lastOffsetY记录上一次的偏移值,否则有新的手指点击的话会出现图片“闪现”效果
event.findPointerIndex(Id)
通过Id获取Index
event.getActionIndex()
获取当前触摸手指的index,event.getPointerId(actionIndex)
通过index获取id
抬起的手指是活跃手指并且是最后一个按下的手指,那么活跃手指更新为抬起手指的前一个->upIndex = event.getPointerCount() -2
如果抬起手指是中间的并且是活跃手指,那么活跃手指更新为下一个->upIndex++
以上两点都是根据多指触摸事件数组规则做出的判断
public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()){ case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); lastOffsetX = offsetX; lastOffsetY = offsetY;
currentPointId = 0; break; case MotionEvent.ACTION_MOVE: if (event.getPointerCount()!=0){ int index = event.findPointerIndex(currentPointId); offsetX = lastOffsetX + event.getX(index) - downX; offsetY = lastOffsetY + event.getY(index) - downY; invalidate(); } break; case MotionEvent.ACTION_POINTER_DOWN: if (event.getPointerCount()>1){ int actionIndex = event.getActionIndex(); currentPointId = event.getPointerId(actionIndex);
downX = event.getX(actionIndex); downY = event.getY(actionIndex); lastOffsetX = offsetX; lastOffsetY = offsetY; } break; case MotionEvent.ACTION_UP: int upIndex = event.getActionIndex(); int pointId = event.getPointerId(upIndex); if (pointId==currentPointId){ if (upIndex == event.getPointerCount()-1){ upIndex = event.getPointerCount() -2; }else{ upIndex++; } currentPointId = event.getPointerId(upIndex); downX = event.getX(upIndex); downY = event.getY(upIndex); lastOffsetX = offsetX; lastOffsetY = offsetY; } break; } return true;
|
总结
其实,多指触摸事件很少使用,不过应用起来还是有很不错的效果。
实现PhotoView的效果重点就是GestureDetector.SimpleOnGestureListener
、ScaleGestureDetector.OnScaleGestureListener
这两个API的使用,其实自定义View这一块,只要把尺寸、偏移等这些处理好了实现想要的效果还是很容易的。
最后附上源码:
源码地址