Android Material Design 之 BottomSheetBehavior

前面已经介绍了如何在地图上自定义Marker 和 Poi搜索 说实话录个视频真麻烦,还得转gif , 转就转吧图片还有大小限制 , 你说气人不 !

看过前两篇地图相关的博客,应该可以看出来, 屏幕底部有个展示数据的列表 ,可以跟随手指拖拽、滑动, 这样的效果在高德地图app中见过, 饿了么点餐的时候好像也有 , 其实这是**Google Material Design **的 BottomSheetBehavior

BottomSheetBehavior 啥意思? 近几年Google大力提倡Material Design , 这是个设计规范 , 里面重新定义了各种UI的规则 , 什么CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout、RecyclerView、NestedScrollView....
官方文档的地址 https://material.io/develop/android/components/bottom-sheet-behavior/

**BottomSheetBehavior **是个底部动作条的意思 , 可以设置最小高度和最大高度 ,执行进入/退出动画,响应拖动/滑动手势等 滑动起来不要太流畅 , 光说文字看不出效果 , 先来张Google Map的实现效果吧

下面就开始来实现个类似的效果喽, 数据填充就用Poi搜索获取的数据 , 想要使用这个新的控件需要添加额外的依赖包

implementation 'com.android.support:design:27.1.0'

先看下布局文件吧




    

    

        

        

    

    

       

    


想要使用BottomSheetBehavior , 其直接父View必须是CoordinatorLayout , 这是个功能强大的View, 可以协调子View的各种嵌套滑动 , 布局很简单,顶部搜索框 , 底部是NestedScrollView 可滑动控件 , CoordinatorLayout自带的 layout_behavior有两个,处理折叠滑动的 appbar_scrolling_view_behavior 和 处理BottomSheet的 bottom_sheet_behavior ; 本文使用的是后者 , 与此行为配合使用的属性还包括

app:behavior_hideable //设置为true表示底部弹出框可隐藏
app:behavior_peekHeight //窥视高度 , 就是BottomSheet折叠后的最小显示高度
app:behavior_skipCollapsed //如果app:behavior_hideable设置为true,并且behavior_skipCollapsed设置为true , 则它没有折叠状态。

重点就在这几个属性上面 ,控制BottomSheet的手势滑动 , 另外BottomSheet共有5种状态

STATE_COLLAPSED:折叠状态,就是peekHeight 设置的窥视高度
STATE_EXPANDED:完全展开
STATE_DRAGGING:拖动中
STATE_SETTLING:拖动/滑动手势后,将稳定到特定高度。如果用户操作导致底部页面隐藏,则这将是峰值高度,扩展高度或0。
STATE_HIDDEN:隐藏

现在运行代码看下效果

可以看到底部的列表一开始以最低高度出现 , 跟随手指滑动完全展开,然后向下滑恢复到折叠状态 , 最后隐藏 , 比起那些千篇一律的从头滑到尾的List有趣了很多 , 但是现在有了一个问题 , 列表完全展开的时候 , 输入框会遮挡列表, 这就尴尬了 ;

解决方案呢就是设置列表的高度 , 这时候就需要在代码中设置了 , 现在问题又来了, 怎么获取BottomSheet对象呢 ? findViewById 吗 ? 显然是行不通的 ;

因为Behavior是通过View来创建的,

BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(nestedScrollView);

上面的xml中我们在NestedScrollView中定义了layout_behavior , 那所以对应的View就是NestedScrollView ; 其实xml中设置的peekHeight 、hideable ... 这些属性也是可以通过Behavior对象在代码中设置的

        int peekHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
        bottomSheetBehavior.setPeekHeight(peekHeight);//设置最小高度
        bottomSheetBehavior.setHideable(true);//设置是否可隐藏
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);//设置当前为隐藏状态

这里定义BottomSheet最小高度peekHeight为100dp ; 那最大高度我应该设置为多少呢? 由于上面有个输入框 , 当底部列表完全展开的时候 ,应该让列表显示在输入框的下面 , 所以BottomSheet的最大高度应该是:

        //获取屏幕的高度
        int heightPixels = getResources().getDisplayMetrics().heightPixels;
        final CardView cardView = findViewById(R.id.cardView);
        cardView.post(new Runnable() {
            @Override
            public void run() {
                CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) cardView.getLayoutParams();

                //获取状态栏的高度
                int statusBarHeight = 0;
                int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
                if (resourceId > 0) {
                    statusBarHeight = getResources().getDimensionPixelSize(resourceId);
                }
                //输入框至屏幕顶部的高度
                int marginTop = cardView.getHeight() + lp.topMargin + lp.bottomMargin / 2 + statusBarHeight;
                //底部列表的最大高度
                int maxHeight = heightPixels - marginTop ;
            }
        });

咦? 为什么要减去状态栏的高度呢 ? 因为CoordinatorLayout 中我设置了 **android:fitsSystemWindows="true" , **将内容区域延伸到了状态栏 , 所以这里要用屏幕的总高度 减去 状态栏的高度

现在BottomSheet的最大高度已经获取到了 , 那应该在什么时候给它设置呢 ? 首先想到的就应该是Behavior的回调方法

bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                if (newState != BottomSheetBehavior.STATE_DRAGGING) {
                    ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams();
                    if (bottomSheet.getHeight() > maxHeight ) {
                        layoutParams.height = maxHeight ;
                        bottomSheet.setLayoutParams(layoutParams);
                    }
                }
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {

                }               
            }
        });

前面有说道Behavior有5中状态 , 就是在onStateChanged () 中进行回调的 , 第一个参数 View bottomSheet 就是当前BottomSheet所持有的View , 我们要动态设置高度的对象就是它

这里是在非拖拽的状态下进行判断 ,如果 bottomSheet.getHeight() > maxHeight 将bottomSheet的最大高度设置为maxHeight ,这样就解决了输入框覆盖列表的问题了

写到这里, 总觉得还欠缺点什么 , 上面的Google map 图片中 右下角的定位按钮 是如何移动的 ? 而且还是跟随Behavior一起滑动 , 好像地图也是在跟随滑动

Behavior的回调方法我们才用了一个而已 , 不是还有个onSlide () 了吗 , 大胆猜一下第二个参数slideOffset 代表啥意思 ?

从命名上来看就知道肯定是滑动时候的高度偏移量喽 , 没错 , 就是它 , 但是这里有个坑 , 既然是高度的偏移量 , 那这个高度是多少呢 ? 如果你认为是上面设置的最大高度的话, 那么恭喜你, 并不是 ! 准确的来说 , 这里应该分两种情况来分析 :

  1. 当Behavior在折叠状态到隐藏状态之间滑动(向上、向下)的时候 , slideOffset 对应的最大高度是peekHeight而并非maxHeight ;
    此时slideOffset取值范围是[-1,0];
  2. 当Behavior在折叠状态到展开状态之间滑动(向上、向下)的时候, slideOffset对应的最大高度才是maxHeight ;

此时slideOffset取值范围是[0,1];

你妹的, 这坑真不小 , 反复计算了好多次 ,才发现这个问题 , 奶奶的 , 你说气人不 !

现在xml中添加两个button , 以便于跟随Behavior一起滑动



        

        

    

在回调方法onSlide() 中进行设置 ; 需要用到View的setTranslationY () ; Y轴方向上的平移

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                float distance;
                if (slideOffset > 0) {//在peekHeight位置以上 滑动(向上、向下) slideOffset bottomSheet.getHeight() 是展开后的高度的比例
                    distance = bottomSheet.getHeight() * slideOffset;
                } else {//在peekHeight位置以下 滑动(向上、向下)  slideOffset 是PeekHeight的高度的比例
                    distance = bottomSheetBehavior.getPeekHeight() * slideOffset;
                }
                if (distance < 0) {
                    fabContainer.setTranslationY(-distance);
                    mapView.setTranslationY(0);
                } else {
                    if (distance <= peekHeight) {
                        fabContainer.setTranslationY(-distance);
                        mapView.setTranslationY(-distance);
                    }
                }
                Log.e(TAG, String.format("slideOffset -->>> %s bottomSheet.getHeight() -->>> %s heightPixels -->>> %s", slideOffset, bottomSheet.getHeight(), heightPixels));
            }

distance 就是当前偏移的高度了 ,distance > 0 是向上滑动 , 此时要取相反数去设置Y轴位移 ,反之亦然 .

当distance > 0 的时候 , 为了不让button一直跟随Behavior滑动 , 这里加了个限制条件, distance <= peekHeight 的时候才位移

这样就实现了Google Map的效果了

到这里就结束了 ,下次讲appbar_scrolling_view_behavior , 后续会介绍更多Material Design的组件
代码已在GitHub托管https://github.com/good-good-study/WeChartApplication

你可能感兴趣的:(Android Material Design 之 BottomSheetBehavior)