[例证]从滑动冲突到事件分发(从源码角度分析)
来源:互联网 发布:ee是哪个国家域名 编辑:程序博客网 时间:2024/06/06 10:57
最近在看安卓开发艺术。看到滑动冲突一章,突然有感。mark一下。
先上一个demo效果图:
上面白色区域是一个listView,外面被一个scrollView包裹着。下面红色区域是一个Linearlayout占位。用来能上下滑动。
布局源码如下:
<?xml version="1.0" encoding="utf-8"?><ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/list_item" android:layout_width="match_parent" android:layout_height="200dp"></ListView>//此处用来占位,能使整个布局达到上下滑动的条件 <LinearLayout android:layout_width="match_parent" android:layout_height="800dp" android:background="#ff0000"> </LinearLayout> </LinearLayout></ScrollView>上述布局样式:会发现,listView无法滑动,上下滑动只能滑动外部的scrollView。
问题一、为什么会滑动冲突?
首先先科普一个知识:1)事件分发机制,是依照Activity——》ViewGroup——》View,从顶部往下分发。
2)而每个ViewGroup当disallowIntercept为false的时候,都会尝试拦截onInterceptTouchEvent()。(ps:后面我会具体谈disallowIntercept这个参数)
从简上盗一张图:点击打开链接 附上事件分发机制的链接。
知道这个知识后,思考一下当前demo,
猜想冲突原因:
分发过程中,被上层的ScrollView拦截了。没有分发到ListView。
我们直接去找ViewGroup拦截的方法onInterceptTouchEvent()。
对于当前demo的布局:activity(忽略)不考虑,直接看最外层ViewGroup类:ScrollView。
发现:ScrollView的onInterceptTouchEvent()继承自FrameLayout,而FrameLayout的onInterceptTouchEvent()原封不动的继承自ViewGroup。所以我直接看ViewGroup的onInterceptTouchEvent()方法:
ViewGroup#onInterceptTouchEvent()源码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }所以ViewGroup默认是不拦截的。而FrameLayout是没有重写这个方法的。再看ScrollView的onInterceptTouchEvent()方法:
ScrollView#onInterceptTouchEvent()源码如下:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ /* * Shortcut the most recurring case: the user is in the dragging * state and he is moving his finger. We want to intercept this * motion.//笔者注:当action为move操作时,且mIsBeingDragged为真的时候,返回true,拦截 。见注释1 */ final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } /* * Don't try to intercept touch if we can't scroll anyway.//笔者注:这个ViewGroup如果不能滑动,则不允许打断。见注释2 */ if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true;//笔者注:见注释3 mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; if (mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), (int) y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. */ mIsBeingDragged = !mScroller.isFinished(); if (mIsBeingDragged && mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } startNestedScroll(SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } stopNestedScroll(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; }
注释1:mIsBeingDragged这个参数,在ScrollView中定义如下:
ScrollView#mIsBeingDragged源码如下:
/** * True if the user is currently dragging this ScrollView around. This is * not the same as 'is being flinged', which can be checked by * mScroller.isFinished() (flinging begins when the user lifts his finger). */ private boolean mIsBeingDragged = false;
mIsBeingDragged默认为false,目前此处是未拦截的。
注释2:对上面的说法,先看一个效果图:上图的红色占位区域位10dp,则scrollView无论如何都不会滑动,因此。因此并未体现滑动冲突效果onInterceptTouchEvent()返回的是false。
注释3:当滑动距离大于最小滑动距离时,onInterceptTouchEvent()返回true。我对此验证重写了ScrollView方法。
public class MyScrollView extends ScrollView{ public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent ev) { Log.e("MyScrollView",super.onTouchEvent(ev)+"||onTouchEvent"); return super.onTouchEvent(ev); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("MyScrollView",super.dispatchTouchEvent(ev)+"||dispatchTouchEvent"); return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("MyScrollView",super.onInterceptTouchEvent(ev)+"||onInterceptTouchEvent"); return super.onInterceptTouchEvent(ev); }}
点击listView区域,对onInterceptTouchEvent打印Log如下:
说明:
前面多个false原因滑动距离不够。当滑动距离大于最小距离后,onInterceptTouchEvent()返回true。注:每一次滑动,都以滑动距离达到最小滑动判断是否滑动。一次滑动包含多个小的滑动。以action down 到 action up ,判断滑动距离。
总结:
滑动冲突产生原因:外部ScrollView拦截了事件,并消费了事件。
解决方案:
处理滑动冲突的方法包涵两种:内部拦截法和外部拦截法:
1)内部拦截法代码:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: getParent().requestDisallowInterceptTouchEvent(true); break; default: getParent().requestDisallowInterceptTouchEvent(true); break; } return super.dispatchTouchEvent(ev); }通过以上方法,发现滑动冲突问题不存在了。实现了,滑动listView则滑动listView自身。
外部红色区域滑动,则滑动外部的ScrollView区域。
内部拦截法的原理
此处可能会有一个疑问:我们这么做的原理是什么?
既然只有一个方法requestDisallowInterceptTouchEvent():
ScrollView#requestDisallowInterceptTouchEvent源码如下:
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent(disallowIntercept); }disallowIntercept为true则,上面的代码暂时忽略,直接使用父类的的requestDisallowInterceptTouchEvent:
ViewGroup#requestDisallowInterceptTouchEvent源码如下:(代码二)
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }Viewparent#requestDisallowInterceptTouchEvent
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
首先放下位运算,先看看这个disallowIntercept这个标识。发现很眼熟。本文最开始也提到过。
对,就在这。ViewGroup#dispatchTouchEvent()源码:(代码一)
<pre name="code" class="java">@Override public boolean dispatchTouchEvent(MotionEvent ev) { //........省略.....final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { 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; } //........省略.....return handled; }
这个disallowIntercept标识也是一个位运算得到的。
disallowIntercept的位运算
好吧,这个位运算绕不开了。
1、先看mGroupFlags,mGroupFlags初始值为0。FLAG_DISALLOW_INTERCEPT初始值为
/** * When set, this ViewGroup should not intercept touch events. * {@hide} */ protected static final int FLAG_DISALLOW_INTERCEPT = 0x800002、mGroupFlags & FLAG_DISALLOW_INTERCEPT 计算知道值为0。
也就是:0000 0000 0000 0000 0000 & 1000 0000 0000 0000 0000 =0000 0000 0000 0000 0000;
代码一的:final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
相当于final boolean disallowIntercept = (0) != 0;
即disallowIntercept =false;
3、再看requestDisallowInterceptTouchEvent(true)时做了什么:
从代码二能看到true时对mGroupFlags重新赋值:mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
也就是mGroupFlags = mGroupFlags| FLAG_DISALLOW_INTERCEPT;
同理:mGroupFlags =0000 0000 0000 0000 0000 |1000 0000 0000 0000 0000 = 1000 0000 0000 0000 0000 ;
然后再看代码一:final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
同理:final boolean disallowIntercept = (1000 0000 0000 0000 0000 & 1000 0000 0000 0000 0000)!= 0;
因此:disallowIntercept = true;
值我们都知道了,再回到源码 代码一:
if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; }总结:
默认情况下:disallowIntercept = false,执行intercepted = onInterceptTouchEvent(ev); 也就是执行我们的onInterceptTouchEvent拦截方法。
当解决滑动冲突时,getParent().requestDisallowInterceptTouchEvent(true),disallowIntercept = false,则始终不执行onInterceptTouchEvent拦截方法。
2)外部拦截法代码:
(此段代码非针对该demo)
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.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; } } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }总结:
从上述代码能看出外部拦截法,是重写了onInterceptTouchEvent()方法。通过滑动操作的不同,返回true(拦截) 或者 false(不拦截)。
针对不同滑动操作,来解决滑动冲突的情景,使用外部拦截法。
外部拦截法的原理:
同事件分发机制的原理,ViewGroup布局的dispatchtouchevent()方法,默认使用onInterceptTouchEvent()方法(默认值为false,不拦截)判断是否拦截,若返回值为true,则ViewGroup自身消费事件,走自身的onTouchevent事件(见上叙述的事件分发机制的图片)。当出现滑动冲突的时候可通过dispatchtouchevent()返回值,来判断是否需要拦截。
- [例证]从滑动冲突到事件分发(从源码角度分析)
- Android 从源码角度分析事件分发机制(三)
- 从源码的角度分析ViewGruop的事件分发
- 从源码的角度分析ViewGruop的事件分发
- 从系统源码角度分析Android事件分发
- 从源码角度分析android事件分发处理机制
- 从源码角度分析android事件分发处理机制
- View的事件分发机制,从源码角度分析一下
- 从源码角度分析RecyclerView监听滑动到底部失效
- 从源码角度剖析Android事件分发机制(一)
- 从源码角度看滑动冲突问题的解决
- 从源码角度带你分析 Android View 事件分发 dispatchTouchEvent,onTouch,onTouchEvent,onClick逻辑顺序过程(一)
- 带你从源码角度分析ViewGroup中事件分发流程
- Android从零开搞系列:自定义View(9)事件分发+事件拦截(滑动冲突)
- 从源码的角度解析View的事件分发
- 从源码的角度解析View的事件分发
- 从源码的角度解析View的事件分发
- 从源码角度解析Android事件分发机制
- socket 通信关于bind那点事
- 遍历Map的四种方法
- BZOJ 3295: [Cqoi2011]动态逆序对 (树状数组套主席树)
- 计蒜客挑战难题:爬楼梯
- Android View滑动
- [例证]从滑动冲突到事件分发(从源码角度分析)
- Android开发笔记之软键盘的隐藏
- js如何判断一个对象是不是Array?
- 字符设备驱动访问原理
- [ERROR] Cannot find or open table wu777/processlist from the internal data dictionary of InnoDB tho
- Android不同api调用显示路径
- 1007: [HNOI2008]水平可见直线
- ckeditor源码编辑模式,添加style、javascript内容丢失的解决
- Android-使用JsBridge来优化js与本地的交互