實現一個網易雲音樂的 BottomSheetDialog

林冠巨集_指尖下的幽靈發表於2018-04-10

作者:林冠巨集 / 指尖下的幽靈

掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8

部落格:http://www.cnblogs.com/linguanh/

GitHub : https://github.com/af913337456/

騰訊雲專欄: https://cloud.tencent.com/developer/user/1148436/activities


Github 開源地址

目錄

  • 前序
  • 直觀對比下 gif 效果
    • Android SDK 自帶的 BottomSheetDialog
    • 網易雲音樂 的 BottomSheetDialog
    • 我開源 的仿網易雲音樂 BottomSheetDialog
  • 核心程式碼簡述

前序:

因為APP 需要參照網易雲音樂的 BottomSheetDialog 的效果,找了一圈沒找到,所以動手寫了一個,涉及圈子裡經常露面的知識點有下面三點,也是個實戰應用。當然,也可以把它當做一個典型的事件分發綜合教例。

  • 事件分發系列的--衝突處理 & 分發順序
  • View 繪製流程的--Measure 模式
  • 相對螢幕取 View 的座標

先來直觀對比下 gif 效果

  • 首先是-- Android SDK 自帶的 BottomSheetDialog
  • 然後是--網易雲音樂 的 BottomSheetDialog
  • 最後是--我開源 的仿網易雲音樂 BottomSheetDialog

首先是-- Android SDK 自帶的 BottomSheetDialog

下面的 gif 圖是一個Android SDK 自帶的 BottomSheetDialog 內部加了 RecyclerView 列表控制元件的效果

實現一個網易雲音樂的 BottomSheetDialog

可以看出:

  • 下滑動作會收起,隱藏掉 dialog
  • 上滑會完全展開
  • 展開後,才能滑動 RecyclerView 內部

其次

  • 如果你內部使用的是 ListView 列表控制元件,你會發現會有其他奇怪的情況。

然後是--網易雲音樂 的 BottomSheetDialog

下面的 gif 圖是一個Android 版 網易雲音樂BottomSheetDialog效果

實現一個網易雲音樂的 BottomSheetDialog

可以看出:

  • 下滑動作會有範圍回彈,也就是下滑到一定距離才會收起,隱藏掉 dialog
  • 上滑不給展開
  • 能夠在半展開的情況下,內嵌滑動列表控制元件,例如 listView
  • 和列表控制元件滑動不衝突,在列表控制元件滑盡的時候,可以下滑隱藏dialog

最後是--我開源 的仿網易雲音樂 BottomSheetDialog

實現一個網易雲音樂的 BottomSheetDialog

可以看出,效果和網易雲的一樣

核心程式碼簡述

SDK 的 BottomSheetDialog 內部佈局的結構如下:

--FrameLayout
--|--CoordinatorLayout
--|--|--FrameLayout
--|--|--|--Our ContentView // 最後是我們設定的 ContentView

複製程式碼

CoordinatorLayout 在 Action_Move 事件時,必要的時候對其子 View 進行事件攔截,所以有第一個 gif 看到的效果,具體不詳說。

第一個步驟 --- 防止 CoordinatorLayoutOur ContentView 攔截事件

這裡使用 ListView 做例子,設定onTouch,在內部做適當時候的適當阻止CoordinatorLayout 攔截事件。

// ListView
setOnTouchListener(
    new OnTouchListener() {
        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (bottomCoordinator == null)
                return false;
            // 拿出當前列表第一個可見 item 的 pos
            int firstVisiblePos = getFirstVisiblePosition();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downY = event.getRawY();
                    bottomCoordinator.requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    moveY = event.getRawY();
                    if ((moveY - downY) > 10) {
                        // 下滑情況
                        if (firstVisiblePos == 0 && isOverScroll) {
                            // 列表控制元件,例如 listView 已經滑到頭了,允許被攔截
                            bottomCoordinator.requestDisallowInterceptTouchEvent(false);
                            break;
                        }
                    }
                    // 上滑時,總是不允許被攔截,listView 消耗當前事件
                    bottomCoordinator.requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return false;
        }
    }
);
複製程式碼

第二個步驟,讓 ListView 能在半展開的情況下,顯示完整的資料條數

重寫 onMeasure,使用自定義的測量模式。

// ListView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if(bottomCoordinator == null){
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        return;
    }
    // 以黃金分割的尺寸來顯示 listView 的高度
    int size = (int)((float)(getResources().getDisplayMetrics().heightPixels*0.618));
    int newHeightSpec = MeasureSpec.makeMeasureSpec(
            size,
            // mode,非法的情況,super 直接使用 size 做高,看原始碼後,你會發現也可以使用 exact 模式
            Integer.MIN_VALUE 
    );
    super.onMeasure(widthMeasureSpec, newHeightSpec);
}
複製程式碼

第三個步驟,實現 BottomSheetDialog 範圍回彈

/**
 * 新增 top 距離頂部多少的時候觸發收縮效果
 * @param targetLimitH int 高度限制
 */
@SuppressWarnings("all")
public void addSpringBackDisLimit(final int targetLimitH){
    if(coordinator == null)
        return;
    // totalHeight 螢幕的總畫素高度
    final int totalHeight = getContext().getResources().getDisplayMetrics().heightPixels;
    // currentH 當前我們的 列表控制元件 展開的高度
    final int currentH = (int) ((float)totalHeight*0.618); // 0.618 是黃金分割點,隨便自定義,對應 contentView
    final int leftH    = totalHeight - currentH;
    coordinator.setOnTouchListener(
            new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()){
                        case MotionEvent.ACTION_MOVE:
                            // 計算相對於螢幕的 座標
                            bottomSheet.getGlobalVisibleRect(r);
                            break;
                        case MotionEvent.ACTION_UP:
                            // 抬手的時候判斷
                            int limitH;
                            if(targetLimitH < 0)
                                limitH = (leftH + currentH/3);
                            else
                                limitH = targetLimitH;
                            if(r.top <= limitH)
                                if (mBehavior != null)
                                    // 範圍內,讓它繼續是 半展開的狀態
                                    mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                            break;
                    }
                    return false;
                }
            }
    );
}
複製程式碼

相關文章