实现一个网易云音乐的 BottomSheetDialog

13,563 阅读3分钟

作者:林冠宏 / 指尖下的幽灵

掘金:https://juejin.cn/user/1785262612681997

博客: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 列表控件的效果

可以看出:

  • 下滑动作会收起,隐藏掉 dialog
  • 上滑会完全展开
  • 展开后,才能滑动 RecyclerView 内部

其次

  • 如果你内部使用的是 ListView 列表控件,你会发现会有其他奇怪的情况。

然后是--网易云音乐 的 BottomSheetDialog

下面的 gif 图是一个Android 版 网易云音乐BottomSheetDialog效果

可以看出:

  • 下滑动作会有范围回弹,也就是下滑到一定距离才会收起,隐藏掉 dialog
  • 上滑不给展开
  • 能够在半展开的情况下,内嵌滑动列表控件,例如 listView
  • 和列表控件滑动不冲突,在列表控件滑尽的时候,可以下滑隐藏dialog

最后是--我开源 的仿网易云音乐 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;
                }
            }
    );
}