自定义Behavior的艺术探索-仿UC浏览器主页
关于Android的事件传递,应该从两个方面来说,5.0以前,5.0以后
在5.0以前:
我们一般关注三个方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
ViewGroup是继承自View的,而onInterceptTouchEvent方法是ViewGroup独有的,可以对事件进行拦截,拦截之后事件是无法传递到子view上的(包括本次事件序列的move、up等)
一个点击事件发生会依次经过activity的dispatchTouchEvent ,ViewGroup的dispatchTouchEvent,ViewGroup的onInterceptTouchEvent。
下面分两种情况: - onInterceptTouchEvent返回true
表示ViewGroup对本事件进行拦截,会直接调用ViewGroup的onTouchEvent,如果onTouchEvent不处理该事件,会调用Activity的onTouchEvent,后续的事件不会再传递到ViewGroup了。 - onInterceptTouchEvent返回false
事件会传递到子view上,会调用View的dispatchTouchEvent,View的onTouchEvent,如果子View的onTouchEvent没有处理事件,事件会经过ViewGroup的onTouchEvent,如果ViewGroup的onTouchEvent没有处理,会调用Activity的onTouchEvent,后续的事件不会再传递到ViewGroup了。 - onInterceptTouchEvent先返回false后返回true,则子view无法接收后续的事件(但会接收到一个cancel事件,cancel事件会重置mFirstTarget,这也就是后续事件不会再执行onInterceptTouchEvent的原因)
一些细节:
onInterceptTouchEvent在两种情况下会调用:
- down事件的时候坑定会调用
- 子view处理了down事件,后续的move也会执行到onInterceptTouchEvent
5.0以后:引入嵌套滑动
引入了嵌套滑动CoordinatorLayout之后,一般是要配合Behavior使用,在使用过程中我们需要关注两种情况:
- 某个view监听另一个view的状态变化,例如大小、位置、显示状态等
- 某个view监听CoordinatorLayout里的滑动状态
对于第一种情况,我们关心的是:
layoutDependsOn和onDependentViewChanged方法,
对于第二种情况,我们关心的是:
onStartNestedScroll和onNestedPreScroll方法。
先来说第一组方法:
layoutDependsOn和onDependentViewChanged的调用时机
这个主要是在CoordinatorLayout类的onAttachedToWindow()方法,内部定义了一个ViewTreeObserver,然后给这个ViewTreeObserver添加了一个监听器OnPreDrawListener,用来监听view的draw操作。然后在这个监听器中循环遍历所有的子View,一旦某个子View发生了draw操作,就循环遍历所有子view,如果发现某个view上有Behavior,就调用Behavior中的这些方法。
看下第二组方法:
写个demo来测试一下CoordinatorLayout、Behavior、子View之间事件的流程
package com.example.huozhenpeng.myapplication;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
/**
* 作者 huozhenpeng
* 日期 2018/8/10
* 邮箱 [email protected]
*/
public class MCoordinateLayout extends CoordinatorLayout {
public MCoordinateLayout(Context context) {
super(context);
}
public MCoordinateLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MCoordinateLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("abc","coordinatorlayout------onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("abc","coordinatorlayout------onTouchEvent");
return super.onTouchEvent(ev);
}
}
package com.example.huozhenpeng.myapplication;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* 作者 huozhenpeng
* 日期 2018/8/10
* 邮箱 [email protected]
*/
public class MBehavior extends CoordinatorLayout.Behavior {
public MBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
Log.e("abc","behavior----onInterceptTouchEvent");
return super.onInterceptTouchEvent(parent, child, ev);
// return true;
}
@Override
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
Log.e("abc","behavior----onTouchEvent");
return super.onTouchEvent(parent, child, ev);
}
}
package com.example.huozhenpeng.myapplication;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
/**
* 作者 huozhenpeng
* 日期 2018/8/10
* 邮箱 [email protected]
*/
public class MTextView extends android.support.v7.widget.AppCompatTextView {
public MTextView(Context context) {
super(context);
}
public MTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("abc","textview---------onTouchEvent");
return super.onTouchEvent(event);
// return true;
}
}
package com.example.huozhenpeng.myapplication;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* 作者 huozhenpeng
* 日期 2018/8/10
* 邮箱 [email protected]
*/
public class MBehavior extends CoordinatorLayout.Behavior {
public MBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
Log.e("abc","behavior----onInterceptTouchEvent");
// return super.onInterceptTouchEvent(parent, child, ev);
return true;
}
@Override
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
Log.e("abc","behavior----onTouchEvent");
return super.onTouchEvent(parent, child, ev);
}
}
package com.example.huozhenpeng.myapplication;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
/**
* 作者 huozhenpeng
* 日期 2018/8/10
* 邮箱 [email protected]
*/
public class MTextView extends android.support.v7.widget.AppCompatTextView {
public MTextView(Context context) {
super(context);
}
public MTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("abc","textview---------onTouchEvent");
// return super.onTouchEvent(event);
return true;
}
}
大概总结一下:事件传递到CoordinatorLayout的时候,CoordinatorLayout会把事件交给Behavior(不管是在onInterceptTouchEvent()方法中还是在onTouchEvent()方法中),Behavior具有中断事件的能力,如果Behavior中断了事件,事件也是传递不到子view的。
我们以NestedScrollView为例来研究下第二组方法
在onTouchEvent()方法中调用startNestedScroll()
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
紧接着
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
return false;
}
其实就是去调用CoordinatorLayout 类的onStartNestedScroll方法了
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
接着就去调用Behavior的onStartNestedScroll()方法了,onNestedScrollAccepted()也是这个调用流程。
然后去调用dispatchNestedScroll()方法
关于自定义Behavior时,重写Behavior中的一些方法:
在CoordinatorLayout的onStartNestedScroll方法中会根据其子view(直接的字view)的Behavior的onStartNestedScroll方法的返回值来给view的LayoutParams设置一个boolean值,后续的方法都会对该值进行判断
比如说在onNestedScroll方法中
CoordinatorLayout
看下CoordinatorLayout的onMeasure和onLayout的部分代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
..........
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
这说明如果View的Behavior重写了onMeasureChild、onLayoutChild方法,那么会优先调用Behavior中的方法,而不会去调用CoordinatorLayout中的方法
有个疑问是假如CoordinatorLayout的onNestedPreScroll在将事件分发给有Behavior的子View时,是可以同时将一个事件分发给所有实现了Behavior的子View吗?看下代码就理解了
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}