推薦:
http://www.itdecent.cn/p/aff5e82f0174
http://blog.csdn.net/lmj623565791/article/details/52204039#reply
搭配使用效果更佳
前言:
在android 5.0之前要是想要實現(xiàn)嵌套滑動,需要自己做對應(yīng)的事件處理:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
但是在5.0之后google為我們提供了NestedScrolling機制,包含如下四個方法(support.v4為大家提供了:NestedScrollingParent、NestedScrollingChild):
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
關(guān)于事件分發(fā)的介紹-part one:
Touch事件分發(fā)的主角只有兩個:ViewGroup、View,Activity的touch事件實際上是調(diào)用內(nèi)部的ViewGroup的Touch事件,可以直接當(dāng)成ViewGroup處理。
View在ViewGroup內(nèi),ViewGroup也可以在其他ViewGroup內(nèi),這時候把內(nèi)部的ViewGroup當(dāng)成View來分析。
ViewGroup的相關(guān)事件有三個:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。View的相關(guān)事件只有兩個:dispatchTouchEvent、onTouchEvent。Activity的相關(guān)事件有兩個:dispatchTouchEvent、onTouchEvent。
先分析 ViewGroup的處理流程:首先得有個結(jié)構(gòu)模型概念:ViewGroup和View組成了一棵樹形結(jié)構(gòu),最頂層為Activity的 ViewGroup,下面有若干的ViewGroup節(jié)點,每個節(jié)點之下又有若干的ViewGroup節(jié)點或者View節(jié)點,依次類推。如圖:

當(dāng)一個Touch事件(觸摸事件)到達根結(jié)點,即activity的ViewGroup時,它會依次下發(fā),下發(fā)的過程調(diào)用子View或者Viewgroup的dispatchTouchEvent方法。簡單來說,就是ViewGroup會遍歷它包含的子View,調(diào)用每個View的dispatchTouchEvent,當(dāng)遇到ViewGroup時,又會調(diào)用ViewGroup的dispatchTouchEvent方法,繼續(xù)調(diào)用其內(nèi)部的View的dispatchTouchEvent方法,上圖中的調(diào)用順序為:1->2->5->6->7->3->4 dispatchTouchEvent方法只負責(zé)事件的分發(fā),它擁有boolean類型的返回值,當(dāng)返回為true時, 順序下發(fā)會中斷。在上述例子中如果5的dispatchTouchEvent返回結(jié)果為true,那么6->7->3->4將都接收不到本次Touch事件。
關(guān)于ViewGroup中dispatchTouchEvent簡單分析:
/*** ViewGroup
* @param ev
* @return
*/
public boolean dispatchTouchEvent(MotionEvent ev){
....//其他處理,在此不管
View[] views=getChildView();
for(int i=0;i<views.length;i++){
//判斷下Touch到屏幕上的點在該子View上面
if(...){
if(views[i].dispatchTouchEvent(ev))
return true;
}
}
...//其他處理,在此不管
}
/** * View
* @param ev
* @return
*/
public boolean dispatchTouchEvent(MotionEvent ev){
....//其他處理,在此不管
return false;
}
由此可見,ViewGroup中的dispatchTouchEvent是真正執(zhí)行“分發(fā)”工作,而View中對應(yīng)的dispatchTouchEvent并不執(zhí)行分發(fā)工作,或者說它分發(fā)的對象就是他自己,決定是否把touch事件交給自己處理,而處理的方法,便是onTouchEvent事件,事實上子View的dispatchTouchEvent方法真正執(zhí)行的代碼是這樣的
/*** View
* @param ev
* @return
*/
public boolean dispatchTouchEvent(MotionEvent ev){
....//其他處理,在此不管
return onTouchEvent(event);
}
一般情況下,我們不該在普通View內(nèi)重寫dispatchTouchEvent方法,因為它并不執(zhí)行分發(fā)邏輯。當(dāng)Touch事件到達View時,我們該做的就是是否在onTouchEvent事件中處理它。
那么,ViewGroup的onTouchEvent事件是什么時候處理的呢?當(dāng)ViewGroup所有的子View都返回false時,onTouchEvent事件便會執(zhí)行。由于ViewGroup是繼承于View的,它其實也是通過調(diào)用View的dispatchTouchEvent方法來執(zhí)行onTouchEvent事件。在目前的情況看來,似乎只要我們把所有的onTouchEvent都返回false,就能保證所有的子控件都響應(yīng)本次Touch事件了。但必須要說明的是,這里的Touch事件,只限于Acition_Down事件,即觸摸按下事件,而Aciton_UP和Action_MOVE卻不會執(zhí)行。事實上,一次完整的Touch事件,應(yīng)該是由一個Down、一個Up和若干個Move組成的。Down方式通過dispatchTouchEvent分發(fā),分發(fā)的目的是為了找到真正需要處理完整Touch請求的View。當(dāng)某個View或者ViewGroup的onTouchEvent事件返回true時,便表示它是真正要處理這次請求的View,之后的Aciton_UP和Action_MOVE將由它處理。當(dāng)所有子View的onTouchEvent都返回false時,這次的Touch請求就由根ViewGroup,即Activity自己處理了。
View mTarget=null;//保存捕獲Touch事件處理的View
public boolean dispatchTouchEvent(MotionEvent ev) {
//....其他處理,在此不管
if(ev.getAction()==KeyEvent.ACTION_DOWN){
//每次Down事件,都置為Null
if(!onInterceptTouchEvent()){
mTarget=null;
View[] views=getChildView();
for(int i=0;i<views.length;i++){
if(views[i].dispatchTouchEvent(ev))
mTarget=views[i];
return true;
}
}
}
//當(dāng)子View沒有捕獲down事件時,ViewGroup自身處理。這里處理的Touch事件包含Down、 Up和Move
if(mTarget==null){
return super.dispatchTouchEvent(ev);
}
//...其他處理,在此不管
if(onInterceptTouchEvent()){
//...其他處理,在此不管
}
//這一步在Action_Down中是不會執(zhí)行到的,只有Move和UP才會執(zhí)行到。
return mTarget.dispatchTouchEvent(ev);
}
ViewGroup還有個onInterceptTouchEvent,看名字便知道這是個攔截事件。這個攔截事件需要分兩種情況來說明:
1.假如我們在某個ViewGroup的onInterceptTouchEvent中,將Action為Down的Touch事件返回true,那便表示將該ViewGroup的所有下發(fā)操作攔截掉,這種情況下,mTarget會一直為null,因為mTarget是在Down事件中賦值的。由于mTarge為null,該ViewGroup的onTouchEvent事件被執(zhí)行。這種情況下可以把這個ViewGroup直接當(dāng)成View來對待。
2.假如我們在某個ViewGroup的onInterceptTouchEvent中,將Acion為Down的Touch事件都返回false,其他的都返回True,這種情況下,Down事件能正常分發(fā),若子View都返回false,那mTarget還是為空,無影響。若某個子View返回了true,mTarget被賦值了,在Action_Move和Aciton_UP分發(fā)到該ViewGroup時,便會給mTarget分發(fā)一個Action_Delete的MotionEvent,同時清空mTarget的值,使得接下去的Action_Move(如果上一個操作不是UP)將由ViewGroup的onTouchEvent處理。情況一用到的比較多,情況二個人還未找到使用場景。
從頭到尾總結(jié)一下:
Touch事件分發(fā)中只有兩個主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三個相關(guān)事件。View包含dispatchTouchEvent、onTouchEvent兩個相關(guān)事件。其中ViewGroup又繼承于View。
ViewGroup和View組成了一個樹狀結(jié)構(gòu),根節(jié)點為Activity內(nèi)部包含的一個ViwGroup。
觸摸事件由Action_Down、Action_Move、Aciton_UP組成,其中一次完整的觸摸事件中,Down和Up都只有一個,Move有若干個,可以為0個。
當(dāng)Acitivty接收到Touch事件時,將遍歷子View進行Down事件的分發(fā)。ViewGroup的遍歷可以看成是遞歸的。分發(fā)的目的是為了找到真正要處理本次完整觸摸事件的View,這個View會在onTouchuEvent結(jié)果返回true。
當(dāng)某個子View返回true時,會中止Down事件的分發(fā),同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接進行處理。由于子View是保存在ViewGroup中的,多層ViewGroup的節(jié)點結(jié)構(gòu)時,上級ViewGroup保存的會是真實處理事件的View所在的ViewGroup對象:如ViewGroup0-ViewGroup1-TextView的結(jié)構(gòu)中,TextView返回了true,它將被保存在ViewGroup1中,而ViewGroup1也會返回true,被保存在ViewGroup0中。當(dāng)Move和UP事件來時,會先從ViewGroup0傳遞至ViewGroup1,再由ViewGroup1傳遞至TextView。
當(dāng)ViewGroup中所有子View都不捕獲Down事件時,將觸發(fā)ViewGroup自身的onTouch事件。觸發(fā)的方式是調(diào)用 super.dispatchTouchEvent函數(shù),即父類View的dispatchTouchEvent方法。在所有子View都不處理的情況下,觸發(fā)Acitivity的onTouchEvent方法。
onInterceptTouchEvent有兩個作用:1.攔截Down事件的分發(fā)。2.中止Up和Move事件向目標(biāo)View傳遞,使得目標(biāo)View所在的ViewGroup捕獲Up和Move事件。
補充:“觸摸事件由Action_Down、Action_Move、Aciton_UP組成,其中一次完整的觸摸事件中,Down和Up都只有一個,Move有若干個,可以為0個?!保@里補充下其實UP事件是可能為0個的。
對于onInterceptTouchEvent事件,它的應(yīng)用場景在很多帶scroll效果的ViewGroup中都有體現(xiàn)。設(shè)想一下再一個ViewPager中,每個Item都是個ImageView,我們需要對這些ImageView做Matrix操作,這不可避免要捕獲掉Touch事件,但是我們又需要做到不影響ViewPager翻頁效果,這又必須保證ViewPager能捕獲到Move事件,于是,ViewPager的onInterceptTouchEvent會對Move事件做一個過濾,當(dāng)適當(dāng)條件的Move事件(持續(xù)若干事件或移動若干距離,這里我沒讀源碼只是猜測)觸發(fā)時,并會攔截掉,返回子View一個Action_Cancel事件。這個時候子View就沒有Up事件了,很多需要在Up中處理的事物要轉(zhuǎn)到Cancel中處理。
單個控件的事件分發(fā)流程圖如下:(以Button為例)

因此,事件分發(fā)之間的關(guān)系是:dispatchTouchEvent方法中先執(zhí)行 onTouch接口回調(diào),然后根據(jù)onTouch方法的返回值判斷是否執(zhí)行onTouchEvent方法,onTouchEvent方法中執(zhí)行了onClick接口回調(diào)。
在ViewGroup中onInterceptTouchEvent返回true時,才會調(diào)用自己的onTouchEvent,dispatchTouchEvent返回true,不會調(diào)用自己的onTouchEvent。
結(jié)論:dispatchTouchEvent---->onTouch---->onTouchEvent----->onClick。并且如果仔細的你會發(fā)現(xiàn),是在**所有ACTION_UP事件之后才觸發(fā)onClick點擊事件。 **
關(guān)于事件分發(fā)的介紹-part two:
Android中默認情況下事件傳遞是由最終的view的接收到,傳遞過程是從父布局到子布局,也就是從Activity到ViewGroup到View的過程,默認情況,ViewGroup起到的是透傳作用。Android中事件傳遞過程(按箭頭方向)如下圖:

觸摸事件是一連串ACTION_DOWN,ACTION_MOVE..MOVE…MOVE、最后ACTION_UP,觸摸事件還有ACTION_CANCEL事件。事件都是從ACTION_DOWN開始的,Activity的dispatchTouchEvent()首先接收到ACTION_DOWN,執(zhí)行super.dispatchTouchEvent(ev),事件向下分發(fā)。
dispatchTouchEvent()返回true,后續(xù)事件(ACTION_MOVE、ACTION_UP)會再傳遞,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。





android中的Touch事件都是從ACTION_DOWN開始的:
**單手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP
多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOVE--ACTION_POINTER_UP---ACTION_UP
**
關(guān)于嵌套滑動機制:
/**
* 重點關(guān)注方法:
* onStartNestedScroll
* onNestedPreScroll
*/
public class StickyNavLayout extends LinearLayout implements NestedScrollingParent {
private static final String TAG = "StickyNavLayout";
/**
* onStartNestedScroll該方法返回true,代表當(dāng)前ViewGroup能接受內(nèi)部View的滑動參數(shù)(這個內(nèi)部View不一定是直接子View),
* 一般情況下建議直接返回true,當(dāng)然你可以根據(jù)nestedScrollAxes:判斷垂直或水平方向才返回true。
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.e(TAG, "onStartNestedScroll");
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
Log.e(TAG, "onNestedScrollAccepted");
}
@Override
public void onStopNestedScroll(View target) {
Log.e(TAG, "onStopNestedScroll");
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.e(TAG, "onNestedScroll");
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
Log.e(TAG, "onNestedPreScroll");
boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
boolean showTop = dy < 0 && getScrollY() >= 0 && !ViewCompat.canScrollVertically(target, -1);
Log.d("onNestedPreScroll", "--------------------------------------------------------------------------------");
Log.d("onNestedPreScroll", "ViewCompat.canScrollVertically(target, -1) target view 向下滾動--->:" + ViewCompat.canScrollVertically(target, -1));
Log.d("onNestedPreScroll", "!ViewCompat.canScrollVertically(target, -1) --->:" + !ViewCompat.canScrollVertically(target, -1));
Log.d("onNestedPreScroll", "ViewCompat.canScrollVertically(target, 1) target view 向上滾動--->:" + ViewCompat.canScrollVertically(target, 1));
Log.d("onNestedPreScroll", "!ViewCompat.canScrollVertically(target, 1)--->:" + !ViewCompat.canScrollVertically(target, 1));
Log.d("onNestedPreScroll", "hiddenTop ->" + String.valueOf(hiddenTop));
Log.d("onNestedPreScroll", "showTop ->" + String.valueOf(showTop));
Log.d("onNestedPreScroll", "dy/2 ->" + String.valueOf(dy / 2));
Log.d("onNestedPreScroll", "--------------------------------------------------------------------------------");
if (hiddenTop || showTop) {
/**
* 這個是top 每次scroll的距離
*/
scrollBy(0, dy);
/**
* 這個是top消耗的距離
* 如果為dy/2,則余下的dy/2由子view消耗,即滑動的過程中,子view同時也會滑動
*/
consumed[1] = dy;
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
Log.e(TAG, "onNestedFling");
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
Log.e(TAG, "onNestedPreFling");
//down - //up+
if (getScrollY() >= mTopViewHeight) return false;
fling((int) velocityY);
return true;
}
@Override
public int getNestedScrollAxes() {
Log.e(TAG, "getNestedScrollAxes");
return 0;
}
private View mTop;
private View mNav;
private ViewPager mViewPager;
private int mTopViewHeight;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private float mLastY;
private boolean mDragging;
public StickyNavLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
mScroller = new OverScroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context)
.getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context)
.getScaledMinimumFlingVelocity();
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTop = findViewById(R.id.id_stickynavlayout_topview);
mNav = findViewById(R.id.id_stickynavlayout_indicator);
View view = findViewById(R.id.id_stickynavlayout_viewpager);
if (!(view instanceof ViewPager)) {
throw new RuntimeException(
"id_stickynavlayout_viewpager show used by ViewPager !");
}
mViewPager = (ViewPager) view;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//不限制頂部的高度
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
getChildAt(0).measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
setMeasuredDimension(getMeasuredWidth(), mTop.getMeasuredHeight() + mNav.getMeasuredHeight() + mViewPager.getMeasuredHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mTop.getMeasuredHeight();
}
/**
* 對于fling方法,我們使用OverScroller的fling方法,另外邊界檢測,重寫了scrollTo方法:
*/
public void fling(int velocityY) {
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
invalidate();
}
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mTopViewHeight) {
y = mTopViewHeight;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
}