布局,绘制,触摸反馈;想要实现和用户手势交互,必须了解触摸反馈,简述下:
dispatchTouchEvent:分发事件
onInterceptTouchEvent:拦截事件
onTouchEvent:消费事件
AAA------dispatchTouchEvent
AAA------onInterceptTouchEvent
BBB------dispatchTouchEvent
BBB------onInterceptTouchEvent
CCC------dispatchTouchEvent
CCC------onTouchEvent
BBB------onTouchEvent
AAA------onTouchEvent
事件从最上面的父view开始到最后一个子view,递归dispatchTouchEvent()然后调用onInterceptTouchEvent()检查自己是否拦截事件,如果都没有,然后反向递归onTouchEvent()
如果父view拦截事件“onInterceptTouchEvent()返回ture”后面的所有子view收不到任何事件;
如果子view设置了点击事件或者本身view是可点击的(即使没有设置点击事件),点击子view的时候所有父view都收不到onTouchEvent事件
public boolean onTouchEvent(MotionEvent event) {
/*
getAction()是以前的方法,多点触碰后新增getActionMasked()
两个方法没有任何性能区别,只不过getAction()不支持多点触碰
*/
switch (event.getActionMasked()) {
// 第一个手指按下
case MotionEvent.ACTION_DOWN:
break;
// 手指移动
case MotionEvent.ACTION_MOVE:
break;
// 手势被取消
case MotionEvent.ACTION_CANCEL:
break;
// 最后一个手指抬起
case MotionEvent.ACTION_UP:
break;
// 多个手指按下
case MotionEvent.ACTION_POINTER_DOWN:
break;
// 手指离开了触摸屏(有其他手指还在触摸屏上,最后一根手指抬起不会触发)
case MotionEvent.ACTION_POINTER_UP:
break;
}
/*
返回true可理解为所有权的标志,返回true代表消费了事件,后续的所有事件都不会往下传递
这里返回true和在down事件返回true一样,当down事件返回true之后就代表消费事件
*/
return true;
}
想要实现长按,双击,缩放等事件仅通过onTouchEvent()方法不太容易,可借助GestureDetectorCompat 和 ScaleGestureDetector 实现;
public class ViewC extends View implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
/*
手势侦测器
GestureDetector和GestureDetectorCompat是一个东西,一般带Compat都是为了兼容性出的
使用GestureDetectorCompat需要三步:
①实现对应方法
②重写onTouchEvent方法,修改返回值为GestureDetectorCompat.onTouchEvent(event);
③如果需要双击事件,修改返回值为GestureDetectorCompat.setOnDoubleTapListener(this)
*/
private GestureDetectorCompat gestureDetector;
public ViewC(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
// 创建手势侦测器,需要重写onTouchEvent
gestureDetector = new GestureDetectorCompat(getContext(), this);
// 设置双击监听
gestureDetector.setOnDoubleTapListener(this);
/*
只实现双击监听可用下面简便写法
① onDown 返回true
② 重写onTouchEvent()返回 detector.onTouchEvent(event)
*/
// gestureDetector=new GestureDetectorCompat(getContext(),new GestureDetector.SimpleOnGestureListener(){
// @Override
// public boolean onDown(MotionEvent e) {
// return true;
// }
//
// @Override
// public boolean onDoubleTap(MotionEvent e) {
// return super.onDoubleTap(e);
// }
// });
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 使用GestureDetector
return gestureDetector.onTouchEvent(event);
}
/*
收到ACTION_DOWN后onDown()就会被调用
onDown()决定事件是否被消费,true为消费
如果这里不消费,下面所有的实现方法都无效
*/
@Override
public boolean onDown(MotionEvent e) {
return true;
}
// 预按下事件(父view是滑动控件会出现预按下事件)
@Override
public void onShowPress(MotionEvent e) {
}
/*
单击:每次按下抬起都会有响应(返回值无用,只有onDown里面的返回值有用)
如果支持长按事件(按下500ms触发长按事件),在按下500ms后抬起不会触发onSingleTapUp()
总:如果长按事件打开,按下后500ms内抬起触发;
如果长按事件未打开,按下后无论何时抬起都会触发
detector.setIsLongPressEnabled(false); // 关闭长按事件
*/
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
/**
* 同步onMove(),和onMove()一样
* 实现功能:图片放大后跟随手指滑动偏移
*
* @param downEvent 按下的事件
* @param event 当前事件
* @param distanceX 上一个点到当前点之间距离
* @param distanceY 上一个点到当前点之间距离
* @return 没用
*/
@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent event, float distanceX, float distanceY) {
return false;
}
// 长按事件
@Override
public void onLongPress(MotionEvent e) {
}
// 惯性滑动,实现fling效果 velocity(速度:抬手一瞬间的 距离/时间 )
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
/*
onSingleTapUp 单击事件 (长按事件未开启的话,只要手指按下抬起就会触发)
onSingleTapConfirmed 单机确认事件
当设置双击监听打开后,想要使用单机监听用onSingleTapConfirmed方法,
否则双击屏幕会先调用onSingleTapUp方法,onDoubleTap
*/
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
// 双击事件(两次触摸间隔<300ms)
@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}
// 双击不松手滑动触发
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
}
public class ViewC extends View implements ScaleGestureDetector.OnScaleGestureListener {
/*
缩放手势侦测器
使用方法和GestureDetectorCompat类似,需要两步:
①实现对应方法
②重写onTouchEvent方法,修改返回值为ScaleGestureDetector.onTouchEvent(event);
*/
private ScaleGestureDetector scaleGestureDetector;
public ViewC(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
scaleGestureDetector = new ScaleGestureDetector(getContext(), this);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 使用GestureDetector
return scaleGestureDetector.onTouchEvent(event);
}
// 在scale的时候
@Override
public boolean onScale(ScaleGestureDetector detector) {
// 倍数
float scaleFactor = detector.getScaleFactor();
// 焦点
detector.getFocusX();
detector.getFocusY();
return false;
}
// 在scale之前,可做初始化工作
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// 使用放缩必须返回true
return true;
}
// 在scale之后
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
如果想要同时使用双击,长按,缩放手势,可在onTouchEvent()方法中根据触摸屏幕手指数量判断;
public boolean onTouchEvent(MotionEvent event) {
// 使用GestureDetector
if (event.getPointerCount()>1){
return scaleGestureDetector.onTouchEvent(event);
}else {
return gestureDetector.onTouchEvent(event);
}
}
根据手势监听器实现一个图片双击缩放,滑动偏移加上fling效果的demo
/**
* ① 计算图片smallScale和bigScale ----> 通过 canvas.scale()方法实现放大缩小
* ② 重写双击事件 ----> 通过 GestureDetectorCompat 实现双击事件监听
* ③ 根据双击事件切换scale ----> 设置标识记录大图小图
* ④ 添加动画 ----> 通过 ObjectAnimator 实现大图小图切换动画
* ⑤ 实现滑动偏移 ----> 在onScroll() 方法中实现手指滑动偏移
* ⑥ 实现fling效果 ----> 在onFling() 方法中通过OverScroller实现fling效果
*/
public class ScalableImageView extends View implements Runnable {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap bitmap;
// 记录手指滑动距离
float offsetX;
float offsetY;
float originalOffsetX;
float originalOffsetY;
float smallScale;
float bigScale;
// 判断是否是大图
boolean big;
// 动画
float scaleFraction; // 0 - 1f
ObjectAnimator scaleAnimator;
// 手势侦测器
private GestureDetectorCompat detector;
CustomGestureListener gestureListener = new CustomGestureListener();
/*
在onFling中使用,OverScroller算是一个计算器,需要在onFling中设置参数
*/
private OverScroller overScroller;
public ScalableImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
bitmap = getAvatar((int) UnitUtil.dp2px(300));
detector = new GestureDetectorCompat(getContext(), gestureListener);
overScroller = new OverScroller(getContext());
}
@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() / (float) bitmap.getHeight() > (float) getWidth() / (float) getHeight()) {
smallScale = (float) getWidth() / (float) bitmap.getWidth();
bigScale = (float) getHeight() / (float) bitmap.getHeight();
} else {
smallScale = (float) getHeight() / (float) bitmap.getHeight();
bigScale = (float) getWidth() / (float) bitmap.getHeight();
}
bigScale = bigScale * 2f;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 给随手指偏移 乘以scaleFraction在缩小的时候自动居中
canvas.translate(offsetX * scaleFraction, offsetY * scaleFraction);
// float scale = big ? bigScale : smallScale;
// 双击的时候给随这动画的执行scaleFraction会变化
float scale = smallScale + (bigScale - smallScale) * scaleFraction;
canvas.scale(scale, scale, getWidth() / 2, getHeight() / 2);
// 图片居中
canvas.drawBitmap(
bitmap,
originalOffsetX, originalOffsetY,
mPaint
);
}
// 使用GestureDetectorCompat
@Override
public boolean onTouchEvent(MotionEvent event) {
return detector.onTouchEvent(event);
}
@SuppressLint("NewApi")
@Override
public void run() {
// 动画是否还在进行中,如果结束了直接return
if (!overScroller.computeScrollOffset()) return;
// 更新xy值,计算之后可获取最新的值(每次获取的时候都需要让scroller先计算一下)
overScroller.computeScrollOffset();
offsetX = overScroller.getCurrX();
offsetY = overScroller.getCurrY();
invalidate();
postOnAnimation(this);
}
public float getScaleFraction() {
return scaleFraction;
}
// 动画使用,set方法里面要加invalidate()刷新
public void setScaleFraction(float scaleFraction) {
this.scaleFraction = scaleFraction;
invalidate();
}
private ObjectAnimator getScaleAnimator() {
if (scaleAnimator == null) {
scaleAnimator = ObjectAnimator.ofFloat(this, "scaleFraction", 0, 1);
}
return scaleAnimator;
}
Bitmap getAvatar(int width) {
BitmapFactory.Options options = new BitmapFactory.Options();
// inJustDecodeBounds为true,不返回bitmap,只返回这个bitmap的尺寸
options.inJustDecodeBounds = true;
// 从资源中读取(比较浪费资源,所以上面设置为true,只获取图片宽高)
BitmapFactory.decodeResource(getResources(), R.drawable.ysdry, options);
// 再设置为false,最后要返回bitmap
options.inJustDecodeBounds = false;
// 根据缩放比例重新计算宽高
options.inDensity = options.outWidth;
options.inTargetDensity = width;
return BitmapFactory.decodeResource(getResources(), R.drawable.ysdry, options);
}
class CustomGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
// 预按下事件(父view是滑动控件会出现预按下事件)
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
/**
* 图片放大后跟随手指滑动偏移
*
* @param downEvent 按下的事件
* @param event 当前事件
* @param distanceX 上一个点到当前点之间距离
* @param distanceY 上一个点到当前点之间距离
* @return 没用
*/
@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent event, float distanceX, float distanceY) {
// 大图的时候移动,设置边界
if (big) {
offsetX -= distanceX;
// (图片宽度 - 屏幕宽度)/ 2
offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
offsetY -= distanceY;
offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
}
invalidate();
return false;
}
// 长按事件
@Override
public void onLongPress(MotionEvent e) {
}
// 惯性滑动,实现fling效果 velocity(速度:抬手一瞬间的 距离/时间 )
@SuppressLint("NewApi")
@Override
public boolean onFling(MotionEvent downEvent, MotionEvent event, float velocityX, float velocityY) {
if (big) {
// 不会自动刷新,只是设置好了overScroller,可通过overScroller计算和设置动画
overScroller.fling(
// 初始位置(原点)
(int) offsetX, (int) offsetY,
// velocity(速度:抬手一瞬间的 距离/时间)
(int) velocityX, (int) velocityY,
// x最大最小范围
-(int) (bitmap.getWidth() * bigScale - getWidth()) / 2, (int) (bitmap.getWidth() * bigScale - getWidth()) / 2,
// y最大最小范围
-(int) (bitmap.getHeight() * bigScale - getHeight()) / 2, (int) (bitmap.getHeight() * bigScale - getHeight()) / 2,
// 过渡滚动(类似IOS效果)
100, 100
);
/*
我们需要一个循环,不停的修改offsetX,offsetY进行刷新页面
post()在主线程立即执行,postOnAnimation()在下一帧去主线程执行
需要做动画,需要一帧一帧执行的时候用postOnAnimation()
postOnAnimation作用:下一帧的时候执行run()方法
*/
postOnAnimation(ScalableImageView.this);
}
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
// 双击事件(两次触摸间隔<300ms)
@Override
public boolean onDoubleTap(MotionEvent e) {
big = !big;
// invalidate();
if (big) {
// 根据手指点击的位置进行偏移量计算,根据手指位置方法
offsetX = (e.getX() - getWidth() / 2f) - (e.getX() - getWidth() / 2) * bigScale / smallScale;
offsetY = (e.getY() - getHeight() / 2f) - (e.getY() - getHeight() / 2) * bigScale / smallScale;
getScaleAnimator().start();
} else {
getScaleAnimator().reverse();
}
// 这里返回无所谓,只要onDown返回true即可
return false;
}
// 双击不松手滑动触发
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
}
}
想实现缩放,添加缩放手势监听