Android应用开发之自定义View触摸相关工具类全解

jopen 8年前

背景

最近有些乱,各种事情,各种交叉。好在还有一点上进心,于是继续将自定义这个系列的核心知识再梳理一下吧。关于自定义控件前面博文说过了,这里不会教你拿来主义,只授之以渔,如果你喜欢拿来主义,不好意思,请绕行,如果你喜欢得渔,那请继续。

前面我们已经叙述过了几篇关于自定义View涉及的东西,大家可以自己回过头去看我之前的博客,譬如事件处理、坐标系、工具类等。下面我们还是继续补充一些常用的自定义控件工具类。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

ViewConfiguration基础参数工具类

ViewConfiguration这个类主要提供了一些自定义控件用到的标准常量,譬如尺寸、滑动距离、敏感度等,当我们自定义控件时就可以直接使用他来避免自己做一些测试。下面是该类的源码注视,使用时直接可以参考,没啥特殊的逻辑东西,所以不再进行源码分析。如下:

public class ViewConfiguration {      ......      //不推荐使用,推荐ViewConfiguration.get(Context)获取实例      public ViewConfiguration() {}      public static ViewConfiguration get(Context context) {}      //不推荐使用,推荐getScaledScrollBarSize()代替;获取水平滚动条的宽或垂直滚动条的高      public static int getScrollBarSize() {}      public int getScaledScrollBarSize() {}      //滚动条褪去消失的持续时间      public static int getScrollBarFadeDuration() {}      //滚动条消失的延迟时间      public static int getScrollDefaultDelay() {}      //不推荐使用,推荐getScaledFadingEdgeLength()代替;褪去边缘的长度      public static int getFadingEdgeLength() {}      public int getScaledFadingEdgeLength() {}      //按下的持续时间长度      public static int getPressedStateDuration() {}      //按住状态转变为长按状态需要的时间      public static int getLongPressTimeout() {}      //重新按键判断时间      public static int getKeyRepeatTimeout() {}      //重复按键延迟的时间      public static int getKeyRepeatDelay() {}      //判断是单击还是滚动的时间,在这个时间内没有移动则是单击,否则是滚动      public static int getTapTimeout() {}      //在这个时间内没有完成这个点击,那么就认为是一个点击事件      public static int getJumpTapTimeout() {}      //得到双击间隔时间,在这个时间内是双击,否则是单击      public static int getDoubleTapTimeout() {}      //不推荐使用,推荐getScaledEdgeSlop()代替;判断是否滑动事件      public static int getEdgeSlop() {}      public int getScaledEdgeSlop() {}      //不推荐使用,推荐getScaledTouchSlop()代替;滑动的时候,手的移动要大于这个距离才算移动      public static int getTouchSlop() {}      public int getScaledTouchSlop() {}      //触摸边沿padding区域的判断      public int getScaledPagingTouchSlop() {}      //不推荐使用,推荐getScaledDoubleTapSlop()代替;判断是否双击的阈值      public static int getDoubleTapSlop() {}      public int getScaledDoubleTapSlop() {}      //不推荐使用,推荐getScaledWindowTouchSlop()代替;触摸窗体边沿区域判断      public static int getWindowTouchSlop() {}      public int getScaledWindowTouchSlop() {}      //不推荐使用,推荐getScaledMinimumFlingVelocity()代替;得到滑动的最小速度, 以像素/每秒来进行计算      public static int getMinimumFlingVelocity() {}      public int getScaledMinimumFlingVelocity() {}      //不推荐使用,推荐getScaledMaximumFlingVelocity()代替;得到滑动的最大速度, 以像素/每秒来进行计算      public static int getMaximumFlingVelocity() {}      public int getScaledMaximumFlingVelocity() {}      //不推荐使用,推荐getScaledMaximumDrawingCacheSize()代替;获取最大的图形可缓存大小,单位bytes      public static int getMaximumDrawingCacheSize() {}      public int getScaledMaximumDrawingCacheSize() {}      ......  }

有了上面这个工具类,我们在自定义控件处理滑动手势等判断时就可以很方便的判断出临界值等问题,不用我们再去自己测试定义一个近似的值来代替。

特别注意: ViewConfiguration还有一个在support包中的兼容类ViewConfigurationCompat,使用时请注意一下。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

Scroller加强版OverScroller回弹工具类

之前有篇博客说到了Scroller的源码浅析,其实Scroller在API 1就出现了,而这里要说的Scroller加强版OverScroller在API 9才出现,所以功能指定比之前的Scroller强大,支持了回弹效果(关于不同的回弹效果我们可以自定义不同的动画插值器即可),不过原理基本和之前分析的Scroller源码一样,所以这里我们不会再对OverScroller源码分析,只对他和Scroller的差异进行说明,下面我们来看看。

OverScroller在Scroller类基础上多出来的方法:

方法 含义
isOverScrolled() 返回当前的位置是否有效或者是否超出滚动边界。
springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) 当你想回滚的时候调用这个方法,回滚的范围在有效的坐标范围内。
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) 同Scroller的,只是最后两个参数含义为fling滚动超过有效值的范围。
notifyHorizontalEdgeReached(int startX, int finalX, int overX) 通知水平滚动是否到达边界。
notifyVerticalEdgeReached(int startY, int finalY, int overY) 同上。

关于Scroller的基本使用流程可以参见我之前博客Scroller源码浅析和ViewDragHelper源码浅析两篇文章,如果需要深入理解可以看看官方ScrollView的实现,其就完全使用了OverScroller。

特别注意: Scroller(OverScroller)这货也有一个在support兼容包的兼容类ScrollerCompat,使用时请留意一下。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

VelocityTracker手势速率工具类

VelocityTracker主要用跟踪触摸屏事件(Flinging及其他Gestures手势事件等)的速率。我们在拿到实例后可以通过computeCurrentVelocity(int)来初始化速率的单位,然后接着通过addMovement(MotionEvent)方法将MotionEvent加入VelocityTracker实例中,然后在需要的地方通过getXVelocity() 或getXVelocity()获得横向和竖向的速率即可。

下面给出相关的API说明(VelocityTracker许多方法都是native实现的):

public final class VelocityTracker {      //获取VelocityTracker实例      static public VelocityTracker obtain() {}      public static VelocityTracker obtain(String strategy) {}      //回收后代表你不需要使用了,系统将此对象在此分配到其他请求者      public void recycle() {}      //清空回到初始状态,computeCurrentVelocity都被reset了      public void clear() {}          //将事件加入到VelocityTracker类实例中      public void addMovement(MotionEvent event) {}      //unitis表示速率的基本时间单位,1表示一毫秒时间单位内运动了多少个像素      public void computeCurrentVelocity(int units) {}      //同上,floatVelocity表示速率的最大值,超过最大值的都返回最大值      public void computeCurrentVelocity(int units, float maxVelocity) {}          //获取xy方向速率      public float getXVelocity() {}      public float getYVelocity() {}          //获取xy速率,id为event的pointid      public float getXVelocity(int id) {}          public float getYVelocity(int id) {}  }

有了上面这些手势速率的检测工具类,下面我们来看下他的一些通用模板:

VelocityTracker mVelocityTracker = null;    @Override      public boolean onTouchEvent(MotionEvent event){          int action = event.getAction();          switch(action){          case MotionEvent.ACTION_DOWN:              if(mVelocityTracker == null){              mVelocityTracker = VelocityTracker.obtain();              }else{              mVelocityTracker.clear();              }              mVelocityTracker.addMovement(event);              break;          case MotionEvent.ACTION_MOVE:              mVelocityTracker.addMovement(event);              mVelocityTracker.computeCurrentVelocity(1000);             Log.i("X = "+mVelocityTracker.getXVelocity());              Log.i("Y = "+mVelocityTracker.getYVelocity());              break;          case MotionEvent.ACTION_UP:          case MotionEvent.ACTION_CANCEL:              mVelocityTracker.recycle();              break;          }          return true;      } 

关于速率检测类的知识就介绍到这里,没啥新鲜的。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

GestureDetector手势工具类

除了我们通过onTouchEvent()自己处理一堆复杂的手势以外,其实Android给我们提供了现成的便捷方式,那就是GestureDetector手势监听类,如下:

public class GestureDetector {      public interface OnGestureListener {      //ACTION_DOWN时触发           boolean onDown(MotionEvent e);      //ACTION_DOWN了过一会还没有滑动时触发,onDown->onShowPress->onLongPress          void onShowPress(MotionEvent e);      //ACTION_DOWN后没有滑动(onScroll)且没有长按(onLongPress)接着ACTION_UP时触发          boolean onSingleTapUp(MotionEvent e);      //滑动时实时触发          boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);      //ACTION_DOWN长按时触发          void onLongPress(MotionEvent e);      //触摸滑动一定距离后松手ACTION_UP时触发,后参数为速率          boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);      }        public interface OnDoubleTapListener {      //ACTION_DOWN后没有滑动(onScroll)且没有长按(onLongPress)接着ACTION_UP时触发          boolean onSingleTapConfirmed(MotionEvent e);      //双击的第二下ACTION_DOWN时触发           boolean onDoubleTap(MotionEvent e);      //双击的第二下ACTION_DOWN和ACTION_UP都会触发,e.getAction()区别          boolean onDoubleTapEvent(MotionEvent e);      }        public interface OnContextClickListener {      //context点击触发,与View#onGenericMotionEvent(MotionEvent)相关,不常用          boolean onContextClick(MotionEvent e);      }        public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener, OnContextClickListener {      ......      //OnGestureListener、OnDoubleTapListener、OnContextClickListener所有接口的默认实现      ......      }        //各种推荐的不推荐的构造方法      @Deprecated      public GestureDetector(OnGestureListener listener, Handler handler) {}      @Deprecated      public GestureDetector(OnGestureListener listener) {}      public GestureDetector(Context context, OnGestureListener listener) {}      public GestureDetector(Context context, OnGestureListener listener, Handler handler) {}      public GestureDetector(Context context, OnGestureListener listener, Handler handler,              boolean unused) {}      //其他两类回调接口的设置,OnGestureListener必须在构造中就处理掉      public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {}      public void setContextClickListener(OnContextClickListener onContextClickListener) {}      //一些处理判断方法      public void setIsLongpressEnabled(boolean isLongpressEnabled) {}      public boolean isLongpressEnabled() {}      public boolean onTouchEvent(MotionEvent ev) {}      public boolean onGenericMotionEvent(MotionEvent ev) {}  }

有了上面这些GestureDetector手势工具类的基本API介绍之后我们就可以各种使用了,没啥特殊的介绍。

特别注意: 其实手势相关的东西还有Gesture类等GestureOverlayView手势创建识别类的,这里不作介绍,作为拓展。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

View及ViewGroup触摸事件总结

关于View触摸屏事件的传递机制源码分析其实可以参考我前面写的博客,这篇文章既然是总结,那就是只给出结论,相关分析请看前面的博文。

触摸事件传递源Activity:

Activity的dispatchTouchEvent()方法将事件传递给它的根布局ViewGroup(即调用根布局ViewGroup的dispatchTouchEvent()方法,该方法会对事件进行如下情况处理:

  • 如果根布局ViewGroup及其内部子布局控件均没处理(此时根布局ViewGroup的dispatchTouchEvent()方法返回false)则调用Activity自己的onTouchEvent()方法;如果Activity自己的onTouchEvent()方法仍然没有处理(返回false)则该事件处理宣告结束。

  • 如果根布局ViewGroup的dispatchTouchEvent()方法返回true则表明根布局中处理了这一次事件,此时就不会再调用Activity的onTouchEvent()方法了(因为Activity没有父控件且不能设置触摸监听OnTouchListener,所以没有onInterceptTouchEvent()方法)。

触摸事件传递View级别处理:

这里所谓的View级别泛指其内部不包含子控件(已经为最小控件单位)的View,当该View的父级ViewGroup触发该View的dispatchTouchEvent()方法时,由于该View没有子控件可以被继续派发,所以事件只能自己调度自己相关方法。测试的调度如下:

  • 如果该View注册了OnTouchListener,则优先调用OnTouchListener的onTouch()方法,如果onTouch()方法返回false则继续调运该View的onTouchEvent()方法,如果onTouch()方法返回true则该View的dispatchTouchEvent()方法直接返回true。

  • 如果该View没有注册OnTouchListener则直接调用该View的onTouchEvent()方法,该方法返回true、false决定了该View的dispatchTouchEvent()方法返回值。

触摸事件传递ViewGroup级别处理:

ViewGroup的dispatchTouchEvent()方法被其父布局 (父ViewGroup或者Activity)调用,当前ViewGroup的dispatchTouchEvent()方法主要任务就是为子控件派发事件(调运子控件的dispatchTouchEvent()),同时向父级布局返回事件处理情况。

不过在当前ViewGroup的dispatchTouchEvent()方法向子控件派发事件之前我们在当前ViewGroup里是可以通过自己的onInterceptTouchEvent()方法来决定触摸事件是否拦截(当前ViewGroup的onInterceptTouchEvent()返回true则不再传递给自己的子控件,而是当前ViewGroup自己处理,接着将处理结果告诉父控件;返回false则不拦截(继续传递给子控件,如果子控件的dispatchTouchEvent()方法都返回false则ViewGroup就尝试自己处理事件,然后告诉父布局自己处理的结果)。

一次完整的事件流程处理:

综合从Activity到根ViewGroup到中间ViewGroup,再到View的事件处理流程,我们要注意其传递过程中的下面几点:

  • 一次完整的事件触发可以分为ACTION_DOWN->[ACTION_MOVE]->ACTION_UP。当我们手指按下派发ACTION_DOWN事件时,如果我们当前层级的View或者ViewGroup的onTouch()或onTouchEvent()方法返回false,则当前层级的View或者ViewGroup的onTouch()或onTouchEvent()方法就再也接收不到其他事件了,直到下次新的触摸事件(ACTION_DOWN)开始。

  • 在一次完整的事件传递(ACTION_DOWN->[ACTION_MOVE]->ACTION_UP)过程中只要当前ViewGroup的onInterceptTouchEvent()方法有一次返回true则当前ViewGroup将会拦截这次事件传递的全部后续触发事件,同时这些后续触发事件都不会再触发当前ViewGroup的onInterceptTouchEvent()方法(直到下次ACTION_DOWN来临),同时向之前处理事件的子布局传递一个ACTION_CANCEL事件。如果当前ViewGroup的onInterceptTouchEvent()方法返回false,则本次传递的每个事件来临时都会触发当前ViewGroup的onInterceptTouchEvent()方法。

  • 只有ViewGroup才有onInterceptTouchEvent()方法,因为最小单位的View不具备再往下派发事件的能力,它只会直接调用自己的onTouch()和onTouchEvent()方法。

  • 当父ViewGroup截获了当前传递事件,常理来说其内部的子布局View或者ViewGroup就不能够再收到派发事件了;但是我们有一种方法可以阻止父ViewGroup截获传递的事件(getParent().requestDisallowInterceptTouchEvent(true);),一旦子布局View或者ViewGroup收到触摸事件后调用这个方法则父ViewGroup就不会再调用她自己的onInterceptTouchEvent()方法了,直到事件处理完毕再getParent().requestDisallowInterceptTouchEvent(false);即可。

到此View的触摸事件传递也就总结完成了,使用中牢记这些准则即可,当然也推荐查看源码。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

总结

可以看见,关于自定义控件的基础触摸相关的东西其实差不多也就这么多了,有了上面这些玩意你也基本上就能够玩转Android自定义控件触摸相关的蛋疼处理了,不用再苦苦思索了。

这一篇博文没有附带任何例子,因为是一个总结性的文章,相关总结到的东西在我前面的博文中基本都有深入的分析文章,所以如果你感兴趣可以翻翻我之前的博文,谢谢喏。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

这里写图片描述

来自: http://blog.csdn.net/yanbober/article/details/50411919