Android原始碼角度分析事件分發消費之應用篇

Drummor發表於2017-09-27

上一篇 《原始碼角度分析事件分發消費》我們已經從原始碼角度瞭解了事件分發的過程和原始碼依據以及需要注意的點,相信大家和我一樣看完之後都想躍躍欲試,懂了這麼多道理想好好的過一下人生,這一篇就是實踐篇,讓我們看下懂了那些只是之後怎麼去運用。

自定義簡單ViewPager

首先我們做一個簡單的ViewPager,廢話不說了,上程式碼。
java 程式碼

public class ScrollerLayout extends ViewGroup {
    private final int mTouchSlop;
    private Scroller mScroller;
    private int leftBorder;
    private int rightBorder;
    private int downX;
    private int lastMoveX;
    private int moveX;


    public ScrollerLayout(Context context) {
        this(context,null);
    }

    public ScrollerLayout(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
        //這個地方是獲取系統預設判定為滑動的最小值
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //為測量每個子控制元件的大小
        int childCount = getChildCount();
        for(int i =0;i<childCount;i++){
            measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
        }

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(!changed){
           return;
        }
        int childCount = getChildCount();
        for(int i = 0;i<childCount;i++){
            View child =  getChildAt(i);
            child.layout(i*child.getMeasuredWidth(),0,(i+1)*child.getMeasuredWidth(),child.getMeasuredHeight());
        }
        leftBorder = getChildAt(0).getLeft();
        rightBorder = getChildAt(getChildCount() - 1).getRight();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                downX = (int) ev.getX();
                lastMoveX = downX;
                break;
            case MotionEvent.ACTION_MOVE:
                moveX = (int) ev.getX();
                float diff = Math.abs(moveX - downX);
                lastMoveX = moveX;
                // 當手指拖動值大於TouchSlop值時,認為應該進行滾動,攔截子控制元件的事件!
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = (int) event.getX();
                lastMoveX = downX;
            case MotionEvent.ACTION_MOVE:
                //etX()是表示Widget相對於自身左上角的x座標,而getRawX()是表示相對於螢幕左上角的x座標值
                moveX = (int) event.getRawX();
                int scrolledX = (lastMoveX - moveX);
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                lastMoveX = moveX;
                break;
            case MotionEvent.ACTION_UP:
                // 當手指抬起時,根據當前的滾動值來判定應該滾動到哪個子控制元件的介面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,呼叫startScroll()方法來初始化滾動資料並重新整理介面
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
       Flag2: //return true;
    }

    @Override
    public void computeScroll() {
        // 第三步,重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}複製程式碼

xml 佈局

<com.weshape3d.customviews.mycostomviews.ScrollerLayout
       android:layout_width="match_parent"
       android:layout_height="50dp">
      <TextView
          android:gravity="center"
          android:textColor="#ff0000"
          android:text="1111"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />
      <TextView
          android:gravity="center"
          android:textColor="#222"
          android:text="USD發hi愛的是否"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />
      <TextView
          android:gravity="center"
          android:textColor="#ff0000"
          android:text="333"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />
   </com.weshape3d.customviews.mycostomviews.ScrollerLayout>複製程式碼

主要實現的邏輯地方我加了註釋,自定義ViewGroup中佈局、測量不是本篇重點。 主要看根據手勢滑動簡單的實現的邏輯:

  • 當按下手勢觸發的時候,記錄下按下的X點不做攔截
  • 當滑動且大於等於預先設定的判定滑動的值的時候在onIntercept()方法中攔截手勢
  • 攔截後會交給他自身的OnTouchEvent去處理 利用View 自身的scrollBy()方法做滑動

    問題

    大家會發現把這段程式碼執行之後,會有左右滑不動!!

    原因

    原因很簡單因為ACTION_DOWN手勢按下的時候,沒有找到消費它的目標就導致後續的ACTION_MOVE 和ACTION_UP等事件得不到分發了!

    解決

  • 在佈局中把TextView換成Button;這樣之所以可以是因為Button控制元件預設是CLICKABLE的,這就導致當ACITION傳遞過來的時候,就被消費了,致使後續的事件也會傳遞過來,這樣在onInterceptTouchEvent()方法中當符合滑動條件的時候,我們就把事件攔截,在onTouchEvent()方法中讓ViewGroup做滑動。
  • 程式碼中Flag2處,讓ViewGroup的onTouchEvent,返回true。

RefreshSwipLayout怎麼進行事件消費和分發

Now,滿心歡喜的開啟開RefreshSwipLayout原始碼

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();

        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                mActivePointerId = ev.getPointerId(0);
                mIsBeingDragged = false;

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                mInitialDownY = ev.getY(pointerIndex);
                break;

            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                    return false;
                }

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                final float y = ev.getY(pointerIndex);
                startDragging(y);
                break;

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }複製程式碼

咦?跟預想的不太一樣!ACTION_POINTER_UP ACTION_POINTER_DOWN是什麼鬼很高階的樣子!先穩住。

  • ACTION_POINTER_UP,ACTION_POINTER_DOWN表示第一個手指之外的其他手指按下和抬起的時候,觸發的
  • 可以按這麼多手機,我怎麼知道哪是一個?有下面這些方法,有了這些就可以輕鬆的知道哪個手指了
//獲取觸控所屬手指的位置,也就是按時間先後放在螢幕上排序第幾個
int indext = event.getActionIndex();
//根據位置獲取手指ID
int pointID = event.getPointerId(index);
//根據ID獲取是第幾個
int index =  event.findPointerIndex(indext);複製程式碼
  • 獲取位置的時候
//獲取第幾個手指x
 float x = ev.getX(pointerIndex);複製程式碼

看透他的實質,拆穿他的本質之前,我們先拎著幾個問題:

  • 像我們上面自定義ViewPager一樣遇到的問題,RefreshSwipLayout是怎麼解決的?
  • 兩個手指多個手勢滑動的時候,怎麼處理?(多點觸控)

    SwipeRefreshLayout中的onInterceptTouchEvent

    看原始碼,必要的地方加了註釋和刪減
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();ensureTarget();
    //獲取多點觸控的動作
    final int action = MotionEventCompat.getActionMasked(ev);
    int pointerIndex;
    switch (action) {
        case MotionEvent.ACTION_DOWN:
        /**
        第一個手指按下時,獲取他的pointID。
        pointID是從觸控點在整個從按下到抬起過程中,唯一不變的id
        **/
            mActivePointerId = ev.getPointerId(0);
            mIsBeingDragged = false;
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            //記錄下按下的位置
            mInitialDownY = ev.getY(pointerIndex);
            break;

        case MotionEvent.ACTION_MOVE:
            //找到要跟蹤的觸控點pointerIndex,用它來獲取位置
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            final float y = ev.getY(pointerIndex);
            //滑動處理
            startDragging(y);
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }
    //返回是否攔截
    return mIsBeingDragged;
}

private void startDragging(float y) {
    final float yDiff = y - mInitialDownY;
    if (yDiff > mTouchSlop && !mIsBeingDragged) {
        mInitialMotionY = mInitialDownY + mTouchSlop;
        mIsBeingDragged = true;
        mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
    }
}複製程式碼

小結

  • 化繁就簡,在onInterceptTouchEvent()中做的主要的事情就是在按下的時候記錄住觸控點的ID也就是第一個觸控點的ID並把這個點的Y座標也記下來。
  • 在MOVE中找出找出我們記錄的觸控點,並把他當前的Y座標找出來,交給startDragging()判斷要不要攔截

SwipeRefreshLayou的OnTouchEvent

public boolean onTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    int pointerIndex = -1;

    switch (action) {
        case MotionEvent.ACTION_DOWN:
        //記錄下要用的觸控點的ID
            mActivePointerId = ev.getPointerId(0);
            mIsBeingDragged = false;
            break;

        case MotionEvent.ACTION_MOVE: {
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            startDragging(y);

            if (mIsBeingDragged) {
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                if (overscrollTop > 0) {
                    //滑動圓圈
                    moveSpinner(overscrollTop);
                } else {
                    return false;
                }
            }
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN: {
            //當其他手指按下的時候,就把使用的觸控點換成當前按下的這個
            pointerIndex = MotionEventCompat.getActionIndex(ev);
            if (pointerIndex < 0) {
                Log.e(LOG_TAG,
                        "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                return false;
            }
            mActivePointerId = ev.getPointerId(pointerIndex);
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP: {
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                return false;
            }
            //如果控制元件的狀態為拖動過程中,返回初始狀態
            if (mIsBeingDragged) {
                final float y = ev.getY(pointerIndex);
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                mIsBeingDragged = false;
                finishSpinner(overscrollTop);
            }

            mActivePointerId = INVALID_POINTER;
            return false;
        }
        case MotionEvent.ACTION_CANCEL:
            return false;
    }
    //預設消費這個事件
    return true;
}複製程式碼

我認為必要的地方,都在上面做了註釋。SwipRefreshLayout在onTouchEvent方法中做的主要工作簡單的說主要三點

  • 當手指按下的時候,記錄住觸控點的ID;結合我們上一篇事件原始碼分析,在SwipRefreshLayout在onTouchEvent中ACTION_DOWN這個動作被接受消費,是在SRL的子view沒有沒有攔截消費的情況下會交給他來處理。
  • SRL上滑動動作交給自己的onTouchEvent來處理,這個時候事件已經被攔截下來了,做控制元件的滑動工作。
  • 手指抬起的時候,控制元件原位復回。

相關文章