Support Lirary 23.2版本,其中Design Support Library庫BottomSheets源碼解析

RosemarieTC 10年前發布 | 20K 次閱讀 Android開發 移動開發

來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2016/0228/4009.html


原文出處:https://github.com/android-cjj/SourceAnalysis/blob/master/README.md 

2月25日早上,Android官網更新了Support Lirary 23.2版本,其中Design Support Library庫新加一個新的東西:Bottom Sheets。然后,第一時間寫了篇Teach you how to use Design Support Library: Bottom Sheets,只是簡單的講了它的使用和使用的一些規范。

blob.png

這篇文章我帶大家看看BottomSheetBehavior的源碼,能力有限,寫的不好的地方,請盡力吐槽。好了,不說廢話,直接主題

我們先簡單的看下用法

        // The View with the BottomSheetBehavior
        View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
        final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
        behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                //這里是bottomSheet 狀態的改變回調
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                //這里是拖拽中的回調,根據slideOffset可以做一些動畫
            }
        });

對于切換狀態,你也可以手動調用behavior.setState(int state); state 的值你可以看我的上一篇戳我

BottomSheetBehavior的定義如下

    public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>

繼承自CoordinatorLayout.Behavior,BottomSheetBehavior.from(V view)方法獲得了BootomSheetBehavior的實例,我們進去看看它怎么實現的。

    public static <V extends View> BottomSheetBehavior<V> from(V view) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        if (!(params instanceof CoordinatorLayout.LayoutParams)) {
            throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
        }
        CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
                .getBehavior();
        if (!(behavior instanceof BottomSheetBehavior)) {
            throw new IllegalArgumentException(
                    "The view is not associated with BottomSheetBehavior");
        }
        return (BottomSheetBehavior<V>) behavior;
    }

源碼中看出根據傳入的參數view的LayoutParams是不是 CoordinatorLayout.LayoutParams,若不是,將拋出"The view is not a child of CoordinatorLayout"的異常,通過 ((CoordinatorLayout.LayoutParams) params).getBehavior()獲得一個behavior并判斷是不是BottomSheetBehavior,若不是,就拋出異常"The view is not associated with BottomSheetBehavior",都符合就返回了BottomSheetBehavior的實例。這里我們可以知道behavior保存在 CoordinatorLayout.LayoutParams里,那它是 怎么保存的呢,懷著好奇心,我們去看看CoordinatorLayout.LayoutParams中的源碼,在LayoutParams的構造函數中,有這么一句:

            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
            }

順藤摸瓜,我們在跟進去看看parseBehavior做了什么

     static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
        Context.class,
        AttributeSet.class
    };

    static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
       /*
        *省略部分代碼
        */
        try {
           /*
            *省略部分代碼
            */
            Constructor<Behavior> c = constructors.get(fullName);
            if (c == null) {
                final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                        context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

這里做的事情很簡單,就是在實例化CoordinatorLayout.LayoutParams時反射生成Behavior實例,這就是為什么自定義behavior需要重寫如下的構造函數

    public class CjjBehavior extends CoordinatorLayout.Behavior{
        public CjjBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }

不然就會看到"Could not inflate Behavior subclass ..."異常 。

目前為止,我們只是了解了CoordinatorLayout.Behavior相關的東西,還是不知道BottomSheetBehavior實現的原理,別急,這就和你說說。

view布局

當你的View持有Behavior的時候, CoordinatorLayout 在 onLayout 的時候會調用Behavior.onLayoutChild方法進行布局.

注意:我們將持有的Behavior 的View 叫做BehaviorView

我們查看onLayoutChild 的源碼

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        // First let the parent lay it out
        if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
            parent.onLayoutChild(child, layoutDirection);
        }
        // Offset the bottom sheet
        mParentHeight = parent.getHeight();
        mMinOffset = Math.max(0, mParentHeight - child.getHeight());
        mMaxOffset = mParentHeight - mPeekHeight;
        if (mState == STATE_EXPANDED) {
            ViewCompat.offsetTopAndBottom(child, mMinOffset);
        } else if (mHideable && mState == STATE_HIDDEN) {
            ViewCompat.offsetTopAndBottom(child, mParentHeight);
        } else if (mState == STATE_COLLAPSED) {
            ViewCompat.offsetTopAndBottom(child, mMaxOffset);
        }
        if (mViewDragHelper == null) {
            mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
        }
        mViewRef = new WeakReference<>(child);
        mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
        return true;
    }

這里主要做了幾件事情:

  1. 對BehaviorView 的擺放:先調用父類 對 BehaviorView 進行布局,根據 PeekHeight 和 State 對 BehaviorView 位置的進行偏移,偏移到合適的位置.

  2. 對mMinOffset,mMaxOffset的計算,根據mMinOffset 和mMaxOffset 可以確定BehaviorView 的偏移范圍.即 距離CoordinatorLayout 原點 Y軸mMinOffset 到mMaxOffset;

  3. 始化了ViewDragHelper 類.ViewDragHelper是一個非常厲害的組件.我們這邊使用它處理進行拖拽和滑動事件.

  4. 存儲BehaviorView 的軟引用和遞歸找到第一個NestedScrollingChild組件,當然NestedScrollingChild也可以為空.下面的邏輯對于NestedScrollingChild為空的情況做了處理的.

onLayoutChild做的事情還是挺少的.算是一些初始化的東西

因為State 默認為STATE_COLLAPSED,偏移量為ParentHeight - PeekHeight, 這時候BehaviorView 被往下調整了,露出屏幕的高度為PeekHeight 的大小.

在Android 5.0上可能是因為優化的原因還是別的因素. 當一開始的 PeekHeight為0的時候 整個BehaviorView 被移到屏幕外, 它就不會被繪制上去.導致你看不到BehaviorView的畫面,但是它是存在的.實實在在存在著

我的好基友dim給出了解決方案Android support 23.2 使用BottomSheetBehavior 的坑

事件攔截

touch 事件會先被onInterceptTouchEvent()捕獲,進行判斷是否攔截.

@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
    if (!child.isShown()) {
        return false;
    }
    int action = MotionEventCompat.getActionMasked(event);
    // Record the velocity
    if (action == MotionEvent.ACTION_DOWN) {
        reset();
    }
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (action) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mTouchingScrollingChild = false;
            mActivePointerId = MotionEvent.INVALID_POINTER_ID;
            // Reset the ignore flag
            if (mIgnoreEvents) {
                mIgnoreEvents = false;
                return false;
            }
            break;
        case MotionEvent.ACTION_DOWN:
            int initialX = (int) event.getX();
            mInitialY = (int) event.getY();
            View scroll = mNestedScrollingChildRef.get();
            if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
                mActivePointerId = event.getPointerId(event.getActionIndex());
                mTouchingScrollingChild = true;
            }
            mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
                    !parent.isPointInChildBounds(child, initialX, mInitialY);
            break;
    }
    if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
        return true;
    }
    // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
    // it is not the top most view of its parent. This is not necessary when the touch event is
    // happening over the scrolling content as nested scrolling logic handles that case.
    View scroll = mNestedScrollingChildRef.get();
    return action == MotionEvent.ACTION_MOVE && scroll != null &&
            !mIgnoreEvents && mState != STATE_DRAGGING &&
            !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
            Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
}

onInterceptTouchEvent 做了幾件事情:

  1. 判斷是否攔截事件.先使用mViewDragHelper.shouldInterceptTouchEvent(event)攔截.

  2. 使用mVelocityTracker 記錄手指動作,用于后期計算Y 軸速率.

  3. 判斷點擊事件是否在NestedChildView 上,將 boolean 存到mTouchingScrollingChild 標記位中,這個主要是用于ViewDragHelper.Callback 中的判斷.

  4. ACTION_UP 和ACTION_CANCEL 對標記位進行復位,好在下一輪 Touch 事件中使用.

onTouchEvent處理

 @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
        if (!child.isShown()) {
            return false;
        }
        int action = MotionEventCompat.getActionMasked(event);
        if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
            return true;
        }
        mViewDragHelper.processTouchEvent(event);
        // Record the velocity
        if (action == MotionEvent.ACTION_DOWN) {
            reset();
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
        // to capture the bottom sheet in case it is not captured and the touch slop is passed.
        if (action == MotionEvent.ACTION_MOVE) {
            if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) {
                mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex()));
            }
        }
        return true;
    }

onTouchEvent 主要做了幾件事情:

  1. 使用mVelocityTracker 記錄手指動作.用于后期計算Y 軸速率.

  2. 使用mViewDragHelper 處理Touch 事件.可能會產生拖動效果.

  3. mViewDragHelper 在滑動的時候對BehaviorView 的再一次捕獲.再一次明確告訴ViewDragHelper 我要移動的是BehaviorView 組件.什么情況需要主動告訴ViewDragHelper ?比如:當你點擊在BehaviorView 的區域,但是BehaviorView 的視圖的層級不是最高的,或者你點擊的區域不在 BehaviorView 上,ViewDragHelper 在做處理滑動的時候找不到BehaviorView, 這個時候你要手動告知它現在要移動的是BehaviorView,情景類似ViewDragHelper處理EdgeDrag 的樣子.

注意

即使你的onInterceptTouchEvent 返回false,也可能因為下面的View 沒有處理這個Touch事件,而導致Touch 事件上發被Behavior的onTouchEvent 被截取.

NestedScrolling事件處理

當 CoordinatorLayout 的子控件有 NestedScrollingChild 產生 Nested 事件的時候.會調用onStartNestedScroll 這個方法

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
            View directTargetChild, View target, int nestedScrollAxes) {
            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//滑動Y軸方向的判斷
    }

返回值 true :表示 BehaviorView 要和NestedScrollingChild 配合消耗這個 NestedScrolling 事件,這里可以看出只要是縱向的滑動都會返回true.

onNestedPreScroll

NestedScrollingChild的在滑動的時候會觸發onNestedPreScroll 方法,詢問BehaviorView消耗多少Y軸上面的滑動.

  @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
            int dy, int[] consumed) {
        View scrollingChild = mNestedScrollingChildRef.get();
        if (target != scrollingChild) {
            return;
        }
        int currentTop = child.getTop();
        int newTop = currentTop - dy;
        if (dy > 0) { // Upward
            if (newTop < mMinOffset) {
                consumed[1] = currentTop - mMinOffset;
                ViewCompat.offsetTopAndBottom(child, -consumed[1]);
                setStateInternal(STATE_EXPANDED);
            } else {
                consumed[1] = dy;
                ViewCompat.offsetTopAndBottom(child, -dy);
                setStateInternal(STATE_DRAGGING);
            }
        } else if (dy < 0) { // Downward
            if (!ViewCompat.canScrollVertically(target, -1)) {
                if (newTop <= mMaxOffset || mHideable) {
                    consumed[1] = dy;
                    ViewCompat.offsetTopAndBottom(child, -dy);
                    setStateInternal(STATE_DRAGGING);
                } else {
                    consumed[1] = currentTop - mMaxOffset;
                    ViewCompat.offsetTopAndBottom(child, -consumed[1]);
                    setStateInternal(STATE_COLLAPSED);
                }
            }
        }
        dispatchOnSlide(child.getTop());
        mLastNestedScrollDy = dy;
        mNestedScrolled = true;
    }

onNestedPreScroll 方法主要做幾件事情:

  1. 判斷發起NestedScrolling 的 View 是否是我們在onLayoutChild 找到的那個控件.不是的話,不做處理.不處理就是不消耗y 軸,把所有的Scroll 交給發起的 View 自己消耗.

  2. 根據dy 判斷方向,根據之前的偏移范圍算出偏移量.使用ViewCompat.offsetTopAndBottom 對BehaviorView 進行偏移操作.

  3. 消耗Y軸的偏移量.發起 NestedScrollingChild 會自動響應剩下的部分

其中comsume[]是個數組,consumed[1]表示 Parent 在 Y 軸消耗的值, NestedScrollingChild 會消耗除BehaviorView消耗剩下的那部分( 比如: NestedScrollingChild 要滑動20像素,因為BehaviorView消耗了10像素,最后NestedScrollingChild 只滑動了10像素);

onStopNestedScroll在Nestd事件結束觸發. 主要做的事情: 根據BehaviorView當前的狀態對它的最終位置的確定,有必要的話調用ViewDragHelper.smoothSlideViewTo 進行滑動.

注意

當你是往下滑動且Hideable 為 true ,他會 使用上面計算的Y軸的速率的判斷.是否應該切換到Hideable 的狀態.

onNestedPreFling

這個是 NestedScrollingChild 要滑行時候觸發的,詢問 BehaviorView是否消耗這個滑行.

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                                float velocityX, float velocityY) {
    return target == mNestedScrollingChildRef.get() &&
            (mState != STATE_EXPANDED ||
                    super.onNestedPreFling(coordinatorLayout, child, target,
                            velocityX, velocityY));
}

處理邏輯是:發起Nested事件要與onLayoutChild 找到的那個控件一致且當前狀態是一個STATE_EXPANDED狀態.

返回值: true表示BehaviorView 消耗滑行事件,那么NestedScrollingChild就不會有滑行了

ViewDragHelper.Callback

ViewDragHelper網上教程挺多的,就不多講了,他主要是處理滑動拖拽的.

小技巧

在說說一個小技巧,Android官網中有這樣一句話:Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android,就是說枚舉比靜態常量更加耗費內存,我們應該避免使用,然后我看BottomSheetBehavior源碼中 mState 是這樣定義的:

    public static final int STATE_DRAGGING = 1;
    public static final int STATE_SETTLING = 2;
    public static final int STATE_EXPANDED = 3;
    public static final int STATE_COLLAPSED = 4;
    public static final int STATE_HIDDEN = 5;

    @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN})
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {}

    @State
    private int mState = STATE_COLLAPSED;

彌補了Android不建議使用枚舉的缺陷。

 本文由用戶 RosemarieTC 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!