第三章 View的基本概念

View是android中所有控件的基類,不管是簡單的Button還是TextView,還是複雜的RelativeLayout和ListView,他們的共同基類都是View,所以說,View是一種界面層的空間的一種抽象,它代表了一個控件。

3.1View的基礎知識

.View的位置參數

View的位置主要由它的四個頂點來決定,分別對應View的四個屬性:top,left,right,bottom.

top=getTop();  left=getLeft();  right=getRight();   bottom=getBottom();


由圖可得View的寬高和座標的關係:

width=right-left;

height=bottom-top;

Android3.0開始View多了額外的幾個參數:x,y,translationX,translationY,其中x和y是View左上角的座標,而translationX,translationY是View左上角相對父容器的偏移量,並且初始默認值爲0。

關係換算等式如下:x=left+translationX;    y=top+translationY;

當View在平移的時候,變的是translationX,translationY這兩個參數,top和left表示的是原始左上角的位置信息,不會改變,此時改變的是x、y、translationX,translationY這四個參數。


二. MotionEvent和TouchSlop

1.MotionEvent

在手指接觸屏幕後產生的一系列事件中,典型的有如下幾種:

ACTION_DOWN-----手指剛接觸屏幕

ACTION_MOVE----手機在屏幕上移動

ACTION_UP-----手指從屏幕上鬆開的一瞬間

當點擊屏幕後鬆手:事件序列爲DOWN->UP

當點擊屏幕滑動一會再鬆手:事件序列爲DOWN->MOVE->..........MOVE->UP

同時通過MotionEvent對象可以得到點擊事件發生的x和y座標,系統提供了兩組方法:getX/getY和getRawX/getRawY.

他們的區別在於,getX/getY返回的是相對於當前View左上角的x和y座標,而getRawX/getRawY返回的是相對於手機屏幕左上角的x和y座標


2.TouchSlop

TouchSlop是系統所能識別的被認爲是滑動的最小距離,手機在屏幕上滑動時,如果兩次滑動距離小於該值,系統就不認爲你在滑動,這個值跟設備有關,是常量。通過ViewConfiguration.get(getContext()).getScaledTouchSlop()可以得到。

這個常量用於當我們處理滑動事件的時候,可以判斷兩次滑動事件的滑動距離如果小於該值,則認爲不是滑動,用戶體驗更好。


3.VelocityTracker

速度追蹤,用於追蹤手指在滑動中的速度,包括水平和豎直方向的速度。它的使用過程很簡單,首先在View的onTouchEvent方法中追蹤當前單擊事件的速度:

        VelocityTracker velocityTracker =VelocityTracker .obtain();

        velocityTracker.addMovement(event);

接着,當我們先知道當前的滑動速度時,這個時候可以採用如下方式來獲得當前的速度:

            velocityTracker.computeCurrentVelocity(1000);
            int xVelocity = (int) velocityTracker.getXVelocity();

            int yVelocity = (int) velocityTracker.getYVelocity();

在這一步有兩點需要注意,獲取速度前必須先計算速度,即.getXVelocity(),getYVelocity()前要computeCurrentVelocity(1000(單位毫秒)),其中參數1000,代表時間間隔1s內,手指在水平方向從左向右劃過100像素,那麼水平速度就是100像素/s,從右往左,水平速度爲負!   速度的計算公式:速度=(終點位置-起點未知)/時間段

當不需要使用它的時候,調用clear方法來重置並回收內存

 velocityTracker.clear();

  velocityTracker.recycle();

4.GestureDetector

手勢檢測,用於輔助檢測用戶的單擊,滑動,長按,雙擊等行爲,使用過程並不負責。

首先需要創建一個GestureDetector對象並實現OnGestureListener接口,根據需要我們還可以實現OnDoubleTapListener從而能夠監聽雙擊行爲:

GestureDetector mGestureDetector=new GestureDetector(VelocityTrackerActivity.this,onGestureListener);
//解決長按屏幕後無法拖動的現象
mGestureDetector.setIsLongpressEnabled(false);

接着,接管目標View的OnTouchEvent方法,在監聽View的OnTouchEvent方法中添加如下實現:

public boolean onTouch(View v, MotionEvent event) {
    if (mGestureDetector.onTouchEvent(event))
        return true;
    return false;

做完上面兩步,我們就可以有選擇性的實現OnGestureListener和OnDoubleTapListener中的方法了,這兩個接口中的方法介紹如下:



日常開發中常用的有onSingleTapUp(單擊)、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)、onDoubleTap(雙擊)。建議:如果只是監聽滑動相關的,建議才onTouchEvent中實現,如果要監聽雙擊這種行爲,那麼就使用GestureDetector。

5.Scroller

彈性滑動對象,用於實現View的彈性滑動,我們知道,當使用View的scrollTo/scrollBy方法進行滑動時,其過程是瞬間完成的,沒有過渡效果用戶體驗不好,這時即可用Scroller+View的computeScroll方法配合使用才能同時完成這個功能,代碼如下:

Scroller mScroller=new Scroller(getContext());
mScroller.startScroll(0, 2, 0, 2, 1000);
invalidate();
public void computeScroll() {
    //先判斷mScroller滾動是否完成
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();//必須調用該方法,否則不一定能看到滾動效果
        
    }
    super.computeScroll();
}

3.2View的滑動

在android設備上,滑動幾乎是應用的標配,不管是下拉刷新還是SlidingMenu,通過三種方式可以實現View的滑動:

第一種是通過使用View本身提供的scrollTo/scrollBy方法實現滑動;

第二種通過動畫給View施加平移效果來實現滑動;

第三種是通過改變View的LayoutParams使得View重新佈局從而實現滑動

3.2.1使用scrollTo/scrollBy

爲了實現View的滑動,View提供了專門的方法來實現這個功能,讓我們看看這兩個方法的實現:



從源碼可以看出,scrollBy實際上也是調用了scrollTo方法,它實現了基於當前位置的相對滑動,例如我傳入了20,但是它會在該基礎上加上當前位置的x,y座標,(100,100,100+20,100+20)從(100,100)滑到了(120,120),而scrollTo則實現了基於所傳遞參數的絕對滑動,我傳入20,他就(100,100,20,20)從(100,100)滑到了(20,20),雖然使用這兩個方法滑動很簡單,但我們要明白View內部兩個屬性mScrollX和mScrollY的改變規則,這兩個屬性可以通過getScrollX和getScrollY方法分別獲得。

在滑動過程中,mScrollX的值總是等於View左邊緣和View內容左邊緣在水平方向的距離,mScrollY得值總等於View上邊緣和View的內容上邊緣在豎直方向上的距離。View的邊緣是指View的位置,由4個頂點組成,而View內容邊緣是指View中的內容的邊緣,scrollBy和scrollTo只能改變View內容的位置而不能改變View在佈局中的位置。mScrollX和mScrollY單位爲像素,並且當View的左邊緣在View內容左邊緣的右邊時,mScrollX爲正值,反之爲負值;當View的上邊緣在View內容上邊緣的下邊時,mScrollY爲正值,反之爲負值。換句話說,如果從左往右滑動,那麼mScrollX爲負值,反之爲正值;從上往下滑動,那麼mScrollY爲負值,反之爲正值

爲了更好地理解,如圖演示。圖中設滑動距離都爲100像素,針對圖中各種滑動情況,都給出了對應的mScrollX和mScrollY的值。根據上面的分析,可以知道使用scrollBy和scrollTo來實現View的滑動,只能將View的內容進行移動,並不能將View本身進行移動,也就是說,不管怎麼滑,也不可能將當前View滑動到附近View所在的區域,這個需要仔細體會一下。



3.2.2使用動畫

上一節介紹了採用scrollBy和scrollTo來實現View的滑動,本節簡述另外一種滑動方式,即使用動畫,通過動畫我們能夠讓一個View進行平移,而平移就是一種滑動。使用動畫來移動View,主要是操作View的translationX和tranalationY屬性,既可以採用傳統的View動畫,也可以使用屬性動畫,如果採用屬性動畫的話,爲了兼容3.0以下版本,需要使用開源動畫庫nineoldandroids(http://nineoldandroids.com/) 。

採用View動畫的代碼,如下所示,此動畫可以在100ms內將一個View從原始位置向右下角移動100像素


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fillAfter="true">

    <translate
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>
Animation animation= AnimationUtils.loadAnimation(VelocityTrackerActivity.this,R.anim.anim_translation);
textView.startAnimation(animation);

fillAfter=true代表動畫結束後控件內容停留在結束的位置,false則控件內容會回去。

如果採用屬性動畫的話,更簡單,以下代碼可以在100ms內將一個View從原始位置向右下角移動100像素

ObjectAnimator.ofFloat(textView,"translationX",0,100).setDuration(100).start();

上面簡單介紹了通過動畫來移動View的方法,關於動畫會在第5章中進行詳細說明。
使用動話來做的滑動需要注意一點,View動畫是對View的影像做操作,它並不能真
正改變View的位置參數,包括寬/高,並且如果希望動畫後的狀態得以保留還必須將fillAfter
屬性設賈爲true,否則動畫完成後其動畫結果會消失。比如我們要把View向右移動100像
素,如果fillAfter爲false,那麼在動畫完成的一剎那,View會瞬間恢復到動畫前的狀態;
如果fillAfter爲true,在動畫完成後,會停留在距原始位置100像索的石邊。使用域
性動畫並不會存在上述問題,但是在3.0以下無法使用屬性動畫,這個時候我們可
以使用動畫兼容庫nineoldandroids來實現屬性動畫,儘管如此在Android3.0以下的手機
上通過nineoldandroids來實現的屬性動畫本質上仍然是View動畫。

上面提到View動畫並不能真正改變View的位置,這會帶來一個很嚴軍的問題。試想
—下,比如我們通過View動畫將一個Button向右移動100px,並且這個View設置的有單
擊事件,然後你會驚奇地發現,單擊新位置無法觸發onClick亨件,而單擊原始位置仍然
可以觸發onClick事件,儘管Button己經不在原始位置了。這個問題帶來的影響是致命的,
但是它卻又是可以理解的,因爲不管Button怎麼做變換,但是它的位置信息(四個頂點和

寬/高)並不會隨着動畫而改變,因此在系統眼裏,這個Button並沒有發生任何改變,它的

真身仍然在原始位置。在這種情況下,單擊新位W當然不會觸發onClick事件了,因爲Button
的真身並沒有發生改變,在新位置上只是View的影像而已。基於這一點,我們不能簡單地

給一個View做平移動畫並且還希望它在新位置繼續觸發一些單擊事件。

從Android3.0開始,使用屬性動畫可以解決上面的問題,但是大多數應用都需要兼容到Android2.2,在Android2.2上無法使用屬性動畫,因此這裏還是會有問題。那麼這種問題難道就無法解決了叫?也不是的,雖然不能直接解決這個問題,但是還可以間接解決這個問題,這裏給出一個簡單的解決方法。針對上面View動畫的問題,我們可以在新位置預先創建一個和目標Button—模一樣的Button,它們不但外觀一樣連onClick事件也一樣,當目標Button完成平移動畫後,就把目標Button隱藏,同時把預先創建的Button顯示出來,通過這種間接的方式我們解決了上面的問題。這僅僅是個參考,面對這種問題時讀齊可以義活應對。


3.2.3改變佈局參數

本節將介紹第三種實現View滑動的方法,那就是改變佈局參數,即改變LayoutParams.
這個比較好理解了,比如我們想把一個Button向右平移100px,我們只需要將這個Button
的LayoutParams裏的marginLeft參數的值增加100px即可,是不是很簡單呢?還有一種情
形,爲了達到移動Button的目的,我們可以在Button的左邊放置一個空的View,這個空
View的默認寬度爲0,當我們需要向右移動Button時,只需要重新設置空View的寬度即
可,當空View的寬度增大時(假設Button的父容器是水平方向的LinearLayout),Button
就自動被擠向右邊,即實現了向右平移的效果。如何重新設置一個View的LayoutParams

呢?很簡申,如下所示。

ViewGroup.MarginLayoutParams params= (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
params.width+=100;
params.leftMargin+=100;
textView.requestLayout();
//或者textView.setLayoutParams(params);
3.2.4各種滑動方式的對比


上面分別介紹了三種不同的滑動方式,它們都能實現View的滑動,那麼它們之間的差別是什麼呢?先#scrollTo/scroUBy這種力'式,它是View提供的原生方法,其作用是專門用於View的滑動,它可以比較方便地實現滑動效果並且不影響內部元索的單擊事件。但是它的缺點也足很顯然的:它只能滑動View的內荇,並不能滑動View本身。再呑動畫,通過動畫來實現View的滑動,這要分情況。如果是Android3.0以上並採用《性動畫,那麼採用這種方式沒有明顯的缺點;如果娃使用View動畫或者在Android3.0以下使用M性動畫,均不能改變View本身的M性。在實際使用中,如果動畫元紊不需要響應用戶的交互,那麼使用動畫來做滑動是比較合適的,否則就不太適合。但是動畫有一個很明M的優點,那就是一些複雜的效果必須要通過動畫才能實現。

        最後再呑一下改變佈局這種方式,它除了使用起來麻煩點以外,也沒冇明顯的缺點,它的主要適用對象是一些具存交互性的View,因爲這些View需要和用戶交互,直接通過動畫去實現會有問題,這在3.2.2節中已經有所介紹,所以這個時候我們可以使用直接改變佈局參數的方式去實現。針對上面的分析做一下總結,如下所示:

        scrollTo/scrollBy:操作簡單,適合對View內容的滑動:
        動畫:操作簡單,主要適用於沒有交互的View和實現複雜的動両效果:

        改變佈局參數:操作稍微複雜,適用於有交互的View。

下面我們實現一個跟手滑動的效果,這是一個自定義View,拖動它可以讓它在整個屏幕上隨意滑動。這個View實現起來很簡單,我們只要重寫它的onTouchEvent方法並處理ACTION_MOVE事件,根據兩次滑動之間的距離就可以實現它的滑動了。爲了實現全屏滑動,我們採用動畫的方式來實現。原因很簡單,這個效果無法採用scrollTo來實現,另外,它還可以採用改變佈局的方式來實現,這裏僅僅是爲了演示,所以就選擇了動畫的方式,核心代碼如下所示。


public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            if (isPointOnViews(event)){
                //標記狀態爲拖拽,並記錄上次觸摸座標
                mCurrentState=State.DRAGGING;
                mLastX=event.getX();
                mLastY=event.getY();
            }
            break;
        case MotionEvent.ACTION_MOVE:
            int deltX= (int) (event.getX()-mLastX);
            int deltY= (int) (event.getY()-mLastY);
            if (mCurrentState==State.DRAGGING&&mDragView!=null&&(Math.abs(deltX)>mSlop||Math.abs(deltY)>mSlop)){
                //如果符合條件則對被拖拽的 child 進行位置移動
                ViewCompat.offsetLeftAndRight(mDragView,deltX);
                ViewCompat.offsetTopAndBottom(mDragView,deltY);
                mLastX=event.getX();
                mLastY=event.getY();
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            if (mCurrentState==State.DRAGGING){
                /*if (mDragView!=null){
                    ValueAnimator animator=ValueAnimator.ofFloat(mDragView.getX(),mDragViewOrigX);
                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mDragView.setX((Float) animation.getAnimatedValue());
                        }
                    });
                    ValueAnimator animator1=ValueAnimator.ofFloat(mDragView.getY(),mDragViewOrigY);
                    animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mDragView.setY((Float) animation.getAnimatedValue());
                        }
                    });
                    AnimatorSet animatorSet=new AnimatorSet();
                    animatorSet.play(animator).with(animator1);
                    animatorSet.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation, boolean isReverse) {
                            mDragView=null;
                        }
                    });
                    animatorSet.start();
                }*/
                mDragView=null;
                mCurrentState=State.IDLE;
            }
            break;
    }
    return true;
}
/**
 * 判斷落點是否在child身上
 *
 *  FrameLayout 的特性,最上面的 child 其實在 ViewGroup 的索引位置最靠後。
 * @param event
 * @return
 */
private boolean isPointOnViews(MotionEvent event) {
    boolean result=false;
    Rect rect=new Rect();
    for (int i=getChildCount()-1;i>=0;i--){
        View child = getChildAt(i);
        rect.set((int)child.getX(),(int)child.getY(),(int)child.getX()+child.getWidth(),(int)child.getY()+child.getHeight());

        if (rect.contains((int)event.getX(),(int)event.getY())){
            //標記被拖拽的child
            mDragView=child;
            mDragViewOrigX=mDragView.getX();
            mDragViewOrigY=mDragView.getY();
            result=true;
            break;
        }
    }
    return result&&mCurrentState!=State.DRAGGING;
}

知道了View的滑動,我們還要知道如何實現View的彈性滑動,比較生硬地滑動過去,這種方式的用戶體驗實在太差了,因此我們要實現漸近式滑動。那麼如何實現彈性滑動呢?
其實實現方法有很多,但是它們都有一個共問思想:將一次大的滑動分成若干次小的滑動
並在一個時間段內完成,彈性滑動的具體實現方式有很多,比如通過Scroller、

Handler#postDelayed以及Thread#sleep等,下面--進行介紹。

3.3.1  使用Scroller

Scroller的使用方法在3.1.4節中已經進行了介紹,下面我們來分析一下它的源碼,從而探究爲什麼他能實現View的彈性滑動

private Scroller scroller;
scroller=new Scroller(context, new Interpolator() {
    @Override
    public float getInterpolation(float t) {
        t-=1.0f;
        return t*t*t*t*t+1.0f;
    }
});

//緩慢滾動到指定位置
private void smoothScrollTo(int destX,int destY){
    int scrollX=getScrollX();//返回該View左側的位置
    int deltaX=destX-scrollX;
    //1000ms內滑向destX,效果就是慢慢滑動
    scroller.startScroll(scrollX,0,deltaX,0,1000);
    invalidate();
}
@Override
public void computeScroll() {
    if (scroller.computeScrollOffset()){
        scrollTo(scroller.getCurrX(),scroller.getCurrY());//這裏做了真實滾動
        postInvalidate();
    }
}
上面是Scroller的典型的使用方法,這裏先描述它的工作原理:當我們構造一個Scroller對象並且調用它的startScroller方法時,Scroller內部其實什麼都沒做,它只是保存了我們傳遞的幾個參數,這幾個參數從startScroller的原型就可以看出來,如下所示。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}
這個方法的參數含義很淸楚,startX和startY表示的是滑動的起點,dx和dy表示的是要滑動的距離,而duration表示的是滑動時間,即整個滑動過程完成所需要的時間,注意這裏的滑動是指View內容的滑動而非View本身位置的改變。可以看到,僅僅調用startScroll方法是無法讓View滑動的,因爲它內部並沒有做滑動相關的事,那麼Scroller到底是如何讓View彈性滑動的呢?答案就是startScroll方法下面的invalidate方法,雖然有點不可思議,但是的確是這樣的。invalidate方法會導致View重繪,在View的draw方法中又會去調用computeScroll方法,computeScroll方法在View中是一個空實現,因此需要我們自己
去實現,上面的代碼己經實現了computeScroll方法。正是因爲這個computeScroll方法,View才能實現彈性滑動。這看起來還是很抽象,其實這樣的:當View重繪後會在draw方法中調用computeScroll,而computeScroll又會去向Scroller獲取當前的scrollX和scrollY;然後通過scrollTo方法實現滑動;接着又調用postInvalidate方法來進行第二次重繪,這一次重繪的過程和第一次重繪一樣,還是會導致computeScroll方法被調用;然後繼續向Scroller獲取當前的scroHX和scrollY,並通過scrollTo方法滑動到新的位置,如此反覆,苴到整個滑動過程結束。

我們再吞一下Scroller的computeScrollOffset方法的實現,如下所示。
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

if (timePassed < mDuration) {
    switch (mMode) {
    case SCROLL_MODE:
        final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
        mCurrX = mStartX + Math.round(x * mDeltaX);
        mCurrY = mStartY + Math.round(x * mDeltaY);
        break;
    return true;
}
是不是突然就明白了?這個方法會根據時間的流逝來計算出當前的scrollX和scrollY的值。計算方法也很簡單,大意就是根據時間流逝的百分比來算出scrollX和scrollY改變的百分比並計算出當前的值,這個過程類似於動畫中的插值器的概念,這裏我們先不去深究這個具體過程。這個方法的返回值也很裏要,它返回true表示滑動還未結束,false則表示滑動已經結束,因此當這個方法返回true時,我們要繼續進行View的滑動.通過上面的分析,我們應該明白Scroller的工作原理了,這裏做一下概括:Scroller本身並不能實現View的滑動,它需要配合View的computeScroll方法才能完成彈性滑動的效果,它不斷地讓View重繪,而每一次重繪距滑動起始時間會冇一個時間間隔,通過這個時間間隔Scroller就可以得出View當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成View的滑動。就這樣,View的每一次重繪都會導致View進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作機制。由此可見,Scroller的設計思想是多麼值得稱讚,整個過程中它對View沒有絲毫的引用,甚至在它內部連計時器都沒有。


3.3.2 通過動畫

動畫本身就是一種漸進的過程,因此通過它來實現的滑動天然就具有彈性效果,比如以下代碼可以讓一個View的內容在100ms內向左移動100像素。

ObjectAnimator.ofFloat(textView,"translationX",0,100).setDuration(100).start();

不過這裏想說的並不是這個問題哦們可以利用動畫的特性來實現一些動不能實現的效果,還拿scrollTo來說,我們也想模仿Scroller來實現View的彈性滑動,那麼利用動畫的特性,我們可以採用如下方式來實現:

final int startX=0;
final int deltaX=100;
ValueAnimator animator=ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float fraction = animation.getAnimatedFraction();//得到的是完成的動畫比例1/5這種
        textView.scrollTo(startX+(int) (deltaX*fraction),0);
    }
});

在上述代碼中,我們的動畫本質上沒有作用於任何對象上,它只是在1000ms內完成了整個動畫過程。利用這個特性,我們就可以在動畫的每一幀到來時獲取動畫完成的比例,然後再根據這個比例計算出當前View所要滑動的距離。注意,這裏的滑動針對的是View的內容而非View本身。可以發現,這個方法的思想其實和Scroller比較類似,都是通過改變一個百分比配合scrollTo方法來完成View的滑動。需要說明一點,採用這種方法除/了能夠完成彈性滑動以外,還可以實現其他動畫效果,我們完全可以在onAnimationUpdafc方法中加上我們想要的其他操作。

3.3.3使用延時策略

本節介紹另外一種實現彈性滑動的方法,那就是延時策略。它的核心思想是通過發送—系列延時消總從而達到一種漸近式的效果.具體來說可以使用Handler或View的postDelayed方法,也可以使用線程的sleep方法。對子postDelayed方法來說,我們可以通過它來延時發送一個消息,然後在消息中來進行View的滑動,如果接連不斷地發送這種延時消息.那麼就可以實現彈性滑動的效果。對sleep方法來說,通過在while循環中不斷地滑動View和sleep,就可以實現彈性滑動的效果。


下面採用Handler來做個舉例,其他方法請讀者自行去嘗試,思想都是類似的。下面的代碼在大約1000ms內將View的內容向左移動了100像素,代碼比較簡單,就不再詳細介紹了,之所以說大約1000ms,是因爲採用這種方式無法精確地定時,原因是系統的消息調度也是需要耍時間的,並且所需時間不定。

private static final int MESSAGE_SCROLL_TO=1;
private static final int FRAME_COUNT=30;
private static final int DELAYED_TIME=33;
private int count=0;


@SuppressLint("HandlerLeak")
private Handler handler=new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case MESSAGE_SCROLL_TO:
                count++;
                if (count<FRAME_COUNT){
                    float fraction=count/(float)FRAME_COUNT;
                    int scrollX= (int) (fraction*100);
                    textView.scrollTo(scrollX,0);//從0-100滑動,根據百分比滑動對應距離
                    handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                }
                break;
           default:
               break;
        }
    }
};

上面幾種彈性滑動的實現方法,在介紹中側重更多的是實現思想,在實際使用中可以對其靈活地進行擴展從而實現更多複雜的效果。

3.4 View的事件分發機制

上面幾節介紹了View的基礎知識以及View的滑動,本節將介紹View的一個核心知識點:事件分發機制。事件分發機制不僅僅是核心知識點更是難點,不少初學者甚至中級開發者面對這個M題時都會覺得困惑。另外,View的另一大難題滑動衝突,它的解決方法的理論基礎就是事件分發機制,因此掌握好View的事件分發機制是十分重要的。本節將深入介紹View的事件分發機制,在3.4.1節會對事件分發機制進行概括性地介紹,而在3.4.2節將結合系統源碼去進一步分析事件分發機制。

3.4.1點擊事件的傳遞規則

在介紹點擊事件的傳遞規則之前,首先我們要明白這裏要分析的對象就是MotionEvent,即點擊亊件,關於MotionEvent在3.1節中已經進行了介紹。所謂點擊事件的事件分發,其實就是對MotionEvent事件的分發過程,即當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發過程。點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我們先介紹一下這幾個方法.

public boolean dispatchTouchEvent(MotionEventev)

用來進行事件的分發。如採事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

public boolean onlnterceptTouchEvent(MotionEventevent)

在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

public boolean onTouchEvent(MotionEventevent)

在dispatchTouchEvem方法中調用,用來處理點擊亊件,返回結果表示是否消耗當前

事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。

上述三個方法到底有什麼區別呢?它們是什麼關係呢?其實它們的關係可以用如下僞代碼表示:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume=false;
    if (onInterceptTouchEvent(ev)){
        consume=onTouchEvent(ev);
    }else{
        consume=child.dispatchTouchEvent(ev);
    }
    return consume;
}

上述僞代碼已經將三者的關係表現得淋漓盡致。通過上面的僞代碼,我們也可以大致瞭解點擊事件的傳遞規則:對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onlnterceptTouchEvent方法返回true就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用:如果這個ViewGroup的onlnterceptTouchEvent方法返回false就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此反覆直到事件被最終處理。

當一個View需要處理事件時,如果它設置了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,則當前View的onTouchEvent方法會被調用:如果返回true,那麼onTouchEvent方法將不會被調用。由此可見,給View設置的OnTouchListener,其優先級比onTouchEvent要高。在onTouchEvent方法中,如果當前設置的有OnClickListener,那麼它的onClick方法會被調用。可以看出,平時我們常用的OnClickUstener,其優先級最低,即處於事件傳遞的尾端。

當一個點擊事件產生後,它的傳遞過程遵循如下順序:Activity-> Window-> View,即車件總是先傳遞給Activity, Activity再傳遞治Window,最後Window再傳遞給頂級View。頂級View接收到車件後,就會按照事件分發機制去分發事件。考慮一種情況,如採一個View的onTouchEvent返回false,那麼它的父容器的onTouchEvent將會被調用,依此類推。如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調用。這個過程其實也很好理解,我們可以換一種思路,假如點擊事件是一個難題,這個難題埔終被上級領導分給了一個程序員去處理(這是亊件分發過

程),結果這個程序員搞不定(onTouchEvem返回了 false),現在該怎麼辦呢?難題必須要解決,那隻能交給水平更高的上級解決(上級的onTouchEvent被調用),如果上級再搞不定,那隻能交給上級的上級去解決,就這樣將難題一層層地向上拋,這是公司內部一種很常見的處理問題的過程。從這個角度來卷,View的事件傳遞過程還是很貼近現實的,畢竟程序員也生活在現實中。

關於事件傳遞的機制,這裏給出一些結論,根據這些結論可以更好地理解整個傳遞機制,如下所示。

(1)同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一系列審件,這個車件序列以down車件開始,中間含有數量不定的move事件,最終以up事件結束。

(2)正常情況下,一個事件序列只能被同一個View攔截並且消耗,這一條的原因可以參考(3)因爲一旦一個元索攔截了此事件,那麼同一個爭件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊手段可以做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。

(3)某個View—旦決定攔截,那麼這一個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不會再被調用。這條也很好理解,就是說當一個View決定攔截一個U件後,那麼系統會把同一個事件序列內的其他方法都直接交給它來處理,因此就不用再調用這個View的onlnterceptTouchEvent去詢問它是否要攔截

(4)某個View—旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent的action_down返回了false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被調用。意思就是事件一旦交給一個View處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程序員一件事,如果這件事沒有處理好,短期內上級就不敢再把事情交給這個程序員做了,二者是類似的道理。

(5)如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到後續的爭件,最終這些消失的點擊事件會傳遞給Activity處理。

(6)ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouch-Event方法默認返回false。

(7)View沒有onlnterceptTouchEvent方法,一旦有點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。

(8)View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時爲false)。View的longClickable屬性默認都爲false,clickable屬性要分情況,比如Button的clickableM性默認爲true,TextView的clickableM性默認爲false。

(9)View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個View是disable狀態的,只要它的clickable或者longClickable有一個爲true,那麼它的onTouchEvent就返回true。

(10)onClick會發生的前提是當前View是可點擊的,並且它收到了down和up的事件

(11)事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisanowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTIOISLDOWN事件除外。

3.4.2   事件分發的源碼解析

上一節分析了View的事件分發機制,本節將會從源碼角度去進一步分析,證實上面的結論。

1.Activity對點擊事件的分發過程

點擊事件用MotionEvent來表示,當一個點擊操作發生時,事件最先傳遞給當前Activity,由Activity的dispatchTouchEvent來進行事件派發,具體的工作是由Activity內部的Window來完成的。Window會將事件傳遞給decor view,decor view—般就是當前界面的底層容器(即setContentView所設置的View的父容器),通過Activity.getWindow.getDecorView()可以獲得。我們先從Activity的dispatchTouchEvent開始分析。

                                                  源碼:Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

現在分析上面的代碼。首先事件開始交給Activity所附屬的Window進行分發,如果返回true,整個亊件循環就結朿了,返回false意味若亊件沒人處理,所冇View的onTouchEvent都返回了false,那麼Activity的onTouchEvent就會被調用。接下來看Window是如何將事件傳遞給ViewGroup的,通過源碼我們知道,Window是個抽象類,而Window的superDispatchTouchEvent方法也是個抽象方法,因此我們必須找到Window的實現類才行

                                            源碼:Window #superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event);

那麼到底Window的實現類是什麼呢/其實是PhoneWindow,這一點從Window的源碼中也可以看出來,在Window的說明中,有這麼一句話:


上面這段話的人概意思是:Window類可以控制頂級View的外觀和行爲策略,它的唯一實現位於android.policy.PhoneWindow中,當你要實例化這個Window類的時候,你並不知道它的細節,因爲這個類會被重構,只有一個工廠方法可以使用。儘管這看起來有點模糊,不過我們可以看一下android.policy.PhoneWindow這個類,儘管實例化的時候此類會被重構,僅是重構而已,功能是類似的。由於Window的唯一實現是PhoneWindow,因此接下來看一下PhoneWindow是如何處理點擊事件的,如下所示。
                源碼:PhoneWindow#superDispatchTouchEvent

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
到這裏邏輯就很清晰了,PhoneWindow將事件直接傳遞給了DecorView,這個DecorView是什麼呢?請看下面:



我們知道,通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這種方式就可以獲取Activity所設置的View,這個mDecor顯然就是getWindow().getDecorView()返回的View,而我們通過setContentView設置的View是它的—個子View。目前事件傳遞到了DecorView這裏,由於DecorView繼承自FrameLayout且是父View,所以最終事件會傳遞給View。換句話來說,事件一定會傳遞到View,不然應用如何響應點擊事件呢?不過這不是我們的重點,重點是事件到了View以後應該如何傳遞,這對我們更有用。從這裏開始,事件已經傳遞到頂級View了,即在Activity中通過setContentView所設置的View,另外頂級View也叫根View,頂級View—般來說都是ViewGroup。

3.頂級View對點擊事件的分發過程

關於點擊事件如何在View屮進行分發,上一節已經做了詳細的介紹,這裏再大致回顧—下。點擊事件達到頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的dispatchTouchEvent方法,然後的邏輯是這樣的:如果頂級ViewGroup攔截事件即onlnterceptTouchEvent返回true,則事件由ViewGrop處理,這時如果ViewGroup的mOnTouchListener被設置,則onTouch會被調用,否則onTouchEvent會被調用。也就是說,如果都提供的話,onTouch會屏蔽掉onTouchEvent。在onTouchEvent中,如果設置了mOnClickListener,則onClick會被調用。如果頂級ViewGroup不攔截事件,則事件會傳遞給它所在的點擊事件鏈上的子View,這時子View的dispatchTouchEvent會被調用。到此爲止,事件已經從頂級View傳遞給了下一層View,接下來的傳遞過程和頂級View是一致的,如此循環,完成整個事件的分發。


首先看ViewGroup對點擊事件的分發過程,其主要實現在ViewGroup的dispatchTouch-Event方法中,這個方法比較長。先看下面一段,很顯然,他描述的是當前View是否攔截點擊事件這個邏輯

// Check for interception.
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;
}

從上面代碼我們可以看出,ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:事件類型爲ACTION_DOWN或者mHrstTouchTarget!=null。ACTION_DOWN事件好理解.那麼mFirstTouchTarget!=null是什麼意思呢?這個從後面的代碼邏輯可以看出來,當事件由ViewGroup的子元索成功處理時,mFirstTouchTarget會被賦值並指向子元索.換種方式來說.當ViewGroup不欄截事件並將事件交由子元素處理時mFirstTouchTarget!=null。反過來,一旦事件由當前ViewGroup攔截時,mFirstTouchTarget!=null就不成立。那麼當ACTION_MOVE和ACTION_UP車件到來時,由於(actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null)這個條件爲false,將導致ViewGroup的onlnterceptTouchEvent不會冉被調用,並且同—序列中的其他事件都會默認交給它處理。


當然,這裏有一種特殊情況,那就是FLAG_DISALLOW_INTERCEPT標記位,這個標記位是通過requestDisallowInterceptTouchEvent方法來設置的,一般用於子View中。FLAG_DISALLOW_INTERCEPT—旦設置後,ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件。爲什麼說事除了ACTION_DOWN以外的其他事件呢?這是因爲ViewGroup在分發事件時,如果是ACTION_DOWN就會重置FLAG_DISALLOWJNTERCEPT這個標記位,將導致子View中設置的這個標記位無效。因此,當面對ACTION_DOWN事件時,ViewGroup總是會調用自己的onlnterceptTouchEvent方法來詢問自己是否要攔截中件,這一點從源碼中也可以看出來。


在下面的代碼中,ViewGroup會在ACTION_DOWN事件到來時做重置狀態的操作,而在resetTouchState方法中會對FLAG_DlSALLOWJNTERCEPT進行重賈,因此子View調用request-DisallowInterceptTouchEvent方法並不能影響ViewGroup對ACTION_DOWN事件的處理。

// Handle an initial 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();
}

從上面的源碼分析,我們可以得出結論:當ViewGroup決定攔截事件後,那麼後續的點擊事件將會默認交給它處理並且不再調用它的onlnterceptTouchEvent方法,這證實了3.4.1節末尾處的笫3條結論。FLAG_DISALLOWJNTERCEPT這個標誌的作用是讓ViewGroup不再攔截事件,當然前提是ViewGroup不攔截ACT10N_D0WN事件,這證實了3.4.1節末尾處的第11條結論。那麼這段分析對我們有什麼價值呢?總結起來有兩點:第一點,onlnterceptTouchEvent不是每次事件都會被調用的,如果我們想提前處理所有的點擊事件,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都會調用,當然前提是事件能夠傳遞到當前的ViewGroup;另外一點,FLAG_DISALLOW_INTERCEPT標記位的作用給我們提供了一個思路,當面對滑動衝突時,我們可以是不是考慮用這種方法去解決問題?關於滑動衝突,將在3.5節進行詳細分析。

接着再看當ViewGroup不攔截事件的時候,事件會向下分發給它的子View進行處理,這段代碼如下所示。

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = 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 there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        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);
        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();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

上面這段代碼邏輯也很淸晰,首先遍歷ViewGroup的所有子元素,然後判斷子元索是否能夠接收到點擊事件。是否能夠接收點擊事件主要由兩點來衡量:子元素是否在播動畫和點擊事件的座標是否落在子元素的區域內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給它來處理。可以看到,dispatchTransformedTouchEvent實際上調用的就是子元索的dispatchTouchEvent方法,在它的內部有如下一段內容,而在上面的代碼中child傳遞的不是null,因此它會直接調用子元素的dispatchTouchEvent方法,這樣事件就交由子元素處理了,從而完成了一輪車件分發。

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

如果子元素的dispatchTouchEvent返回true,這時我們暫時不用考慮事件在子元素內部是怎麼分發的,那麼mFirstTouchTarget就會被賦值同時跳出for循環,如下所示。

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
這幾行代碼完成了mFirstTouchTarget的陚值並終止對子元素的遍歷。如果子元素的dispatchTouchEvent返冋false,ViewGroup就會把事件分發給下一個子元索(如果還有下—個子元素的話)。

其實mFirstTouchTarget貞正的賦值過程是在addTouchTarget內部完成的,從下面的addTouchTarget方法的內部結構可以看出,mFirstTouchTarget其實是一種單鏈表結構。mFirstTouchTarget是否被賦值,將直接影響到ViewGroup對事件的攔截策略,如果mFirstTouchTarget爲null,那麼ViewGroup就默認攔截接下來同一序列中所有的點擊事件,這一點在前面已經做了分析。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

如果遍歷所有的子元索後事件都沒有被合適地處理,這包含兩種情況:第一種是ViewGroup沒有子元索:第二種是子元素處理了點擊事件,侶是在dispatchTouchEvent中返回了false,這一般是因爲子元素在onTouchEvent中返回了false。在這兩種情況下,ViewGroup會自ci處理點擊事件,這裏就證實了3.4.1打中的第4條結論,代碼如下所示。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

注意上面這這段代碼,這裏第三個參數child爲null,從前面的分析可以知道,它會調用super.dispatchTouchEvent(event).很顯然,這裏就轉到了View的dispatchTouchEvent方法,即點擊事件開始交由View來處理,請看下面的分析。

4.View對點擊事件的處理過程

View對點擊事件的處理過程稍微簡單一些,注意這裏的View不包括ViewGroup。先看它的dispatchTouchEvent方法,如下所示。

 if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        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;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

View對點擊事件的處理過程就比較簡單了,因爲View(這裏不包含ViewGroup)是一個單獨的元素,它沒有子元素因此無法向下傳遞事件,所以它只能自己處理事件。從上面的源碼可以看出View對點擊事件的處理過程,首先會判斷有沒有設置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那麼onTouchEvent就不會被調用,可見OnTouchListencr的優先級高於onTouchEvent,這樣做的好處是方便在外界處理點擊事件。接苕再分析onTouchEvem的實現。先看當View處於不可用狀態下點擊事件的處理過程,如下所示。很顯然,不可用狀態下的View照樣會消耗點擊事件,儘管它看起來不可用。

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return clickable;
}

接着,如果View設置有代理,那麼還會執行TouchDelegate的onTouchEvent方法,這個onTouchEvent的工作機制看起來跟OnTouchListener類似,這裏就不深入研究了。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

下面再看一下onTouchEvent中對點擊事件的具體處理。如下所示。

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                // take focus if we don't have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    // The button is being released before we actually
                    // showed it as pressed.  Make it show the pressed
                    // state now (before scheduling the click) to ensure
                    // the user sees it.
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();

                    // Only perform take click actions if we were in the pressed state
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;
    return true;
}

return false;

從上面的代碼來看,只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,不管它是不是DISABLE狀態,這就證實了3.4.1節末尾處的第8,第9,第10條結論。然後就是當ACTION_UP事件發生時,會觸發performClick方法,如果View設置了OnClickListener,那麼performClick方法內部會調用它的onClick方法,如下所示。

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

View的LONG_CLICICABLE屬性默認爲false,而CLICKABLE屬性是否爲false和具體的View有關,確切來說垃可點擊的View其CLICKABLE爲true,不可點擊的View其CLICKABLE爲false,比如Button逛可點擊的,TextView是不可點擊的?通過setClickable

和setLongCHckable可以分別士變View的CLICKABLE和LONG_CLICKABLE屬性。另外,setOnClickListener會自動將View的CLICKABLE設爲true,setOnLongClickListener則會自動將View的LONG_CUCICABLE設爲true,這一點從源碼中可以苻出來,如下所示。

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}

到這裏,點擊事件的分發機制的源碼實現已經分析完了,結合3.4.1節中的理論分析和相關結論,讀者就可以更好地理解事件分發了。在3.5節將介紹滑動衝突相關的知識,具體情況請看下面的分析。

3.5 View的滑動衝突

本節幵始介紹View體系中一個深入的話題:滑動衝突。相信開發Android的人都會有這種體會:滑動衝突實在是太坑人了,本來從網上下載的demo運行得好好的,但是隻要出現滑動衝突,demo就無法正常工作了。那麼滑動衝突是如何產生的呢?其實在界面中只要內外兩層同時可以滑動,這個時候就會產生滑動衝突。如何解決滑動衝突呢?這既是一件困難的事又是一件簡單的事,說困難是因爲許多幵發着面對滑動衝突都會顯得束手無策,說簡單是因爲滑動衝突的解決有固定的套路,只要知道了這個固定套路問題就好解決了。本節是View體系的核心章節,前面4節均是爲本節服務的,通過本節的學習,滑動衝突將不再是個問題。

        3.5.1常見的滑動衝突場景

常見的滑動衝突場景可以簡單分爲如下三種(詳情請參看圖3-4):

場景一----外部滑動方向和內部滑動方向不一致:

場景二----外部滑動方向和內部滑動方向一致:

場景三---上面兩種情況的嵌套。


先說場景1,主要是將ViewPager和Fragment配合使用所組成的頁面滑動效果,主流應用幾乎都會使用這個效果。在這種效果中,可以通過左右滑動來切換頁面,而每個頁面內部往往又是一個ListView。本來這種愔況下是有滑動衝突的,但是ViewPager內部處理了這種滑動衝突,因此採用ViewPager時我們無須關注這個問題,如果我們採用的不是ViewPager而是ScrollView等,那就必須手動處理滑動衝突了,否則造成的後果就足內外兩層只能有一層能夠滑動,這是因爲兩者之間的滑動事件有衝突。除了這種典型情況外,還存在其他情況,比如外部上下滑動、內部左右滑動等,但是它們屬於同一類滑動衝突。

再說場景2,這種情況就稍微複雜一些,當內外兩層都在同一個方向可以滑動的時候,顯然存在邏輯問題。因爲當手指開始滑動的時候,系統無法知道用戶到底是想讓哪一層滑動,所以當手指滑動的時候就會出現問題,要麼只有一層能滑動,要麼就是內外兩層都滑動得很卡頓。在實際的開發中,這種場景主要是指內外兩層同時能上下滑動或者內外兩層同時能左右滑動。

最後說下場景3,場景三是場景1和場景2種情況的嵌套,因此場景3的滑動衝突看起來就更加複雜了。比如在許多應用中會有這麼一個效果:內層有一個場景1中的滑動效果,然後外層又有一個場景2中的滑動效果。具體說就是,外部有一個SlideMenu效果,然後內部有一個ViewPager,ViewPager的每一個頁面中又是一個ListView。雖然說場景3的滑動衝突餚起來更復雜,但是它是幾個單一的滑動衝突的脅加,因此只需要分別處理內層和中層、中層和外層之間的滑動衝突即可,而具體的處理方法其實是和場景1、場景2相同的。從木質上來說,這三種滑動衝突場景的複雜度其實是相同的,因爲它們的區別僅僅是滑動策略的不同,至於解決滑動衝突的方法,它們幾個是通用的,在3.5.2節中將會詳細介紹這個問題。

            3.5.2滑動衝突的處理規則

—般來說,不管滑動衝突多麼複雜,它都有既定的規則,根據這些規則我們就可以選擇合適的方法去處理。

如圖34所示,對於場景1,它的處理規則是:當用戶左右滑動時,需要讓外部的View攔截點擊事件,當用戶上下滑動時,需要讓內部View攔截點擊事件。這個時候我們就可以根據它們的特徵來解決滑動衝突,具體來說是:根據滑動楚水平滑動還是豎直滑動來判斷到底由誰來攔截事件,如圖3-5所示,根據滑動過程中兩個點之間的座標就可以得出到底是水平滑動還是豎直滑動。如何根據座標來得到滑動的方向呢?這個很簡單,有很多可以參考,比如可以依據滑動路徑和水平方向所形成的夾角,也可以依據水平方向和豎直方向h的距離差來判斷,某些特殊時候還可以依據水平和豎直方向的速度差來做判斷。這裏我們可以通過水平和豎直方向的距離差來判斷,比如豎直方向滑動的距離大就判斷爲豎直滑動,否則判斷爲水平滑動。根據這個規則就可以進行下一步的解決方法制定了。

對於場景2來說,比較特殊,它無法根據滑動的角度、距離差以及速度差來做判斷,但是這個時候一般都能在業務上找到突破點,比如業務上有規定:當處於某種狀態時需要外部View響應用戶的滑動,而處於另外一種狀態時則需要內部View來響應View的滑動,根據這種業務上的需求我們也能得出相應的處理規則,有了處理規則同樣可以進行下一步處理。這種場景通過文字描述可能比較抽象,在下一節會通過實呩的例子來演示這種情況的解決方案,那時就容易理解了,這裏先有這個概念即可.

對於場景3來說,它的滑動規則就更復雜了,和場景2—樣,它也無法直接根據滑動的角度、距離差以及速度差來做判斷,同樣還是隻能從業務上找到突破點,具體方法和場景2—樣,都是從業務的需求上得出相應的處理規則,在下一節將會通過實際的例子來演示這種情況的解決方案。

3.5.3 滑動衝突的解決方式

在3.5.1節中描述了三種典型的滑動衝突場景,在本節將會一一分析各種場景並給出具體的解決方法。首先我們要分析第一種滑動衝突場景,這也是最簡單、最典型的一種滑動衝突,因爲它的滑動規則比較簡單,不管多複雜的滑動衝突,它們之間的區別僅僅是滑動規則不同而已。拋開滑動規則不說,我們需要找到一種不依賴具體的滑動規則的通用的解決方法,在這裏,我們就根據場景1的情況來得出通用的解決方案,然後場景2和場景3我們只需要修改有關滑動規則的邏輯即可。

上面說過,針對場景1中的滑動,我們可以根據滑動的距離差來進行判斷,這個距離差就是所謂的滑動規則。如果用ViewPager去實現場景1中的效果,我們不需要手動處理滑動衝突,因爲ViewPager已經幫我們做了,但是這裏爲了更好地演示滑動衝突的解決思想,沒有采用ViewPager。其實在滑動過程中得到滑動的角度這個是相當簡單的,但是到底要怎麼做才能將點擊事件交給合適的View去處理呢?這吋就要用到3.4節所講述的事件分發機制了。針對滑動衝突,這裏給出兩種解決滑動衝突的方式:外部攔截法和內部攔截法

1 外部攔截法

所謂外部攔截法是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動衝突的問題,這種方法比較符合點擊事件的分發機制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在內部做相應的攔截即可,這種方法的僞代碼如下所示。

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:
            if (父容器需要當前點擊事件){
                intercepted=true;
            }else{
                intercepted=true;
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
         default:
             break;
    }
    mLastX=x;
    mLastY=y;
    return intercepted;
}

上述代碼是外部攔截法的典型邏輯,針對不同的滑動衝突,只需要修改父容器需要當前點擊亊件這個條件即可,其他均不需做修改並且也不能修改。這裏對上述代碼再描述一下,在onlnterceptTouchEvent方法中,萏先是ACTION_DOWN這個事件,父容器必須返回false,即不攔截ACTION_DOWN事件,這是因爲一旦父容器攔截了ACTION_DOWN,那麼後續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,這個時候事件沒法再傳遞給子元素了;其次是ACTION_MOVE事件,這個事件可以根據需要來決定是否攔截,如果父容器需要攔截就返回true,否則返回false;最後是ACTION_UP事件,這裏必須要返回false,因爲ACTION_UP事件本身沒有太多意義。考慮一種情況,假設事件交由子元素處理,如果父容器在ACTION_UP時返回了true,就會導致子元素無法接收到ACTION_UP事件,這個時候子元索中的onClick事件就無法觸發,但是父容器比較特殊,一旦它開始攔截任何一個事件,那麼後續的事件都會交給它來處理,而ACTION_UP作爲最後一個事件也必定可以傳遞給父容器,即便父容器的onlnterceptTouchEvent方法在ACTION_UP時返回了false。


2.內部攔截法

內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元索需要此事件就直接消耗掉,否則就交由父容器進行處理,這種方法和Android中的車件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來較外部攔截法稍顯複雜。它的僞代碼如下,我們需要重寫子元素的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
    int x= (int) event.getX();
    int y= (int) event.getY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            int deltaX=x-mLastX;
            int deltaY=y-mLastY;
            if (父容器需要此類點擊事件){
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    mLastX=x;
    mLastY=y;
    return super.dispatchTouchEvent(event);
}

上述代碼是內部攔截法的典型代碼,當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動而且也不能有改動。除了子元素需要做處理以外,父元素也要默認攔截除了ACT10N_D0WN以外的其他事件,這樣當子元素調用parent.requestDisal-lowInterceptTouchEvent(false)方法時,父元索才能繼續攔截所需的事件。

爲什麼父容器不能攔截ACT10N_D0WN事件呢?那是因爲ACT10N_D0WN事件並不受FLAG_DlSALLOWJNTERCEPT這個標記位的控制,所以一旦父容器攔截ACTION_DOWN事件,那麼所有的事件都無法傳遞到子元素中去,這樣內部攔截就無法起作用了。父元素所做的修改如下所示。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action=ev.getAction();
    if (action==MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return true;
    }
}
下而通過一個實例來分別介紹這兩種方法。我們來實現一個類似於ViewPager中嵌套ListView的效果,爲了製造滑動衝突,我們寫一個類似於ViewPager的控件即可,名字就叫HorizontalScrollViewEx,這個控件的具體實現思想會在第4章進行詳細介紹,這裏只講述滑動衝突的部分。

爲了實現ViewPager的效果,我們定義了一個類似於水平的LinearLayout的東西,只不過它可以水平滑動,初始化時我們在它的內部添加若干個ListView,這樣一來,由於它內部的Listview可以豎直滑動。而它本身又可以水平滑動,因此一個典型的滑動衝突場景就出現了,並且這種衝突屬於類型1的衝突。根據滑動策略,我們可以選擇水平和豎直的滑動距離差來解決滑動衝突。


首先來看一下Activity中的初始化代碼,如下所示。



上述初始化代碼很簡單,就是創建了 3個ListView並且把ListView加入到我們自定義的 HorizontalScrollViewEx 中,這裏 HorizontalScrollViewEx 是父容器,而ListView則是子元索,這裏就不冉多介紹了。

首先採用外部攔截法來解決這個問題,按照前面的分析,我們只需要修改父容器需要攔截事件的條件即可。對於本例來說,父容器的攔截條件就是滑動過程中水平距離差比豎直距離差大,在這種情況下,父容器就攔截當前點擊事件,根據這一條件進行相應修改,修改後的 HorizontalScrollViewEx 的 onInterceptTouchEvent 方法如下所示。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted=false;
    float x = ev.getX();
    float y = ev.getY();
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN://默認返回false,不然後續事件將都由父容器處理
            intercepted=false;
            if (!mScroller.isFinished()){//滑動還未結束時點擊了,則停止動畫
                mScroller.abortAnimation();
                intercepted=true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            int deltaX= (int) (x-mLastX);
            int deltaY= (int) (y-mLastY);
            if (Math.abs(deltaX)>Math.abs(deltaY)){
                intercepted=true;
            }else{
                intercepted=true;
            }
            break;
        case MotionEvent.ACTION_UP://默認返回false,不然子控件的onClick無法響應
            intercepted=false;
            break;
         default:
             break;
    }
    mLastX=x;
    mLastY=y;
    return intercepted;
}

從上面的代碼來看,它和外部攔截法的僞代碼的差別很小,只是把父容器的攔截條件換成了具體的邏輯。在滑動過程中,當水平方向的距離大時就判斷爲水平滑動,爲了能夠水平滑動所以讓父容器攔截亊件;而豎直距離大時父容器就不攔截事件,於是事件就傳遞給了 ListView,所以ListView也能上下滑動,如此滑動衝突就解決了,至於mScroller.abortAnimationO這一句話主要是爲了優化滑動體驗而加入的。


        考慮一種情況,如果此時用戶正在水平滑動,伹是在水平滑動停止之前如果用戶再迅速進行豎直滑動,就會導致界面在水平方向無法滑動到終點從而處於一種中間狀態。爲了避免這種不好的體驗,當水平方向正在滑動時,下一個序列的點擊事件仍然交給父容器處理,這樣水平方向就不會停留在中間狀態了。

下面是HorizontalScrollViewEx的具體實現,只展示了和滑動衝突相關的代碼:

public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG="HorizontalScrollViewEx";

    private int mChildrenSize;//子控件個數
    private int mChildWitdh;//子控件寬度
    private int mChildIndex;//當前子控件的index

    //分別記錄上次滑動的座標
    private float mLastX=0;
    private float mLastY=0;

    //分別記錄上次滑動的座標(onInterceptTouchEvent)
    private float mLastXIntercept=0;
    private float mLastYIntercept=0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    ...

    private void init() {
        mScroller=new Scroller(getContext());
        mVelocityTracker=VelocityTracker.obtain();
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted=false;
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN://默認返回false,不然後續事件將都由父容器處理
                intercepted=false;
                /**
                 *  考慮一種情況,如果此時用戶正在水平滑動,伹是在水平滑動停止之前如果用戶再迅速進行豎直滑動,
                 *  就會導致界面在水平方向無法滑動到終點從而處於一種中間狀態。爲了避免這種不好的體驗,當水平方向正在滑動時
                 *  ,下一個序列的點擊事件仍然交給父容器處理,這樣水平方向就不會停留在中間狀態了。
                 */
                if (!mScroller.isFinished()){//滑動還未結束時點擊了,則停止動畫
                    mScroller.abortAnimation();
                    intercepted=true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX= (int) (x-mLastX);
                int deltaY= (int) (y-mLastY);
                if (Math.abs(deltaX)>Math.abs(deltaY)){
                    intercepted=true;
                }else{
                    intercepted=true;
                }
                break;
            case MotionEvent.ACTION_UP://默認返回false,不然子控件的onClick無法響應
                intercepted=false;
                break;
            default:
                break;
        }
        Log.d(TAG,"intercept="+intercepted);
        mLastX=x;
        mLastY=y;
        mLastXIntercept=x;
        mLastYIntercept=y;
        return intercepted;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        float x = getX();
        float y = getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX= (int) (x-mLastX);
                int deltaY= (int) (y-mLastX);
                scrollBy(-deltaX,0);//左右移動
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                int scrollToChildIndex=scrollX/mChildWitdh;
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity)>=50){//模仿ViewPager左滑右滑,當前Container的index改變
                    mChildIndex=xVelocity>0 ? mChildIndex-1:mChildIndex+1;
                }else{
                    mChildIndex=(scrollX+mChildWitdh/2)/mChildWitdh;//滑動到一半,根據scrollX的正負判斷index
                }
                mChildIndex=Math.max(0,Math.min(mChildIndex,mChildrenSize-1));//index最小0,最大子控件個數-1
                int dx = mChildIndex * mChildWitdh - scrollX;
                smoothScrollBy(dx,0);
                mVelocityTracker.clear();
                break;
            default:
                break;
        }
        mLastX=x;
        mLastY=y;
        return true;
    }
    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(),0,dx,0,500);
        invalidate();
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
    ...
}

如果採用內部攔截法也是可以的,按照前面對內部攔截法的分析,我們只需要修改ListView的dispatchTouchEvent方法中的父容器的攔截邏輯,同時讓父容器攔截ACTION_MOVE和ACTION_UP事件即可。爲了重寫ListView的dispatchTouchEvcnt方法,我們必須自定義一個ListV^iew,稱爲ListViewEx,然後對內部攔截法的模板代碼進行修改,根據需要,ListViewEx的實現如下所示。

public class ListViewEx extends ListView {
    private static final String TAG="ListViewEx";
    private HorizontalScrollViewEx2 mHorizontalScrollViewEx;

    //分別記錄上次滑動的座標
    private float mLastX=0;
    private float mLastY=0;

    ...

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX= (int) (x-mLastX);
                int deltaY= (int) (y-mLastY);
                if (Math.abs(deltaX)>=Math.abs(deltaY)){
                    mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX=x;
        mLastY=y;
        return super.dispatchTouchEvent(ev);
    }
}

除了上面對ListView所做的修改,我們還需要修改HorizontalScrollViewEx的onInterceptTouchEvent方法,修改後的類暫且叫HorizontalScrollViewEx2,其onInterceptTouchEvent方法如下所示。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    float x = ev.getX();
    float y = ev.getY();
    int action = ev.getAction();
    if (action == MotionEvent.ACTION_DOWN) {//默認返回false,不然後續事件將都由父容器處理
        mLastX = x;
        mLastY = y;
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            return true;
        }
        return false;
    } else {
        return true;//假設子控件OnTouchEvent的DOWN返回true,事件上傳到父容器進行處理
    }
}

上面的代碼就是內部攔截法的示例,其中mScroller.abortAnimation()這一句不是必須的,在當前這種情形下主要是爲了優化滑動體驗。從實現上來餚,內部攔截法的操作要稍微複雜一些,因此推薦採用外部攔截法來解決常見的滑動衝突。

前面說過,只要我們根據場景1的情況來得出通用的解決方案,那麼對於場景2和場景3來說我們只需要修改相關滑動規則的邏輯即可,下面我們就來演示如何利用場景1得出的通用的解決方案來解決更復雜的滑動衝突。這裏只詳細分析場景2中的滑動衝突,對於場景3中的疊加型滑動衝突,由於它可以拆解爲單一的滑動衝突,所以其滑動衝突的解決思想和場景1、場景2中的單一滑動衝突的解決思想一致,只需要分別解決每層之間的滑動衝突即可,再加上本書的篇幅有限,這裏就不對場景3進行詳細分析了。

對於場景2來說,它的解決方法和場景1—樣,只是滑動規則不同而已,在前面我們
已經得出了通用的解決方案,因此這裏我們只需要替換父容器的攔截規則即可。注意,這
裏不再演示如何通過內部攔截法來解決場景2中的滑動衝突,因爲內部攔截法沒有外部攔
截法簡單易用,所以推薦採用外部攔截法來解決常見的滑動衝突。


下面通過一個實際的例子來分析場景2,首先我們提供一個可以以上下滑動的父荇器,這裏就叫StickyLayout,它看起來就像是可以上下滑動的豎直的LinearLayout,然後在它的內部分別放一個Header和一個ListView,這樣內外兩層都能上下滑動,於是就形成了場景2中的滑動衝突了。當然這個StickyLayout 是有滑動規則的:當Header顯示時或者ListView滑動到頂部時,由StickyLayout攔截事件;當Header隱藏時,這要分情況,如果ListVicw己經滑動到頂部並且當前手勢是向下滑動的話,這個時候還是StickyLayout攔截事件,其他情況則由ListView攔截事件。這種滑動規則看起來有點複雜,爲了解決它們之間的滑動衝突,我們還是需耍重寫父容器StickyLayout的onlnterceptTouchEvent方法,至於ListView則不用做任何修改,我們來看一下StickyLayout的具體實現,滑動衝突相關的主要代碼如下所示。

                    暫無

我們來分析上面這段代碼的邏輯,這裏的父容器是StickyLayout,子元素是ListView。首先,當事件落在Header上面時父容器不會攔截事件;接着,如果豎直距離差小於水平距離差,那麼父容器也不會攔截事件;然後,當Header是展開狀態並且向上滑動時父容器攔截事件。另外一種情況,當ListView滑動到頂部了並且向上滑動時,父容器也會攔截事件,經過這些層層判斷就可以達到我們想要的效果了.另外,giveUpTouchEvent是一個接口方法,由外部實現,在本例中主要是用來判斷ListView是否滑動到頂部,它的具體實現如下:


上面這個例子比較複雜,需要讀者多多體會其中的寫法和思想,到這裏滑動衝突的解決方法就介紹完畢了,至於場景3中的滑動衝突,利用本節所給出的通用的方法是可以輕鬆解決的,讀者可以自己練習一下。在第4章會介紹View的底層工作原理,並且會介紹如何寫出一個好的自定義View。同時,在本節中所提到的兩個自定義View: Horizontal-ScrollViewEx和StickyLayout將會在第4章中進行詳細的介紹,它們的完整源碼請査看本書所提供的示例代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章