layout: post
date: 2016-01-08
title: Android开发艺术探索-第三章-View的事件体系
categories: blog
tags: [Activity,Android,View,MotionEvent,TouchSlop]
category: Android
description:
本文首发于个人博客KuTear,转载引用请注明原出处.谢谢!
另外,更多文章分享请查看博客KuTear
3.1 View的基础知识
-
位置参数
top、left、right、bottom,在3.0之后增加了x、y、translationX、translationY.这里的所有参数都是相对其父布局来说的.
下面是具体的含义表示其中参数的关系为
x = left + translationX y = top + translationY
-
MontionEvent和TouchSlop
MontionEvent代表着触摸事件封装的数据,包括常用的Action和位置参数等.如上面图示,注意函数getRaw*()是相对与屏幕的.
TouchSlop表示滑动的最小常量.是常量(int),不是具体的类.获取方式为:ViewConfiguration.get(getContext()).getScaledTouchSlop()
-
VelocityTracker,GestureDetector和Scroller
VelocityTracker用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。
速度计算公式:速度 = (终点位置 - 起点位置) / 时间段
速度可能为负值,例如当手指从屏幕右边往左边滑动的时候。此外,速度是单位时间内移动的像素数,单位时间不一定是1秒钟,可以使用方法
computeCurrentVelocity(xxx)指定单位时间是多少,单位是ms。例如通过computeCurrentVelocity(1000)来获取速度,手指在1s中
滑动了100个像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)来获取速度,在100ms内手指只是滑动了
10个像素,那么速度是10,即10(像素/100ms)。
VelocityTracker的使用方式://初始化 VelocityTracker mVelocityTracker = VelocityTracker.obtain(); //在onTouchEvent方法中 mVelocityTracker.addMovement(event); //获取速度 mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); //重置和回收 mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用 mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
GestureDetector用于辅助检测用户的单击、滑动、长按、双击等行为。GestureDetector的使用比较简单,主要也是辅助检测常见的触屏事件。
作者建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。//自定义的View,实现相关接口(onGestureListener,onDoubleTabListener) GestureDetector mGestureDetector = new GestureDetector(this/*context*/,listener/*onGestureListener*/); //function onTouchEvent(...)或onTouchListener的onTouch(...)中,直接返回 return mGestureDetector.onTouchEvent(event)
更多使用参见[参考2]
3.2 View的滑动
-
layout
public void layout (int l, int t, int r, int b)
参数都是相对与父布局.
@Override public boolean onTouchEvent(MotionEvent event) { int rawX = (int) (event.getRawX()); //相对与屏幕的坐标 int rawY = (int) (event.getRawY()); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 记录触摸点坐标 lastX = rawX; lastY = rawY; break; case MotionEvent.ACTION_MOVE: // 计算偏移量 int offsetX = rawX - lastX; int offsetY = rawY - lastY; // 在当前left、top、right、bottom的基础上加上偏移量 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); // 重新设置初始坐标 lastX = rawX; lastY = rawY; break; } return true; }
-
offsetLeftAndRight和offsetTopAndBottom
使用方法同上几乎一致
//直接在onTouchEvent中调用,替换上面的layout(...)部分 offsetLeftAndRight(offestX); offsetTopAndBottom(offestY);
-
LayoutParams
这个方式在平时开发中应该使用的比较多.使用也是很简单,就是修改params的某些参数
//ViewGroup.MarginLayoutParams layoutParams = // (ViewGroup.MarginLayoutParams) getLayoutParams(); //LinearLayout.LayoutParams extends ViewGroup.MarginLayoutParams, //几乎所有的LayoutParms都是继承至 //ViewGroup.MarginLayoutParams, //所以ViewGroup.MarginLayoutParams是通用的... LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); //requestLayout();效果和上面这一句一样
-
动画
动画部分在Android群英传-第七章 Android动画机制与使用技巧中已经有比较详细的说明,在这里就不做说明.
-
ViewDragHelper
ViewDragHelper的使用过程其实也是比较简单的,主要用户控制部分都在Callback中.CallBack中的函数比较多
下面是一个简单的栗子:
//初始化 mDragHelper = ViewDragHelper.create(this/*要处理的ViewGroup*/, 1.0f/*敏感度*/, new DragHelperCallback()/*前面说的Callback*/); //复写一些函数,代码几乎固定 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mDragHelper.cancel(); return false; } return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { mDragHelper.processTouchEvent(ev); return true; }
这里没有详细写出CallBack的代码,可以在这里查看.
-
ScrollTo和ScrollBy
根据函数名称就知道这两个函数的区别,To是到具体的点,by只是与当前的偏移.
这两个函数不是针对view本身,而是针对其内容,具体来说就是ViewGroup调用这两函数,是其内部的view在移动,view调用是其内容在动(TextView-->文本,ImageView-->图像)
另一方面就是他的参数不同与其他,正数X往左,正数Y往上.原因查看这里,
如果想要移动View,就需要在她的parent上调用这函数,下面是个栗子//替换上文onTouchEvent中的layout(...) ((ViewGroup) getParent()).scrollBy(-offsetX, -offsetY);
-
Scroller
在以前都不知道有这个类,哎,基础不够诶.下面一个栗子说明
//初始化,还可以使用插值器 Scroller mScroller = new Scroller(mContext,interpolator/*插值器,可以不用*/); //View的computescroll() @Override public void computeScroll() { super.computeScroll(); // 判断Scroller是否执行完毕 if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY()); // 通过重绘来不断调用computeScroll invalidate();//很重要 } } //启动 mScroller.startScroll(startX,startY,dX,dY,duration);
本质上Scroller不能移动View,在我看来她同属性动画中的ValueAnimator是一样的,因为他们都只是按照某种插值器产生数值,需要自己把数值同移动
相联系.
3.3 View的事件分发机制
-
事件分发过程的三个重要方法
-
dispatchTouchEvent
函数原型
public boolean dispatchTouchEvent(MotionEvent ev)
主要的功能是负责事件的分发.
返回值:
true: 表示向下分发中断
false: 表示继续向下分发 -
onInterceptTouchEvent
函数原型
public boolean onInterceptTouchEvent(MotionEvent event)
主要功能是负责事件的拦截
返回值:
true:拦截,事件交由自己(View/ViewGroup)的onTouchEvent(...)处理
false:不拦截,事件继续向下分发. -
onTouchEvent
函数原型
public boolean onTouchEvent(MotionEvent event)
主要功能是处理触摸事件
返回值:
true:表示消费了这个事件.
false:表示没有消费该事件,返回到上级处理.如果一直得不到处理,最终反馈到Activity的onTouchEvent(...)
-
-
函数之间的逻辑关系
-
以上三个函数的伪代码
类似于递归调用的方式
public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
-
函数与监听接口
在通常情况下,我们为Button等组件设置了onClickListener接口,有时也会设置onTouchListener接口,但在什么时候接口中的方法才会执行呢?如果设置了onTouchListener接口监听,会对View(ViewGroup)的onTouchEvent有一定的影响.如果设置了onTouchListener,她的onTouch的返回值会影响view中onTouchEvent的调用与否,onTouch返回值的含义与onTouchEvent一样,表示是否消费了该事件.onTouch会先于onTouchEvent执行.伪代码为
//true表示消费掉 if(!listener.onTouch(ev)){ onTouchEvent(ev); }
对于onClickListener接口,他内部方法onCLick的调用是在onTouchEvent中(根据上面就知道如果在onTouchListener的onTouch中返回true,onclick就不会再执行了),其内部部分代码如下.
//View#onTouchEvent(...) if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } //点击事件的处理者 private final class PerformClick implements Runnable { @Override public void run() { performClick(); } } //点击调用onClick函数 public boolean performClick() { //ListenerInfo封装了各种监听 final ListenerInfo li = mListenerInfo; if (...) { //调用部分 li.mOnClickListener.onClick(this); result = true; } ... return result; }
根据上面的描述,知道调用顺序为onTouchListener#onTouch,返回值决定是否继续执行view的onTouchEvent,最后在onTouchEvent中执行onClickListener的onClick方法.
-
-
分发过程
-
Activity分发
触摸事件最先到达Activity,所以首先会在Activity中分发
//Activity#dispatchTouchEvent() public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //分发到Window. if (getWindow().superDispatchTouchEvent(ev)) { //true表示不再向下分发 return true; } return onTouchEvent(ev); }
在getWindow()中返回mWindow,最终在函数attach(...)中发现
mWindow = new PhoneWindow(this);
PhoneWindow不在SDK中,在在线源码(Android源码)网站上可以找到相关的代码
public boolean superDispatchTouchEvent(MotionEvent event ) { //DecorView extends FrameLayout // DecorView#superDispatchTouchEvent(ev) // public boolean superDispatchTouchEvent(MotionEvent event) { // //来到了ViewGroup // return super.dispatchTouchEvent(event); // } return mDecorView.superDispatchTouchEvent(event); }
由此就把事件分发到了ViewGroup,接下来就是在VieGroup中分发.
-
-
View分发
函数dispatchTouchEvent(...)中的部分代码
... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // result==true,函数onTouchEvent(...)就执行不到了,而影想result的主要就是 //li.mOnTouchListener.onTouch(this, // event)的返回值,返回true, //表示事件被处理了,自然不需要在调用onTouchEvent(...)来重新处理 // 前面说过onClick(...)是在onTouchEvent(...)中调用的.即优先级小于onTouch() if (!result && onTouchEvent(event)) { result = true; } } ...
函数onTouchEvent(...)主要就是处理事件,前面已经说过onClick的执行过程了.这里就不说了.
-
ViewGroup分发
函数dispatchTouchEvent(...)中的部分代码
// Check for interception. final boolean intercepted; // 事件为ACTION_DOWN或者mFirstTouchTarget不为null //(即已经找到能够接收touch事件的目标组件)时if成立 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //判断disallowIntercept(禁止拦截)标志位 //因为在其他地方可能调用了 //requestDisallowInterceptTouchEvent(boolean disallowIntercept) //从而禁止执行是否需要拦截的判断 //(有点拗口~其实看requestDisallowInterceptTouchEvent()方法名就可明白) final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //补充:根据下面的代码可以发现, disallowIntercept 的值等于函数 //requestDisallowInterceptTouchEvent的参数. if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
注意上文代码中的注释部分,这里看一下部分requesrDisallowInterceptTouchEvent(...)的部分源码
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { //更具这里可以看出,当disallowIntercept=true时, //(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 成立, //这就意味着上面一段代码中的disallowIntercept=true; if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } ... }
由此可见VIewGroup只会在ACTION=ACTION_DOWN或者mFirstTouchTarget != null时才判断是否拦截事件,因为一个事件序列(DOWN->MOVE->...->UP)只能有一个View处理.但是mFirstTouchTarget != null表示什么呢?
当事件被ViewGroup的子元素成功处理了(子View的onTouchEvent/onTouch返回了true??),mFirstTouchTarget被赋值指向子元素(即!=null)
函数dispatchTouchEvent(...)的部分实现.
final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. // 找到接收Touch事件的子View!!!!!!!即为newTouchTarget. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //注意这个方法,再后面再看看..根据源码, //可以知道它返回的是子View(child)的dispatchTouchEvent(...) //当child==null,返回super.dispatchTouchEvent(...), //即View的dispatchTouchEvent(...) if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //找到了事件的处理者,终止循环 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); }
同样是dispatchTouchEvent(...)的部分代码
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//这里说明没有子View处理该事件,只得有View的dispatchTouchEvent(...)来处理.
//关于该函数的部分源码在后面介绍.
handled = dispatchTransformedTouchEvent(ev, canceled, null/*child*/,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
函数addTouchTarget(...)的具体实现.
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
函数dispatchTransformedTouchEvent(...)的部分实现.
....
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
....
return handled.
3.4 View的滑动冲突
-
常见的滑动冲突的场景:
- 外部滑动方向和内部滑动方向不一致,例如viewpager中包含listview;
- 外部滑动方向和内部滑动方向一致,例如viewpager的单页中存在可以滑动的bannerview;
- 上面两种情况的嵌套,例如viewpager的单个页面中包含了bannerview和listview。
-
滑动冲突处理规则
可以根据滑动距离和水平方向形成的夹角;或者根绝水平和竖直方向滑动的距离差;或者两个方向上的速度差等
-
解决方式
-
外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,其他均不需要做修改。伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
-
内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。
public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { getParent().requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) { getParent().requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }
父View的onInterceptTouchEvent(...)伪代码
public boolean onInterceptTouchEvent(MotionEvent ev){ if(ev.getAction() == MotionEvent.ACTION_DOWN){ retuen false; }else{ retuen true; } }
内部拦截法过程说明,父类在ACTION_DOWN时不拦截,子类在ACTION_DOWN时拦截,这时mFirstTouchTarget!=null, disallowIntercept = true,这意味着父类的onInterceptTouchEvent(...)不会再被执行,并且一个事件序列只有一个View来处理,则所有的后续ACTION_MOVE都会传到子View,当在子View中判断到某个事件应该由父View处理,只需重置disallowIntercept=false即可,即调用函数requestDisallowInterceptTouchEvent(false),这时事件就到父View的onTouchEvent(...)处理的(因为onInterceptionTouchEvent在非ACTION_DOWN时都返回true).如果父类没有在设置requestDisallowInterceptTouchEvent(true)的话,这个事件就会一直都在父View中做处理了.(注:为个人理解,若有不对,望其指出)
-
参考
Art of Android Development Reading Notes 3
用户手势检测-GestureDetector使用详解
ViewDragHelper详解
Android触摸屏事件派发机制详解与源码分析一(View篇)