android 自定义View学习一之onMeasure和onLayout
这里我们开始第二篇学习:android的事件分发
本文虽然有不少源码,主要看注释的地方就行,一些细节可以忽略,暂时没找到如何让注释高亮的方法,导致有些源码看起来不太友善。
我们这里主要是讲单点触摸:
关键点:move事件会多次触发
为什么这么说呢?我们往下看,等会会解开这个疑惑?
一般情况下的分发逻辑:
我们在屏幕上点击一下屏幕,首先是activity的dispatchTouchEvent先响应:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//这是一个空方法,不用理会
onUserInteraction();
}
//android里面window只有一个实现就是PhoneWindow
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
PhoneWindow的dispaTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//mdecor就是我们的decorView
return mDecor.superDispatchTouchEvent(event);
}
decorView是继承自FrmaeLayout的,但是FrameLayout并没有实现dispaTouchEvent,所以直接到了ViewGroup里面去了。
class ViewGroup extends View ,但是Viewgroup并没有去重写它的onTouchEvent的,因为一般情况下它只负责分发事件。
我们对同一个View进行onClickView和onTouch监听:
btn_click.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick");
}
});
btn_click.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: " + event.getAction());
return true;
}
});
当onTouch返回为false的时候两者都打印了log,onTouch返回为true的时候onclick没打印。
我们直接到View的dispatchTouchEvent方法里面看:
我们是看一个正常流程,一些其他因素暂时抛开:关键代码是:
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
**//关键代码:看这里这个判断方法就行
//正常流程,第一个down事件进来,ENABLED肯定是正常的。
//所以决定这个if判断成功或者失败主要看li.mOnTouchListener.onTouch(this, event))**
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
mListenerInfo肯定不为空,因为我们实现了onTouch方法,如果你不实现result就是默认值fasle
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
@UnsupportedAppUsage
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
你只要实现了onTouch方法mListenerInfo就一定不为空
(mViewFlags & ENABLED_MASK) == ENABLED,这里我就不过多解释,从字面意思:ENABLED,是否激活,判断你这个是否可点击是否可见等,你是一个正常View这里就通过。
那么也就是这个result是false还是true完全是看你onTouch的返回值了。
//如果resutl是false才会进入onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
我们的点击事件是action_up,直接到:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
//直接找它
performClickInternal();
}
}
}
...
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
//点击播放音效
notifyAutofillManagerOnClick();
//跟进去
return performClick();
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
//我们上面说了li不过空
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//点击事件
li.mOnClickListener.onClick(this);
//直接给你返回true,所以onclik没有返回值
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
从上面的流程我们知道了为什么onTouch返回false的时候onclick才能执行
接下来进入我们的主菜,事件分发
当我一个布局ViewPager,里面是三个子界面Fragment,界面就是一个ListView。
Viewpager可以左右滑动,ListView可以上下滑动,我们知道即可以上下滑也可以左右滑。
但是当我继承了ViewPager,重写了:
public boolean onInterceptTouchEvent(MotionEvent event) {
return false;
}
这里我返回true或者false,都会有问题,要不抢了listView要不抢了viewPager的滑动事件,很明显,google帮我们处理了,这里我们就要学习如何自己处理。
当我们的手机点击屏幕的时候,事件最开始是在decorView进行分发,它是一个ViewGroup
所以我们直接找ViewGroup的dispatchTouchEvent
正常流程还是来到这个if判断:
//进入if
if (onFilterTouchEventForSecurity(ev)){
//如果是down事件,把之前的状态清零
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//mFirstTouchTarget 这个时候为空,什么时候赋值等会会说
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//第一次down进来 disallowIntercept:
//1、判断事件是否拦截,如果拦截intercepted == true
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
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;
}
}
我们先看第一种情况:拦截
拦截:相当于你就是最后一个View了,分发或者处理。
//如果是拦截,上面的if进不去,直接来到这里
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//进来这里,我们看一下dispatchTransformedTouchEvent
//第三个参数null,因为拦截不传给子View
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//看这里,调用它的super,也就是View,View刚才我们分析过了
//如果View的onTouch和onClick都不处理,则返回给Activity
handled = super.dispatchTouchEvent(event);
} else {
...
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
...
return handled;
}
所以为什么onInterecepTouchEnvet返回true的时候listView不能滑动了
下面我们继续跟进不拦截的情况:
//走一个正常流程,那么你的down的时候cancled肯定是false,跟着我的代码注释走
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//如果是down事件才分发
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
//newTouchTarget 局部变量,刚定义的为null,如果你有子View则进去
if (newTouchTarget == null && childrenCount != 0) {
**//解释一下下面的代码逻辑:拿出每一个子View的x、y,判断点击范围是否在
//你的响应范围,同时判断你的这个View是否是可点击的,且判断animation动画
//中的平移你的原来的x、y,判断的是你原来的位置,一直循环递归下去,直到最底层
//符合要求的View才返回,同时给newTouchTarget 赋值,记住你这个View
//本来这里还有一个Y,就是我们在xml里面可以设置它在这里的循环优先级
//我的这个30的源码里面已经没有了**
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
//赋值 第一个符合要求的View,它也可能是一个ViewGrop
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.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//对第一个符合要求的View进行再次递归循环
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();
//找到真正接收了事件的View真正给newTouchTarget 赋值
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);
}
if (preorderedList != null) preorderedList.clear();
}
newTouchTarget 赋值过程,这个很重要,下面要用到。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
mFirstTouchTarget = target; != null
target.next = mFirstTouchTarget; == null
alreadyDispatchedToNewTouchTarget = true;
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//不拦截走这里
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//target现在不为空了
while (target != null) {
//next == null,记住这里因为 target.next == null
final TouchTarget next = target.next;
//alreadyDispatchedToNewTouchTarget == true
//target == newTouchTarget == true 所以down事件到这里就结束了
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
//next == null,所以target现在是为空了,这个循环只会走一次,
//指的单点触控,如果是多点的话会执行多次
target = next;
}
}
流程走到这里,我们的down事件也找到真正的消费的View了。
接下来我们分析move事件,由于我们不拦截,newTouchTarget 不为空,move事件不再进入分发的判断if里面了,最终还是跑到上面的代码里,我把上面的代码赋值一份下来:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//不拦截走这里
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//target现在不为空了
while (target != null) {
//next == null,记住这里因为 target.nextnull
final TouchTarget next = target.next;
//alreadyDispatchedToNewTouchTarget == true
//target == newTouchTarget == true 所以down事件到这里就结束了
**//move事件alreadyDispatchedToNewTouchTarget局部变量又变成fasle**
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
**//move事件走这里 cancelChild 正常情况是fasle**
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
**//target.child就是刚才down事件的view,全局变量存着呢
//递归找到最终的子View传递给它处理**
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
//next == null,所以target现在是为空了,这个循环只会走一次,
//指的单点触控,如果是多点的话会执行多次
target = next;
}
}
move事件不拦截的话没什么好说的,之前的down事件是被谁消费了,直接又指给它,下面我们来分析一下move事件的拦截。
move事件–>处理事件冲突
处理事件冲突我们分为两种方法:内部拦截法和外部拦截法对应子View和父View谁处理
我们先学习内部拦截法:子View处理
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
//父View拦截,子View处理
return true;
我们看一下这个代码:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//intercepted你要拦截还得看disallowIntercept允许你拦截不
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
//下面可以看出是false或者ture,我们对disallowIntercept的返回直接决定
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
所以子View调用requestDisallowInterceptTouchEvent方法是可以让父View拦截不了你
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 内部拦截法:子view处理事件冲突
private int mLastX, mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//down的时候不拦截,那么down就到了listView
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
//记录滑动的x和y
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//如果是竖直方向则不进去,如果是水平方向取消请求不拦截,让父View正常去拦截
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
//滑动事件有多个,所以一开始是0,之后是从listView原来的x、y点开始计算
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
实际上上面这样处理还是解决不了问题:
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
//down事件触发的时候会把所有参数清零
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//down事件触发的时候这里if百分百会进去,所以父View拦截成功了
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
所以我们还需要额外的处理:
public boolean onInterceptTouchEvent(MotionEvent event) {
//down的时候不拦截,让listView拿到焦点
if (event.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(event);
return false;
}
return true;
还是之前的代码,我把之前的注释去掉便于关注核心代码:
//上一次move事件已经mFirstTouchTarget 设置 == null
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//直接给自己的View进行分发,跟down事件一样
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//listView不请求父View不拦截了之后父View拦截intercepted == true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//上面代码走完cancelChild == true
//dispatchTransformedTouchEvent方法里面的处理看下面我重新贴代码
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
//handle == true,有人处理事件不会返回给activity
handled = true;
}
if (cancelChild) {
//局部变量predecessor == null
if (predecessor == null) {
//将mFirstTouchTarget 设置为空,next等于空上面分析过了
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
//又将target也设置为空
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
dispatchTransformedTouchEvent方法
//保存旧的action
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
//cancel == true进入这里 将event设置为cancle
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
//上面肯定会成功进行分发,所以handle返回true
return handled;
}
流程走到这里子View处理就结束了,我们就可以左右滑也可以上下滑了。
父view可以抢子View的事件,而子View不可以抢父View的事件,这也是我们上下滑的时候可以进行左右滑,左右滑的时候你不能上下滑。
下面我们来看父View处理的拦截法,跟刚才子View的处理几乎一摸一样
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastX = (int) event.getX();
mLastY = (int) event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//左右滑的话就拦截,上下滑就不拦截
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.onInterceptTouchEvent(event);
}
看起来父View处理就是比子View给力啊,省事。