Android進階學習(三)View的事件體系

文本是閱讀《Android開發藝術探索》的學習筆記記錄,詳細內容可以自行閱讀書本。

View的事件體系

1 View基礎知識

1.1 什麼是View

  View是Android中所有控件的基類。不管是簡單的Button和TextView還是複雜的RelativeLayout和ListView,它們的共同基類都是View。還有ViewGroup也繼承了View,它內部可以包含多個View。

1.2 View的位置參數

  View的位置主要由它四個頂點確定,分別對應View的四個屬性:top、left、right、bottom。在Android中,它的座標系與我們所看到的有區別,如圖所示。

在這裏插入圖片描述

1.3 MotionEvent和TouchSlop

  1)MotionEvent

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

ACTION_DOWN ——手指剛接觸屏幕
ACTION_MOVE ——手指在屏幕上移動
ACTION_UP ——手指從屏幕上鬆開

  2)TouchSlop
  TouchSlop是系統所能夠識別出的被認爲是滑動的最小距離。可以通過ViewConfiguration.get(this).getScaledTouchSlop()獲取。

1.4 Scroller

  彈性滑動對象,用於實現View的彈性滑動。當使用View的scrollTo/scrollBy時,其過程是瞬間完成的。這個時候就可以用Scroller來實現過渡滑動效果。它需要和View的computeScroll方法配合使用,如下:

scroller = new Scroller(context);

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }

2 View的滑動

  通過三種方式可以實現View的滑動:第一種是通過View本身提供的scrollTo/scrollBy方法;第二種是通過動畫實現滑動;第三種是通過修改View的LayoutParams使得View重新佈局從而實現滑動。

2.1使用scrollTo/scrollBy

  先來看看兩個方法的源碼

    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

  首先scrollBy方法也是調用了scrollTo方法,scrollBy方法是相對當前位置移動,而scrollTo是指定位置的絕對移動。通過先判斷傳進來的(x, y)值是否和View的X, Y偏移量相等,如果不相等,就調用onScrollChanged()方法來通知界面發生改變,然後重繪界面。
  舉個例子,調用兩次scrollTo(-10, 0),View第一次會向左滑動10,第二次不變化。調用兩次scrollBy(-10, 0),View第一次向左滑動10,第二次再向左滑動10。
  我們要先理解View 裏面的兩個成員變量mScrollX, mScrollY,X軸方向的偏移量和Y軸方向的偏移量,這個是一個相對距離,相對的不是屏幕的原點,而是View的左邊緣和上邊緣。

  變化規律如圖所示:
在這裏插入圖片描述

2.2 使用動畫

  上一篇介紹過了,直接使用屬性動畫。

        ObjectAnimator.ofFloat(stopWebBt, "translationY", -startWebBt.getHeight()).start();

2.3 改變佈局參數

  修改LayoutParams參數

ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams)
                        button.getLayoutParams();
                marginLayoutParams.leftMargin += 100;
                button.requestLayout();

3 彈性滑動

  彈性滑動的方式很多,比如通過Scroller、動畫、延時策略等等

3.1 使用Scroller

	//彈性滑動到指定位置
    public void smoothScrollto(int destX, int destY) {
        int x = getScrollX();
        int deltaX = destX - x;
        //1000ms內
        scroller.startScroll(x, 0, deltaX, 0, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }

  下面探索一下Scroller的內部工作原理,我們調用Scroller的startScroll方法,其實Scroller內部什麼也沒有做,這是保存了傳遞進去的參數,源碼如下。

    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重繪,View重繪時又調用我們實現的computeScroll()方法,其中又調用postInvalidate()方法進行二次重繪,直到滑動結束。源碼如下:

    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;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

  該方法主要兩個作用,一個是判斷滑動是否結束,一個是如果未結束,根據時間流失的百分比來給CurrX和CurrY賦值,幫助滑動。

3.2 通過動畫

  動畫上篇結束了,就不多說了。動畫實現彈性滑動非常輕鬆,也提供了許多豐富的api。

3.3 延時策略

  它的核心思想結束通過發送延時消息來達到漸進式的效果,可以使用Handler的postDelayed方法;線程的sleep方法等等。

4 View的事件分發機制

4.1 點擊事件的傳遞

  點擊事件的分發由三個很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。
  dispatchTouchEvent,進行事件分發,如果事件傳遞給當前View,此方法一定被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent影響,表示是否消耗當前事件。
  onInterceptTouchEvent,內部調用,用來攔截某個事件,如果當前View攔截了事件,那麼同一個事件序列中,此方法不再調用,返回結果表示是否攔截當前事件。
  onTouchEvent,在dispatchTouchEvent方法中調用,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接受到事件。
  當一個點擊事件產生後,它的傳遞順序:Activity -> Window -> View。

4.2 事件分發的源碼分析

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

  當一個點擊操作發生時,事件最先傳遞給當前Activity,由activity的dispatchTouchEvent進行事件分發,具體工作由activity內部的Window來完成。Window會將事件傳遞給decor view,decor view一般就是當前界面的底層容器(即setContentView所設置的View的父容器)。

  先從activity的dispatchTouchEvent開始分析

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //Window進行分發
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //未有view消耗事件,則調用activity的onTouchEvent
        return onTouchEvent(ev);
    }

  首先事件交給activity所附屬的Window進行分發,如果返回true,整個事件結束。否則調用activity的onTouchEvent。

  2)Window點擊事件的分發過程

  由於Window唯一的實現是PhoneWindow,從源碼上看:

    @Override
    public boolean superDispatchGenericMotionEvent(MotionEvent event) {
        return mDecor.superDispatchGenericMotionEvent(event);
    }

  PhoneWindow將事件直接傳遞給了mDecor ,而mDecor = (DecorView) preservedWindow.getDecorView();也就是事件一定會傳遞到view。

  3)頂層View點擊事件的分發過程

  頂層ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由頂層ViewGroup處理,這時如果頂層ViewGroup的OnTouchListener被設置,則onTouch會被調用,否則onTouchEvent會被調用。如果頂層ViewGroup不攔截事件,則事件會傳遞給它所在的點擊事件鏈的子View。
  先看ViewGroup的攔截源碼

           // 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和mFirstTouchTarget。從後面代碼可知子View如果成功處理事件,會將mFirstTouchTarget賦值。即當ViewGroup不攔截事件交給子View處理時,mFirstTouchTarget != null。反之ViewGroup自己處理了該事件後,mFirstTouchTarget == null,onInterceptTouchEvent不再被調用,其餘點擊事件由ViewGroup自己處理。

  特殊情況,FLAG_DISALLOW_INTERCEPT標記位,這個是通過子View調用設置。原來控制ViewGroup不攔截除了ACTION_DOWN以外的其他點擊事件。因爲當ACTION_DOWN事件到來時,ViewGroup會重置FLAG_DISALLOW_INTERCEPT標記位。源碼如下:

            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決定攔截事件後,後續點擊事件不再調用onInterceptTouchEvent,直接交給它處理。

  再看ViewGroup不攔截事件時,事件會向下分發給它的子View處理。

                        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的子View,判斷子View是否能夠接收到點擊事件。兩點判斷:是否再播放動畫和點擊事件座標是否落在子View區域內。滿足則調用dispatchTransformedTouchEvent方法,部分源碼如下:

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

  該方法內如果傳入參數child 不爲空,則調用子View的dispatchTouchEvent方法。該方法返回true,則跳出for循環,並在addTouchTarget方法內對mFirstTouchTarget賦值。

  4)View對點擊事件的處理過程
  View是單獨元素,沒有子View進行傳遞,部分dispatchTouchEvent源碼:

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

  首先,判斷是否設置mOnTouchListener,有則返回true,並且不調用onTouchEvent。

5 View的滑動衝突

5.1 常見滑動衝突場景

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

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

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

在這裏插入圖片描述

5.2 滑動衝突的處理規則

  場景一:根據滑動是水平還是豎直來判斷誰來攔截事件

  場景二:需要根據業務來判斷,由誰來攔截事件

  場景一:需要根據業務來判斷,由誰來攔截事件

5.3 滑動衝突的解決方法

  1.外部攔截法
  是指經過父類容器的攔截處理,父類容器需要則攔截此事件,反之傳遞給子View處理。

    @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;
            default:
                break;
        }

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