View的事件體系《Android開發藝術探索》筆記

文章目錄

1 View的基礎知識

1.1什麼是View

View是所有控件的基類,View是一種界面層的控件的一種抽象,它代表了一個控件。ViewGroup是一個控件組,內部包含多個View,同時ViewGroup也是繼承自View,這樣就形成了View樹結構。Button就是一個View,而LinearLayout是一個View,同時還是一個ViewGroup。ViewGroup內部可以是單個View,也可以是一個ViewGroup。

1.2位置參數

View的位置參數包括,top,bottom,left,right,它們分別代表的是與父容器之間的相對距離。值得一提的是,x軸是向右,y軸是向下的。

  • left: View 左上頂點相對於父容器的橫座標
  • top: View 左上頂點相對於父容器的縱座標
  • right: View右下頂點相對於父容器的橫座標
  • bottom: View 右下頂點相對於父容器的縱座標
    在這裏插入圖片描述
    View同時還具有x,y,translationX,translationY,x和y是View左上角的座標(相對於父容器),但往往x和y可能不等於left與top,原因就是translationX與translationY,這個是View左上角相對於父容器的偏移量。
x = translationX+left
y = translationY+top

下圖就可以很好的體現出translation的作用,原本的父容器相當於是黑框,而有了translationX、Y之後的父容器相當於變成了綠框,而我們的View與綠框之間的距離不變,仍然爲left與top。可以從圖中看出下列式子。

x = translationX+left
y = translationY+top

在這裏插入圖片描述
因此我們看出在每次平移(有translation的時候),top與left都不會改變,而xy會相應發生變化。

1.3 MotionEvent與Touchslop

1.MotionEvent
手指滑動包含三個狀態:手指接觸屏幕,手指在屏幕移動,手指移出屏幕
ACTION_DOWN:手指接觸屏幕
ACTION_MOVE:手指在屏幕移動
ACTION_UP:手指移出屏幕
包含兩種情況:
1)DOWN->UP
2)DOWN->MOVE->MOVE->MOVE->UP
通過MotionEvent對象可以獲取點擊事件的座標:getX與getY(相對於當前View左上角的x與y座標);getRawX與getRawY(相對於手機屏幕左上角的x與y座標)
2.Touchslop
可以通過兩次點擊之間的距離的大小與Touchslop比較來判斷是否是滑動,Touchslop就是最小滑動距離。
代碼中通過下述方法獲取。

ViewConfiguration.get(getContext()).getScaledTouchSlope()

1.4 VelocityTracker、GestureDetector和Scroller

1.VelocityTracker

用來追蹤手指滑動的速度,包括水平速度與垂直速度。

調用方法:
在View的onTouchEvent中使用。

//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();

//在onTouchEvent方法中
mVelocityTracker.addMovement(event);

//獲取速度
mVelocityTracker.computeCurrentVelocity(1000);

int xVelocity = mVelocityTracker.getXVelocity();
int yVelocity = mVelocityTracker.getYVelocity();
//重置和回收

mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用

mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用

使用總結

(1)滑動速度可以爲負,當我們向座標軸反向滑動就爲負

(2)當我們獲取速度的時候,一定要先使用computeCurrentVelocity()方法,在使用getXVelocity()方法。代碼中的1000是1000ms,假設在1000ms內水平向右滑動了100像素,那麼水平速度就爲100(像素/1s)。當傳入的是100ms,那麼水平速度就爲10(像素/100ms)。因此公式可以總結如下:
速度 = (終點位置 - 起點位置) / 時間段,這個時間段是由我們傳入的參數決定。

當我們不使用了需要將其回收。

//重置和回收

mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用

mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用

2.GestureDetector

手勢檢測,用於輔助檢測單擊,滑動,長按,雙擊等手勢。

需要首先創建一個GestureDetector的對象,並實現OnGestureListener的接口,爲了需要還可以實現OnDoubleTapListener用於監聽雙擊事件。

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

3.Scroller

彈性滑動對象,(由於View自身的ScrollTo和scrollBy方法進行滑動,是一瞬間完成,用戶體驗較差)。因此使用Scroller用於實現View的彈性滑動。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。

2.View的滑動

大概有三種方式:
1.通過View自帶的ScrollTo與ScrollBy方法
2.通過動畫給View施加平移效果
3.通過改變View的LayoutParams使得View重新佈局從而實現滑動

1.通過View自帶的ScrollTo與ScrollBy方法

1.使用scrollTo/scrollBy scrollTo和scrollBy方法只能改變view內容的位置而不能改變view在佈局中的位置。
2. scrollBy是基於當前位置的相對滑動(如果傳入的位置和當前位置相同就不動),而scrollTo是基於所傳參數的絕對滑動(調用scrollBy並傳入(當前位置+所傳參數))。
在這裏插入圖片描述
3.mScrollx與mScrollY可以通過View的getScrollX和getScrollY方法可以得到,以mScrollX爲例,它是由View的左邊緣到View中內容的左邊緣的距離決定的。邊緣就是指四個頂點。當內容的左邊緣在View左邊緣的左邊時,mScrollX大於0,反之小於0。

這裏也可以看出由原始狀態到第二個狀態需要scrollTo(100,0),也就是說如果想要讓View中的內容左移,就要將傳入大於0的x,其他三個方向同理。
在這裏插入圖片描述
簡單的嘗試下上述方法。由於只能移動View中的內容,這裏思維不要侷限,單個View的內容就是裏面的內容,比如TextView的內容,而ViewGroup的內容就是其中的View。我現在有一個ConstraintLayout,其中包含三個Button,那麼我對Constraintlayout使用scrollTo,更改的就是三個Button的位置。

 ConstraintLayout constraintLayout = findViewById(R.id.layout);
        constraintLayout.scrollTo(-100,-100);

在這裏插入圖片描述
在這裏插入圖片描述
可以看到三個Button(ViewGroup中的內容)向右下移動。

2.通過動畫給View施加平移效果

使用動畫來移動View主要是更改View的translationX與translationY,以更改View左上角相對於父容器之間的偏移量。既可以使用傳統的view動畫,也可以使用屬性動畫。使用後者需要考慮兼容性問題,如果要兼容Android3.0以下版本系統的話推薦使用nineoldandroids。

使用動畫還存在一個交互問題:在android3.0以前的系統上,view動畫和屬性動畫,新位置均無法觸發點擊事件,同時,老位置仍然可以觸發單擊事件。從3.0開始,屬性動畫的單擊事件觸發位置爲移動後的位置,view動畫仍然在原位置。

3.通過改變View的LayoutParams使得View重新佈局從而實現滑動

這種方法簡單易懂,例如我們要將Button向右移100px,只需要將其marginleft+100px即可。我們還可以在Button左邊放一個width爲0的view,如果想要右移100pxButton,就將View的width設爲100px。

ViewGroup.MarginLayoutParams mp = (ViewGroup.MarginLayoutParams) mybutton.getLayoutParams();
        mp.leftMargin+=100;
        mybutton.setLayoutParams(mp);

在這裏插入圖片描述在這裏插入圖片描述

4.三種方式的比較(重要)

1.scrollTo/scrollBy方式:適合於View內容的滑動,因爲View無法滑動
2.動畫方式:3.0版本以上採用這種方式沒有明顯缺陷,3.0以下版本,使用屬性動畫和View動畫均不能改變View的屬性,影響用戶交互。(新位置無法觸發點擊事件,而老位置還可以觸發點擊事件)。
3.通過更改View的layoutparams使得View重新佈局:操作複雜,適合於需要與用戶交互

5.跟手滑動

這裏是使用更改其layoutparams的參數來滑動,每一次回調OnTouchEvent時都記錄觸摸點的位置(這裏記錄的是getX和getY,相對於View本身的座標)

當用戶的動作是按下時,更新上一次的位置;當用戶動作是滑動的時候,計算上次的位置與當前觸摸點位置的差值,並將差值與View當前所在位置的值加和,更新View的位置。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        //每次回調onTouchEvent的時候,我們獲取觸摸點的代碼
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 記錄觸摸點座標
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 計算偏移量
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                // 在當前left、top、right、bottom的基礎上加上偏移量
                layout(getLeft() + offsetX,
                        getTop() + offsetY,
                        getRight() + offsetX,
                        getBottom() + offsetY);
                break;
        }
        return true;
    }

3.彈性滑動(重要)

1.使用Scroller

核心思想:使用Scroller可以將View中的內容緩慢的移動,而非一下子到達指定位置。不過僅僅使用Scroller無法完成功能,需要與View的computeScroll函數相結合,才能完成彈性滑動。它不斷地讓view重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出view的當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成view的滑動。就這樣,view的每一次重繪都會導致view進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作原理

主要代碼如下。首先創建Scroller對象,然後調用smoothScrollTo函數,並傳入滑動到的指定位置。


    Scroller scroller = new Scroller(getContext());

    private void smootthScrollTo(int destX,int destY){
        int scrollX = getScrollX();
        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();
        }
    }

startScroll方法做到的就是保存參數,其中比較重要的是mDurationReciprocal,代表的是單位延遲。其他比較好理解,start的都是初始位置,d的都是最終滑動的終點,duration代表滑動時長。

 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;
    }

滑動功能在於invalidate方法。在該方法中會重新繪製View,會調用computeScroll函數。我們的computeScroll函數中會不斷的將當前View中的內容向當前的ScrollX與ScrollY滑動,並繼續調用postIncalidate方法來進行第二次重繪,第二次重繪重複第一重繪的過程,不斷的遞歸,直到滑動截至。其實一個緩慢滑動就是多次滑動的結果。

接下來看computeScrollOffset方法。首先他有個boolean變量來判斷是否返回false來終止遞歸。主要代碼中通過判斷當前距離開始時間走過的時間是否小於滑動總時間,小於就需要繼續滑動,否則就不用繼續滑動了。

如果處於小於的情況下,計算當前走過的時間對應總時間的百分比,進而計算出距離需要改變的百分比並與初始位置加和後分別賦給mCurrx與mCurry,並返回true。

    /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        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;
           ...
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

流程圖

在這裏插入圖片描述

2.使用動畫

我們可以在動畫的每一幀到來時獲取動畫完成的比例,然後再根據這個比例計算出當前View所要滑動的距離,並通過scrollTo來移動。同時可以通過onAnimationUpdate方法做更多的事情。

    final int startX = 0;
        final int startY = 100;
        final  int deltaX = 0;
        final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float fraction = animator.getAnimatedFraction();
                mybutton.scrollTo(startX + (int)(deltaX * fraction),0);
            }
        });

3.使用延時策略

它的核心思想是通過發送一系列延時消息從而達到一種漸近式的效果,可以使用Handler或者View的postDelayed方法,也可以使用線程的sleep方法。

以postDelayed爲例,我們可以通過它來延時發送一個消息,然後在消息中來進行View的滑動,如果接連不斷地發送這種延時消息,那麼就可以實現彈性滑動的效果,Handler與其相似。

代碼以Handler爲例。

private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MESSAGE_SCROLL_TO: {
                mCount++;
                if (mCount <= FRAME_COUNT) {
                    float fraction = mCount / (float) FRAME_COUNT;
                    int scrollX = (int) (fraction * 100);
                    mButton1.scrollTo(scrollX, 0);
                    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                }
                break;
            }

            default:
                break;
            }
        };
    };

Button的點擊函數

 @Override
    public void onClick(View v) {
        if (v == mButton1) {
            mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
        }
    }

整體流程:

點擊button後,會發送一個具有延遲的Message,handler在消息處理時首先count++,然後會根據當前count值,來判斷應該滑動的距離,滑動之後,繼續發送相同的具有延遲的Message以達到遞歸效果,直至count大於閾值,終止遞歸,直接跳出,完成整個滑動過程。

4.View的事件分發(重要)

4.1點擊事件的傳遞規則

1.三個重要函數(dispatchTouchEvent、onInterceptTouchEvent與onTouchEvent)

首先,要明確我們分析的對象是MotionEvent(點擊事件)。點擊事件的事件分發,其實就是對MotionEvent事件的分發過程,即當一個MotionEvent發生後,系統要傳遞給一個具體的View,這個傳遞過程就是分發過程。其中包含三個很重要的函數。

1.public boolean dispatchTouchEvent(MotionEvent ev)
描述:用來進行事件的分發。如果事件能夠傳遞給當前的View,那麼此方法一定會被調用。
返回值:受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。返回true表示事件被消費,本次的事件終止。返回false表示View以及子View均沒有消費事件,將調用父View的onTouchEvent方法

2.public boolean onInterceptTouchEvent(MotionEvent event)
描述:在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用。(因爲理論上只要View攔截了一個事件,系統就會把整個事件序列都分發給這個View
返回值:返回結果表示是否攔截當前事件。返回false表示不做攔截,事件將向下分發到子View的dispatchTouchEvent方法。

3.public boolean onTouchEvent(MotionEvent event)
描述:在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前的事件
返回值:返回true表示事件被消費,本次的事件終止。返回false表示事件沒有被消費,將調用父View的onTouchEvent方法

三個方法的關係可以如下所示:

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

總流程:對於一個根ViewGroup,點擊事件發生後,會先傳遞給它,它的dispatchTouchEvent就會被調用,如果它要攔截這個事件,onInterceptTouchEvent就會返回true,這個事件就會交給當前的ViewGroup處理,也就是去調用onTouchEvent方法;如果不攔截這個事件,onInterCepterTouchEvent會返回false,進而傳遞給ViewGroup的子View去處理,也就是調用子View的dispatchTouchEvent。持續上述過程,直到該點擊事件被處理。

2.onTouchListener的優先級大於OnTouchEvent

當一個View要處理事件,同時設置了OnTouchListenr,那麼OnTouchListener中的OnTouch方法就會回調,如果返回false則會去繼續調用OnTouchEvent,否則不調用。在onTouchEvent方法中,如果當前view設置了OnClickListener,那麼它的onClick方法會被調用,所以OnClickListener的優先級最低

3.點擊事件的傳遞過程:Activity->window->View

如果一個View的onTouchEvent方法返回false,那麼它的父容器的onTouchEvent方法將會被調用,依次類推,如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理(調用Activity的onTouchEvent方法)

4.總結(重要)

1.一個事件序列是由一個Action_down,一個Action_up與多個Action_Move組成。

2.正常情況下,一個事件序列只能被一個View所攔截與消耗。(當View一旦決定攔截事件,整個事件序列都將交給它來處理,並且它的onInterceptTouchEvent不會再被調用。)

3.某個View開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那麼整個事件序列剩下的事件也不會給它處理,會給它的父View處理(調用父View的onTouchEvent)。如果它只不消耗ACTION_DOWN事件,但消耗剩餘的事件,這個點擊事件(ACTION_DOWN)就會消失,View可以持續收到剩下的事件,而這個點擊事件最終會被Activity處理。

4.ViewGroup默認不攔截事件,它的onInterceptTouchEvent默認返回false,View沒有onInterceptTouchEvent方法,只要事件傳遞給它,就會默認調用onTouchEvent。

5.View的onTouchEvent默認會返回true,除非不可點擊(clickable和Longclickable都爲false)。一般View的Longclickable爲false,而clickable分不同View,比如Button默認爲true,而TextView爲false

6.View的enable(可用性)不影響onTouchEvent的返回值。即使View是不可用的(disable),只要有一個屬性(clickable和longclickable)爲true,onTouchEvent就返回true。

7.onclick會發生的前提實際當前的View是可點擊的,並且他收到了down和up的事件。

8.事件傳遞的過程總是先發給父元素,再由父元素髮給子View。

流程圖(複習的時候可以畫一畫這個流程圖)

來自一文讀懂Android View事件分發機制

1.View的結構圖
主要是樹形結構圖,同時涉及包括了點擊事件的產生:Activity->Window->頂級View->分發到子View
在這裏插入圖片描述
2. View事件分發流程
主要是一個整體的分發過程,第一層是根ViewGroup,第二層時子ViewGroup,第三層是子View,其中包括區分了View與ViewGroup在事件分發中的不同(View沒有onIntercepterTouchEvent方法,會直接調用View的onTouchEvent)
在這裏插入圖片描述

4.2源碼分析

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

點擊事件最先是傳遞到Activity,因此首先看Activity的dispatchTouchEvent方法

Class Activitypublic boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//事件分發並返回結果
            return true;//事件被消費
        }
        return onTouchEvent(ev);//沒有View可以處理,調用Activity onTouchEvent方法
    }

可以看到事件傳遞到Activity後,會傳遞至預期綁定的window,如果最終返回false,則說明沒有一個View可以消費這個事件,就返回Acitivity的onTouchEvent,交由Activity處理。

2.Window對點擊事件的分發過程

我們繼續跟進getWindow.superDispatchTouchEvent()方法,會發現這是一個抽象方法。

Window類說明
/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
Class Window:
//抽象方法,需要看PhoneWindow的實現
public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window是一個抽象類,而唯一實現的類就是phonewindow類,繼續跟進phonewindow類

class PhoneWindow
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到我們將事件傳遞到DecorView,而這個DecorView是Window的頂級View,我們通過setContentView設置的View是它的子View。

此時事件已經傳遞至頂級View,也就是我們設置的View。

3.View對點擊事件的分發

頂級View一般是ViewGroup,我們首先去看ViewGroup的dispatchTouchEvent方法。

ViewGroup會首先判斷是否攔截該事件

mFirstTouchTarget變量是一個單向的鏈式節點,當ViewGroup自己不處理而選擇給子View處理時,會將這個變量指向子View。

   class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
     	// 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();
            }
        // Check for interception.
        final boolean intercepted;//是否攔截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通過
            //requestDisallowInterceptTouchEvent方法進行設置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調用onInterceptTouchEvent方法判斷是否需要攔截
                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;
        }
        ...
    }

(1)當事件爲Action_down或者mFirstTouchTarget不爲空時,會選擇去判斷是否攔截這個事件。
注意,子View可以通過requestDisallowInterceptTouchEvent方法去設置FLAG_DISALLOW_INTERCEPT,來阻止ViewGroup去攔截除了Action_down以外的事件。

//FLAG_DISALLOW_INTERCEPT是子View通過
            //requestDisallowInterceptTouchEvent方法進行設置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

而當動作爲ACTION_DOWN的時候,則無論子View設置與否都會去判斷是否攔截。如下述代碼,當點擊事件爲ActionDown,會重置FLAG_DISALLOW_INTERCEPT與mFirstTouchTarget。因此,當點擊事件爲Action_down,無論子View是否設置FLAG_DISALLOW_INTERCEPT都無法阻止ViewGroup去攔截。

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

(2)如果事件不爲ACTION_DOWN 且事件爲ViewGroup本身處理(即mFirstTouchTarget ==null)那麼intercepted = true,很顯然事件已經交給自己處理根本沒必要再調用onInterceptTouchEvent去判斷是否攔截

結論(重要)

當ViewGroup選擇攔截事件時

(1)如果View選擇自身處理這個事件,則事件序列中其他事件也不需要判斷是否攔截。直接會默認爲攔截。

(2)子View可以使用requestDisallowInterceptTouchEvent方法去設置 FLAG_DISALLOW_INTERCEPT來阻止ViewGroup攔截除了ACTION_DOWN以外的其他事件。

(3)Action_Down事件不受子View設置標誌位與否的影響,都會去判斷是否需要攔截。

ViewGroup選擇不攔截這個事件並分發給子View

核心思路是遍歷ViewGroup中的子元素,判斷子元素是否能夠接收到事件(子元素是否在播動畫和點擊時按的座標是否落在子元素的區域內),接下來的重要函數是dispatchTransformedTouchEvent函數。

                        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;
                            }

在dispatchTransformedTouchEvent方法中有一個判斷,判斷傳入的child是否爲空。最後該函數會返回一個boolean型變量,如果返回true,就會將mFirstTouchTarget(單向鏈式節點)賦值同時跳出for循環。

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

當ViewGroup的子View全部被遍歷後,會執行以下判斷,最後如果mFirstTouchTarget變量爲空,就會讓ViewGroup自己處理。具體處理方法是使用dispatchTransformedTouchEvent方法,其中第三個參數傳入null

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

注意,由於傳入的child爲null,如前文代碼可知,這時候在dispatchTransformedTouchEvent方法中就會執行super.dispatchTouchEvent(event),也就是讓ViewGroup自己處理。

結論(重要)

當ViewGroup選擇不攔截事件時

1.ViewGroup會遍歷所有子View去尋找能夠處理點擊事件的子View(可見,沒有播放動畫,點擊事件座標落在子View內部),最終會調用子View的dispatchTouchEvent方法處理事件。

2.如果ViewGroup中有一個View可以處理事件,就會將mFirstTouchTarget賦值,並停止ViewGroup中對View的尋找。

3.如果ViewGroup遍歷了所有子View都無法處理事件,就會讓ViewGroup自己處理事件。

4.ViewGroup對事件的分發,是通過對View(子View或本身)調用dispatchTouchEvent來處理。

4.View對事件的處理

注意:這裏的View不包含ViewGroup。
View不包含子View,因此無法向下傳遞事件,只能自己處理事件。觀察下述代碼可以看到一個先後順序,首先判斷是否有OnTouchLinstener的onTouch函數,如果返回的是true,就不調用onTouchEvent函數了。

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
   		...
        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;
            }
        }
        ...
        return result;
    }

繼續看onTouchEvent中對事件的處理,可以看到只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼他就會消耗這個事件,即onTouchEvent返回true,不管他是不是DISABLE狀態。

public boolean onTouchEvent(MotionEvent event) {
		...
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
  						...
                        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();
                                }
                            }
                        }
                 black;
             }
        ....
    return true;
    }
    return false;
 }

如果事件是ACTION_UP,且當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);
        return result;
    }

View的LONG_CLICKABLE屬性默認爲false,而CLICKABLE屬性是否爲false和具體的View有關,確切的說是可點擊的View其CLICKABLE爲true,不可點擊的爲false。比如button是可點擊的,textview是不可點擊的,通過setonclik或者longclik都是可以改變狀態的,這點我們看源碼:

 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;
    }

總結

1.View對事件的處理中具有優先級。如果設置了onTouchListener,就會首先調用onTouch,返回false後,會調用onTouchEvent。

2.在onTouchEvent中,不考慮View是否可用,只要Clickable和Longclickable有一個爲true,就可以消耗事件,而Longclickable默認爲false,clickable根據不同的View有不同(TextView爲false,Button爲true)

3.如果設置了onClickListener,onClick就會在onTouchEvent中的performClick()函數中被調用。

整體腦圖

在這裏插入圖片描述

5.滑動衝突

5.1外部攔截法

點擊事件都經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,該方法只需要重寫父容器的onInterceptTouchEvent方法,在內部做相應的攔截即可,僞代碼如下:

    @Override
    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 = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = x;
        return intercepted;
    }

在onInterceptTouchEvent方法中,首先是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事件就無法觸發。

5.2內部攔截法

父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就由父容器進行處理,這種方法和Android中的事件分發機制不一樣,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

    @Override
    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 =  x - mLastY;
                if("父容器的點擊事件"){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

上述代碼就是內部攔截法的典型代碼,當面對不同的滑動策略只需要修改裏面的條件即可,其他不需要做改動,除了子元素需要處理之外,父元素默認也要攔截除ACTION_DOWN之外的其他事件,這樣當子元素調用getParent().requestDisallowInterceptTouchEvent(true)方法時,父元素才能繼續攔截所需要的事件

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if(action == MotionEvent.ACTION_DOWN){
            return false;
        }else {
            return true;
        }
    }

面試題精選

1.MotionEvent是什麼?包含幾種事件?什麼條件下會產生?

我的回答

MotionEvent是用戶觸摸屏幕而產生的事件。
包含四類:1.Action_down,當手指放下時會產生。
2.ACTION_MOVE,當手指在屏幕上滑動時會產生。
3.ACTION_UP:當手指從屏幕中移開會產生
4.ACTION_CANCEL:當手指按下後,從一個View轉到另一個View會產生。
ACTION_MOVE系統可以通過兩次移動的距離,來判斷是否爲滑動。

參考答案

注意:ACTION_CANCEL回答的不好,這裏可以再看下參考答案的
MotionEvent是手指觸摸屏幕鎖產生的一系列事件。包含的事件有:
ACTION_DOWN:手指剛接觸屏幕
ACTION_MOVE:手指在屏幕上滑動
ACTION_UP:手指在屏幕上鬆開的一瞬間
ACTION_CANCEL:手指保持按下操作,並從當前控件轉移到外層控件時會觸發。

2.scrollTo()和scrollBy()的區別?

我的回答

區別在於scrollTo是相對滑動,而scrollBy是絕對滑動。scrollTo會比較傳入的位置與當前位置,如果兩個位置相同則不會滑動。而scrollBy在源碼中調用scrollTo,只不過傳入的參數是當前位置加傳入的位置。也就是說調用scrollBy,view一定會滑動傳入的參數的距離。

參考答案

scrollBy內部調用了scrollTo,它是基於當前位置的相對滑動;而scrollTo是絕對滑動,因此如果利用相同輸入參數多次調用scrollTo()方法,由於View初始位置是不變只會出現一次View滾動的效果而不是多次。

這個引申非常重要,我在作答的時候忘記這一點了。
引申:兩者都只能對view內容進行滑動,而不能使view本身滑動,且非平滑可使用Scroller有過渡滑動的效果

3.Scroller中最重要的兩個方法是什麼?主要目的是?

我的回答

先簡述一下Scroller滑動的整個過程,Scroller可以實現平滑的滑動,但也是對View內容的滑動,Scroller不能通過自身完成整個過程,需要藉助View的computeScroll方法

當ACTION_UP事件發生後,會調用startScroll方法,這個方法只是賦值參數,並沒有做到滑動。之後會調用invalidate方法,在這個方法中,會對View重繪,就會調用computeScroll方法,而這個方法需要我們重寫。在這個方法中首先會調用computeScrollOffSet方法,該方法判斷是否還需要繼續滑動,在這個方法中通過當前已走過的時間佔滑動總時間的佔比,來計算當前應該滑動的值。當該方法返回true的時候,在cmputeScroll方法中調用scrollTo方法,完成滑動,並繼續調用postinvalidate方法,直到結束滑動。

因此我認爲最重要的兩個函數分別是1.需要重寫的computeScroll方法,2.invilate方法,因爲是通過這個方法與computeScroll方法聯繫起來,並完成一次一次的滑動。

參考答案

參考答案說的沒有我細緻,不過感覺更加有邏輯性,這個流程圖很好,可以參考。

  • 在MotionEvent.ACTION_UP事件觸發時調用startScroll()方法,該方法並沒有進行實際的滑動操作,而是記錄滑動相關量。
  • 馬上調用invalidate/postInvalidate()方法,請求View重繪,導致View.draw方法被執行。緊接着會調用View.computeScroll()方法,此方法是空實現,需要自己處理邏輯。
  • 具體邏輯是:先判斷computeScrollOffset(),若爲true(表示滾動未結束),則執行scrollTo()方法,它會再次調用postInvalidate(),如此反覆執行,直到返回值爲false。流程圖如下:
    在這裏插入圖片描述

4.談一談View的事件分發機制?

5.如何解決View的滑動衝突?

6.onTouch()、onTouchEvent()和onClick()關係?

參考鏈接

1.2019校招Android面試題解1.0(上篇)
2.《Android開發藝術探索》
3.LearningNotes

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