開源庫AndroidSwipeLayout分析(二),SwipeLayout源碼探究



如需轉載請注明博客出處: http://www.cnblogs.com/wondertwo/p/5525670.html


開源庫AndroidSwipeLayout地址請戳: https://github.com/daimajia/AndroidSwipeLayout 開源庫作者 @代碼家


從上面的開源庫目錄可以看出,SwipeLayout是整個swipe效果實現的基石,我們就從源碼着手,SwipeLayout.java文件的源碼較為復雜龐大,有1600+行代碼,所以閱讀源碼的正確的打開姿勢是這樣的:分清主線,只抓重點!所以我們重點去看SwipeLayout在TouchEvent事件處理方面是怎樣實現的。本篇博客分三個部分來寫:

  • SurfaceView和BottomView繪制();
  • TouchEvent事件傳遞(多層嵌套不破壞事件傳遞是怎樣做到的?);
  • Listener監聽與回調(設置隱藏百分比、過渡動畫效果);

第一部分 SurfaceView和BottomView繪制

SwipeLayout從本質上來說,可以看作一個自定義的View控件,繼承自幀布局FrameLayout。通過我的前一篇博客,相信同學們已經了解了,它的布局其實是由上層的SurfaceViews和下層的BottomViews疊加在一起的。先來看構造方法,SwipeLayout有3個重載的構造方法,主要做一些初始化工作。我們需要關注一下mDragEdges這個成員變量,mDragEdges是一個記錄滑動方向DragEdge的哈希表,一共記錄Left,Right,Top,Bottom四個方向值。

有過自定義View經驗的同學都很清楚要處理好兩件事情:View繪制和事件處理。ViewGroup需要先遍歷View樹中所有子View,通過onMeasure()方法對其大小進行測量,調用onLayout()方法把它顯示出來。按照這個思路去看源碼,果然,SwipeLayout重寫了onLayout()方法,由於Android原生android.view.View.OnLayoutChangeListener is added in API 11,為了兼容到api 8,作者做了OnLayout接口抽象,一共涉及到一個接口和四個成員方法,把這部分代碼單獨拿出來如下:

/**
 * {@link android.view.View.OnLayoutChangeListener} added in API 11. I need
 * to support it from API 8.
 */
public interface OnLayout {
    void onLayout(SwipeLayout v);
}

private List<OnLayout> mOnLayoutListeners;

public void addOnLayoutListener(OnLayout l) {
    if (mOnLayoutListeners == null) mOnLayoutListeners = new ArrayList<OnLayout>();
    mOnLayoutListeners.add(l);
}

public void removeOnLayoutListener(OnLayout l) {
    if (mOnLayoutListeners != null) mOnLayoutListeners.remove(l);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    updateBottomViews();

    if (mOnLayoutListeners != null) for (int i = 0; i < mOnLayoutListeners.size(); i++) {
        mOnLayoutListeners.get(i).onLayout(this);
    }
}

private void updateBottomViews() {
    View currentBottomView = getCurrentBottomView();
    if (currentBottomView != null) {
        if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
            mDragDistance = currentBottomView.getMeasuredWidth() - dp2px(getCurrentOffset());
        } else {
            mDragDistance = currentBottomView.getMeasuredHeight() - dp2px(getCurrentOffset());
        }
    }

    if (mShowMode == ShowMode.PullOut) {
        layoutPullOut();
    } else if (mShowMode == ShowMode.LayDown) {
        layoutLayDown();
    }

    safeBottomView();
}
  1. addOnLayoutListener(OnLayout l)和removeOnLayoutListener(OnLayout l)這兩個方法是添加和移除布局監聽;
  2. 在重寫的父類onLayout()方法中,我們看到先調用了updateBottomViews(),那么updateBottomViews()的作用字面上理解就是更新底層View,當CurrentBottomView非空,根據拖動方向(左右方向為一組,上下方向為一組)來計算拖動距離mDragDistance,接着根據兩種不同的模式執行對應的方法把表層View移開,
  3. 在onLayout()方法的最后,調用safeBottomView()來攔截掉底層View的所有事件;safeBottomView()方法在第二部分會出現;
  4. 到這里我們很熟悉,遍歷所有子View,回調onLayout(),將其布局顯示在SwipeLayout這個父控件中;

第二部分 TouchEvent傳遞(多層嵌套不破壞事件傳遞是怎樣做到的?)

其實自定義View說到底就是在View繪制和事件處理這兩塊大做文章!在第一部分分析了SwipeLayout的View繪制過程后,我們接着來看它的事件傳遞過程,哈哈,原作者提到它的特性:“可以嵌套在任何地方而不破壞觸摸事件傳遞(這是最難的地方)”,我想說這也是這個開源庫最精華的地方,以前自己也寫過View控件,在觸摸事件處理上遇到了各種問題,SwipeLayout在這方面確實值得借鑒。

所以很快的我們就會想到要去SwipeLayout中找三個很重要很重要的重寫方法:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()!他們都只接收一個MotionEvent對象,分別負責事件分發、事件攔截、事件消耗。把這部分相關的代碼單獨拿出來如下:

private int mEventCounter = 0;

protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, int dx, int dy) {
    DragEdge edge = getDragEdge();
    boolean open = true;
    if (edge == DragEdge.Left) {
        if (dx < 0) open = false;
    } else if (edge == DragEdge.Right) {
        if (dx > 0) open = false;
    } else if (edge == DragEdge.Top) {
        if (dy < 0) open = false;
    } else if (edge == DragEdge.Bottom) {
        if (dy > 0) open = false;
    }

    dispatchSwipeEvent(surfaceLeft, surfaceTop, open);
}

protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, boolean open) {
    safeBottomView();
    Status status = getOpenStatus();

    if (!mSwipeListeners.isEmpty()) {
        mEventCounter++;
        for (SwipeListener l : mSwipeListeners) {
            if (mEventCounter == 1) {
                if (open) {
                    l.onStartOpen(this);
                } else {
                    l.onStartClose(this);
                }
            }
            l.onUpdate(SwipeLayout.this, surfaceLeft - getPaddingLeft(), surfaceTop - getPaddingTop());
        }

        if (status == Status.Close) {
            for (SwipeListener l : mSwipeListeners) {
                l.onClose(SwipeLayout.this);
            }
            mEventCounter = 0;
        }

        if (status == Status.Open) {
            View currentBottomView = getCurrentBottomView();
            if (currentBottomView != null) {
                currentBottomView.setEnabled(true);
            }
            for (SwipeListener l : mSwipeListeners) {
                l.onOpen(SwipeLayout.this);
            }
            mEventCounter = 0;
        }
    }
}

/**
 * prevent bottom view get any touch event. Especially in LayDown mode.
 */
private void safeBottomView() {
    Status status = getOpenStatus();
    List<View> bottoms = getBottomViews();

    if (status == Status.Close) {
        for (View bottom : bottoms) {
            if (bottom != null && bottom.getVisibility() != INVISIBLE) {
                bottom.setVisibility(INVISIBLE);
            }
        }
    } else {
        View currentBottomView = getCurrentBottomView();
        if (currentBottomView != null && currentBottomView.getVisibility() != VISIBLE) {
            currentBottomView.setVisibility(VISIBLE);
        }
    }
}

protected void dispatchRevealEvent(final int surfaceLeft, final int surfaceTop, final int surfaceRight, final int surfaceBottom) {
    if (mRevealListeners.isEmpty()) return;
    for (Map.Entry<View, ArrayList<OnRevealListener>> entry : mRevealListeners.entrySet()) {
        View child = entry.getKey();
        Rect rect = getRelativePosition(child);
        if (isViewShowing(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop,
                surfaceRight, surfaceBottom)) {
            mShowEntirely.put(child, false);
            int distance = 0;
            float fraction = 0f;
            if (getShowMode() == ShowMode.LayDown) {
                switch (mCurrentDragEdge) {
                    case Left:
                        distance = rect.left - surfaceLeft;
                        fraction = distance / (float) child.getWidth();
                        break;
                    case Right:
                        distance = rect.right - surfaceRight;
                        fraction = distance / (float) child.getWidth();
                        break;
                    case Top:
                        distance = rect.top - surfaceTop;
                        fraction = distance / (float) child.getHeight();
                        break;
                    case Bottom:
                        distance = rect.bottom - surfaceBottom;
                        fraction = distance / (float) child.getHeight();
                        break;
                }
            } else if (getShowMode() == ShowMode.PullOut) {
                switch (mCurrentDragEdge) {
                    case Left:
                        distance = rect.right - getPaddingLeft();
                        fraction = distance / (float) child.getWidth();
                        break;
                    case Right:
                        distance = rect.left - getWidth();
                        fraction = distance / (float) child.getWidth();
                        break;
                    case Top:
                        distance = rect.bottom - getPaddingTop();
                        fraction = distance / (float) child.getHeight();
                        break;
                    case Bottom:
                        distance = rect.top - getHeight();
                        fraction = distance / (float) child.getHeight();
                        break;
                }
            }

            for (OnRevealListener l : entry.getValue()) {
                l.onReveal(child, mCurrentDragEdge, Math.abs(fraction), distance);
                if (Math.abs(fraction) == 1) {
                    mShowEntirely.put(child, true);
                }
            }
        }

        if (isViewTotallyFirstShowed(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop,
                surfaceRight, surfaceBottom)) {
            mShowEntirely.put(child, true);
            for (OnRevealListener l : entry.getValue()) {
                if (mCurrentDragEdge == DragEdge.Left
                        || mCurrentDragEdge == DragEdge.Right)
                    l.onReveal(child, mCurrentDragEdge, 1, child.getWidth());
                else
                    l.onReveal(child, mCurrentDragEdge, 1, child.getHeight());
            }
        }

    }
}
  1. mEventCounter,顧名思義,事件計數器,只要SwipeListener監聽到有事件觸發,mEventCounter++;
  2. 第一個重載的dispatchSwipeEvent()方法,屏蔽掉了一些無效的觸摸事件,也就是屏蔽掉了SwipeLayout(也就是ItemView)邊界以外觸發的所有事件,當事件無效,則把布爾型標記位open變量置為false,這個標記位直接控制着是否回調事件的監聽;
  3. 第二個重載的dispatchSwipeEvent()方法,則是真正的事件分發邏輯所在。safeBottomView(),在第一部分出現過,攔截底層View的所有事件;然后通過getOpenStatus()拿到表層View的打開狀態status,表層View有三種狀態:Middle,Open,Close表示半打開狀態,完全打開(此時只能看底層View),關閉(此時只能看到表層View)。
  4. 循環遍歷mSwipeListeners,拿到每一個觸摸事件,根據標記位open判斷是否執行事件的監聽回調;
  5. 根據表層View的打開狀態status,通過回調方法執行關閉或者打開操作;並把mEventCounter重新置零。到此整個觸摸事件的分發,似乎已經完成;但是你發現沒有?上面的這些事件分發都是一些SwipeListener的方法回調,其具體邏輯是交由用戶去實現的;那么問題來了,我們的ItemView總得響應用戶的觸摸事件,進行相應的UI更新吧,比如表層View的Open和Close,你會發現這些事件是不能交給用戶去處理的,所以這里涉及到了另外一個很重要的接口OnRevealListener(關於接口的監聽回調,會在第三部分細講),負責重繪ItenView以更新用戶界面顯示的UI。這是整個開源庫的精髓所在;
  6. 此時看到最后還有一個dispatchRevealEvent(),那么這個方法是干嘛的呢?dispatchRevealEvent()就負責分發
    UI更新相關事件。邏輯也很簡單,通過getShowMode() == ShowMode.LayDown | ShowMode.PullOut判斷用戶的滑動操作模式是LayDown模式還是PullOut模式,根據不同的模式計算兩個不同的參數distance和fraction,這兩個參數就是直接控制着表層View的滑動行為;
  7. 最后,遍歷所有的OnRevealListener接口對象,回調onReveal()更新用戶界面UI。

上面說完了SwipeLayout的事件分發,那么SwipeLayout事件是如何攔截以及消耗的呢?把相關的代碼單獨拿出來如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (!isSwipeEnabled()) {
        return false;
    }
    if (mClickToClose && getOpenStatus() == Status.Open && isTouchOnSurface(ev)) {
        return true;
    }
    for (SwipeDenier denier : mSwipeDeniers) {
        if (denier != null && denier.shouldDenySwipe(ev)) {
            return false;
        }
    }

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDragHelper.processTouchEvent(ev);
            mIsBeingDragged = false;
            sX = ev.getRawX();
            sY = ev.getRawY();
            //if the swipe is in middle state(scrolling), should intercept the touch
            if (getOpenStatus() == Status.Middle) {
                mIsBeingDragged = true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            boolean beforeCheck = mIsBeingDragged;
            checkCanDrag(ev);
            if (mIsBeingDragged) {
                ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            if (!beforeCheck && mIsBeingDragged) {
                //let children has one chance to catch the touch, and request the swipe not intercept
                //useful when swipeLayout wrap a swipeLayout or other gestural layout
                return false;
            }
            break;

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingDragged = false;
            mDragHelper.processTouchEvent(ev);
            break;
        default://handle other action, such as ACTION_POINTER_DOWN/UP
            mDragHelper.processTouchEvent(ev);
    }
    return mIsBeingDragged;
}

private float sX = -1, sY = -1;

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (!isSwipeEnabled()) return super.onTouchEvent(event);

    int action = event.getActionMasked();
    gestureDetector.onTouchEvent(event);

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mDragHelper.processTouchEvent(event);
            sX = event.getRawX();
            sY = event.getRawY();


        case MotionEvent.ACTION_MOVE: {
            //the drag state and the direction are already judged at onInterceptTouchEvent
            checkCanDrag(event);
            if (mIsBeingDragged) {
                getParent().requestDisallowInterceptTouchEvent(true);
                mDragHelper.processTouchEvent(event);
            }
            break;
        }
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mDragHelper.processTouchEvent(event);
            break;

        default://handle other action, such as ACTION_POINTER_DOWN/UP
            mDragHelper.processTouchEvent(event);
    }

    return super.onTouchEvent(event) || mIsBeingDragged || action == MotionEvent.ACTION_DOWN;
}

上面這段代碼很簡單,重寫父類的onInterceptTouchEvent()方法和onTouchEvent(),和我們預想的一模一樣。那么什么情況下,SwipeLayout攔截掉了子View的觸摸事件?當SurfaceView處於打開狀態,想要點擊關閉,卻點擊在SurfaceView上時,SwipeLayout會攔截掉SurfaceView的觸摸事件。至於事件攔截和處理的具體邏輯,就在上面這段代碼中了。


第三部分 Listener監聽與回調(設置隱藏百分比、過渡動畫效果)

第三部分我們來分析Listener監聽與回調,包括怎樣設置子View顯示隱藏百分比,以及更新UI的過渡動畫效果。前文提到了兩個監聽接口OnLayout和OnRevealListener,前者的毀掉方法,主要響應一些用戶的自定義操作,比如手機QQ聊天列表的ItenView左滑呼出刪除、置頂、標為未讀等操作功能;后者在第二部分已經提及,主要處理更新UI等功能,而這些功能已經在SwpieLayout中封裝好了,不需要用戶關心;當然,作為一個小巧但並不影響它強大存在的開源庫,用戶的可定制性還是非常高的,比如可以實現一些ItemView呼出或者關閉的動畫。

關於SwipeLayout開源庫的使用方法,點擊閱讀我的上一篇博客:開源庫AndroidSwipeLayout分析(一),炫酷ItemView滑動呼出效果


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM