自定義Behavior的藝術(shù)探索-仿UC瀏覽器主頁(yè)
關(guān)于Android的事件傳遞,應(yīng)該從兩個(gè)方面來(lái)說(shuō)匿垄,5.0以前移宅,5.0以后
在5.0以前:
我們一般關(guān)注三個(gè)方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
ViewGroup是繼承自View的,而onInterceptTouchEvent方法是ViewGroup獨(dú)有的椿疗,可以對(duì)事件進(jìn)行攔截漏峰,攔截之后事件是無(wú)法傳遞到子view上的(包括本次事件序列的move、up等)
一個(gè)點(diǎn)擊事件發(fā)生會(huì)依次經(jīng)過(guò)activity的dispatchTouchEvent 届榄,ViewGroup的dispatchTouchEvent浅乔,ViewGroup的onInterceptTouchEvent。
下面分兩種情況: - onInterceptTouchEvent返回true
表示ViewGroup對(duì)本事件進(jìn)行攔截,會(huì)直接調(diào)用ViewGroup的onTouchEvent靖苇,如果onTouchEvent不處理該事件席噩,會(huì)調(diào)用Activity的onTouchEvent,后續(xù)的事件不會(huì)再傳遞到ViewGroup了贤壁。 - onInterceptTouchEvent返回false
事件會(huì)傳遞到子view上悼枢,會(huì)調(diào)用View的dispatchTouchEvent,View的onTouchEvent脾拆,如果子View的onTouchEvent沒(méi)有處理事件馒索,事件會(huì)經(jīng)過(guò)ViewGroup的onTouchEvent,如果ViewGroup的onTouchEvent沒(méi)有處理假丧,會(huì)調(diào)用Activity的onTouchEvent双揪,后續(xù)的事件不會(huì)再傳遞到ViewGroup了。 - onInterceptTouchEvent先返回false后返回true包帚,則子view無(wú)法接收后續(xù)的事件(但會(huì)接收到一個(gè)cancel事件渔期,cancel事件會(huì)重置mFirstTarget,這也就是后續(xù)事件不會(huì)再執(zhí)行onInterceptTouchEvent的原因)
一些細(xì)節(jié):
onInterceptTouchEvent在兩種情況下會(huì)調(diào)用:
- down事件的時(shí)候坑定會(huì)調(diào)用
- 子view處理了down事件渴邦,后續(xù)的move也會(huì)執(zhí)行到onInterceptTouchEvent
5.0以后:引入嵌套滑動(dòng)
引入了嵌套滑動(dòng)CoordinatorLayout之后疯趟,一般是要配合Behavior使用,在使用過(guò)程中我們需要關(guān)注兩種情況:
- 某個(gè)view監(jiān)聽(tīng)另一個(gè)view的狀態(tài)變化谋梭,例如大小信峻、位置、顯示狀態(tài)等
- 某個(gè)view監(jiān)聽(tīng)CoordinatorLayout里的滑動(dòng)狀態(tài)
對(duì)于第一種情況瓮床,我們關(guān)心的是:
layoutDependsOn和onDependentViewChanged方法盹舞,
對(duì)于第二種情況,我們關(guān)心的是:
onStartNestedScroll和onNestedPreScroll方法隘庄。
先來(lái)說(shuō)第一組方法:
layoutDependsOn和onDependentViewChanged的調(diào)用時(shí)機(jī)
這個(gè)主要是在CoordinatorLayout類的onAttachedToWindow()方法踢步,內(nèi)部定義了一個(gè)ViewTreeObserver,然后給這個(gè)ViewTreeObserver添加了一個(gè)監(jiān)聽(tīng)器OnPreDrawListener丑掺,用來(lái)監(jiān)聽(tīng)view的draw操作获印。然后在這個(gè)監(jiān)聽(tīng)器中循環(huán)遍歷所有的子View,一旦某個(gè)子View發(fā)生了draw操作街州,就循環(huán)遍歷所有子view兼丰,如果發(fā)現(xiàn)某個(gè)view上有Behavior,就調(diào)用Behavior中的這些方法唆缴。
看下第二組方法:
寫(xiě)個(gè)demo來(lái)測(cè)試一下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
* 郵箱 huohacker@sina.com
*/
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
* 郵箱 huohacker@sina.com
*/
public class MBehavior extends CoordinatorLayout.Behavior<View> {
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
* 郵箱 huohacker@sina.com
*/
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;
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.huozhenpeng.myapplication.MainActivity">
<com.example.huozhenpeng.myapplication.MCoordinateLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.huozhenpeng.myapplication.MTextView
android:layout_width="100dp"
android:layout_height="50dp"
android:background="#ff00ff"
android:text="hhh "
app:layout_behavior="com.example.huozhenpeng.myapplication.MBehavior"
/>
</com.example.huozhenpeng.myapplication.MCoordinateLayout>
</android.support.constraint.ConstraintLayout>
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
* 郵箱 huohacker@sina.com
*/
public class MBehavior extends CoordinatorLayout.Behavior<View> {
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
* 郵箱 huohacker@sina.com
*/
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;
}
}
大概總結(jié)一下:事件傳遞到CoordinatorLayout的時(shí)候琐谤,CoordinatorLayout會(huì)把事件交給Behavior(不管是在onInterceptTouchEvent()方法中還是在onTouchEvent()方法中)蟆技,Behavior具有中斷事件的能力,如果Behavior中斷了事件,事件也是傳遞不到子view的质礼。
我們以NestedScrollView為例來(lái)研究下第二組方法
在onTouchEvent()方法中調(diào)用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;
}
其實(shí)就是去調(diào)用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;
}
接著就去調(diào)用Behavior的onStartNestedScroll()方法了,onNestedScrollAccepted()也是這個(gè)調(diào)用流程旺聚。
然后去調(diào)用dispatchNestedScroll()方法
關(guān)于自定義Behavior時(shí),重寫(xiě)B(tài)ehavior中的一些方法:
在CoordinatorLayout的onStartNestedScroll方法中會(huì)根據(jù)其子view(直接的字view)的Behavior的onStartNestedScroll方法的返回值來(lái)給view的LayoutParams設(shè)置一個(gè)boolean值眶蕉,后續(xù)的方法都會(huì)對(duì)該值進(jìn)行判斷
比如說(shuō)在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);
}
}
}
這說(shuō)明如果View的Behavior重寫(xiě)了onMeasureChild砰粹、onLayoutChild方法,那么會(huì)優(yōu)先調(diào)用Behavior中的方法造挽,而不會(huì)去調(diào)用CoordinatorLayout中的方法
有個(gè)疑問(wèn)是假如CoordinatorLayout的onNestedPreScroll在將事件分發(fā)給有Behavior的子View時(shí)碱璃,是可以同時(shí)將一個(gè)事件分發(fā)給所有實(shí)現(xiàn)了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);
}
}