《Android 开发艺术探索》笔记——(3)View 的事件体系
2016-05-09 16:12
513 查看
View 基础知识
View 是 Android 中所有控件的基类,ViewGroup 也继承了 View。Android 中,x 轴和 y 轴的正方向分别为右和下。
位置参数(相对于父容器):
(left , top ): View 左上角原始坐标(right, bottom): View 右下角原始坐标
(x , y ): View 左上角最终坐标
translationX: View 左上角横向偏移量
translationY: View 左上角纵向偏移量
x = left + translationX
y = top + translationY (setX/Y() 时其实就是改变 translationX/Y 的值)
width = right - left
height = bottom - top
MotionEvent 和 TouchSlop
MotionEvent
典型事件:ACTION_DOWN, ACTION_MOVE,ACTION_UP意思也很容易理解,分别是落,动,起
一次触摸会触发一系列事件:
点击屏幕后离开松开:DOWN -> UP
点击屏幕滑动再松开:DOWN -> MOVE ->…-> MOVE -> UP
通过 MotionEvent 获得点击事件的坐标:
getX / getY : 相对于当前 View 左上角的 x 和 y 坐标
getRawX / getRawY:相对于手机屏幕左上角的 x 和 y 坐标
(View.getX / getY 获得的是相对于父容器的 x 和 y 坐标)
TouchSlop
滑动的最小距离,若没达到,则不认为是滑动,默认 8dp。VelocityTracker、GestureDetector 和 Scroller
VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度。在 View 的 onTouchEvent 方法中:
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000);// 1000ms内划过的像素数 int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity();
回收:
velocityTracker.clear(); velocityTracker.recycle();
GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。一般监听滑动相关,在 onTouchEvent 中自己实现,若是监听双击,则使用 GestureDetector。
Scroller
弹性滑动对象,用于实现 View 的弹性滑动,即有过过渡效果的滑动,与 View 的 computeScroll 方法配合使用。(下面会详细介绍)Viewd 的滑动
三种方式:
scrollTo/scrollBy操作简单,适合对 View 内容的滑动
典型应用:ScrollView 的滑动
动画
操作简单,适合没有交互的 View 和实现复杂的动画效果
改变 LayoutParams
操作稍复杂,适合有交互的 View
弹性滑动
Scroller
典型用法:private Scroller mScroller = new Scroller(context); public void smoothScrollTo(int destX, int destY) { int scrollX = getScrollX(); int delta = destX - scrollX; mScroller.forceFinished(true); mScroller.startScroll(scrollX, 0, delta, 0, 5000); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollBy(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
当我们调用 startScroll() 时,Scroller 内部其实什么也没做,只是保存了我们传递的几个参数。所以仅仅调用 startScroll() 是不会产生滑动的,因为它内部没有做与滑动相关的事,Scroller 产生滑动,其实是因为 invalidate()。
invalidate() 会导致 View 重绘,在 View 的 Draw 方法中会调用 computeScroll(),computeScroll() 在 View 中是一个空实现,所以我们要自己实现,正如上所示。
过程:
invalidate() –>
View 重绘, draw() –>
computeScroll() ==>
scrollTo() –>
postInvalidate() 再次重绘 –>
…
computeScrollOffset() 会根据时间流逝算出当前的 ScrollX/Y,返回 true 即表示滑动还没有结束,继续滑动。
动画
动画本身就是一种渐进的过程,所以利用动画天然就具有弹性效果。另外通过动画,也可以实现类似 Scroller 对 View 的弹性滑动。
使用延时策略
核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。View 的事件分发机制
优先级:onTouchListener > onTouchEvent > onClickListener
传递过程:
Activity –> Window –> View
事件总是先传给 Activity,Activity 再传给 Window,Window 再传给顶级 View,接着顶级 View 就按照事件分发机制去分发事件。如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 就会被调用,若全部 View 都不处理这个事件,那这个事件将最终传递给 Activity 来处理,即 Activity 的 onTouchEvent 会被调用。
View 的滑动冲突
常见的三种滑动冲突场景:外部滑动与内部滑动的方向不一致
外部滑动与内部滑动的方向一致
以上两种的嵌套
滑动冲突的处理规则
对场景一:根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。由滑动轨迹的起终点的坐标即可判断为水平还是竖直(距离差、角度、速度差等)。对场景二:根据业务需求具体分析。
对场景三:以上两者的混合。
滑动冲突的解决方式
外部拦截法
指事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。需重写父容器的 onInterceptTouchEvent 方法。伪代码:
public boolean onInterceptTouchEvent(MotionEvent event){ boolean intercepted = false; switch(event.getAction()){ case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if(父容器需要当前点击事件){ intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } return intercepted; }
对于 ACTION_DOWN 事件,父容器必须返回 false,因为一旦拦截了 ACTION_DOWN,后续的 ACTION_MOVE,ACTION_UP 都会交给父容器处理。
对于 ACTION_UP 事件,父容器也返回 false,一旦拦截,子元素的 onClick 事件便无法触发。父容器(ViewGroup)比较特殊,一旦开始拦截(ACTION_DOWN 的 onTouchEvent 返回 true),那么后续事件也都会交给它处理(并且 onInterceptTouchEvent 不会再被调用),而 ACTION_UP 作为最后一个事件也一定能够传到父容器(因为 onInterceptTouchEvent 不会被调用,所以返回 false 也就不起作用)。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,需要配合 requestDisallowInterceptTouchEvent 方法来设置(控制 FLAG_DISALLOW_INTERCEPT 标志位,设置后 ViewGroup 将无法拦截除了 ACTION_DOWN 以外的点击事件,ACTION_DOWN 不受此标志位约束),需重写子元素的 dispatchTouchEvent 方法(子元素,view 无 onInterceptTouchEvent 方法)。子元素:
public boolean dispatchTouchEvent(MotionEvent event){ switch(event.getAction()){ case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); case MotionEvent.ACTION_MOVE: if(父容器需要当前点击事件){ parent.requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } return super.dispatchTouchEvent(event); }
因为 ACTION_DOWN 不受 requestDisallowInterceptTouchEvent 方法影响,父元素也要做相应处理:
public boolean onInterceptTouchEvent(MotionEvent event){ int action = event.getAction(); if (action == ACTION_DOWN){ return false; } else { return true; } }
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories