美文网首页Android UI控件
Android Material Design 之 Botto

Android Material Design 之 Botto

作者: 说点儿什么吧 | 来源:发表于2019-07-20 16:57 被阅读0次

    前面已经介绍了如何在地图上自定义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'
    

    先看下布局文件吧

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/coordinatorLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">
    
        <com.amap.api.maps.MapView
            android:id="@+id/mapView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true" />
    
        <android.support.v7.widget.CardView
            android:id="@+id/cardView"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_margin="8dp"
            app:cardBackgroundColor="@color/white"
            app:cardCornerRadius="8dp"
            app:cardElevation="8dp"
            app:cardPreventCornerOverlap="false"
            app:cardUseCompatPadding="false">
    
            <android.support.v7.widget.AppCompatEditText
                android:id="@+id/search_view"
                style="@style/text_15_color_1_style"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center_vertical"
                android:background="@null"
                android:cursorVisible="false"
                android:drawablePadding="8dp"
                android:drawableStart="@drawable/ic_search_gray_24dp"
                android:gravity="center_vertical"
                android:hint="搜索关键词"
                android:imeOptions="actionSearch"
                android:maxLines="1"
                android:paddingEnd="8dp"
                android:paddingStart="8dp"
                android:singleLine="true" />
    
            <ImageView
                android:id="@+id/close"
                style="@style/MSV_ImageButton"
                android:layout_gravity="center_vertical|end"
                android:src="@drawable/ic_action_navigation_close_gray24dp"
                android:visibility="invisible" />
    
        </android.support.v7.widget.CardView>
    
        <android.support.v4.widget.NestedScrollView
            android:id="@+id/nestedScrollView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp"
            android:background="@drawable/white_solid_round_top_8"
            app:behavior_hideable="true"
            app:behavior_peekHeight="100dp"
            app:layout_behavior="@string/bottom_sheet_behavior">
    
          <android.support.v7.widget.RecyclerView
                    android:id="@+id/recyclerView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" /> 
    
        </android.support.v4.widget.NestedScrollView>
    
    </android.support.design.widget.CoordinatorLayout>
    

    想要使用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一起滑动

    <LinearLayout
            android:id="@+id/fab_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_marginBottom="120dp"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:background="@android:color/transparent"
            android:gravity="center_horizontal"
            android:orientation="vertical"
            android:paddingBottom="8dp"
            android:translationY="120dp">
    
            <android.support.design.widget.FloatingActionButton
                android:id="@+id/fab_my_location"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/ic_my_location_blue_24dp"
                app:elevation="0dp"
                app:rippleColor="@color/translate_white" />
    
            <android.support.design.widget.FloatingActionButton
                android:id="@+id/fab_scrolling"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:src="@drawable/ic_navigation_blue_24dp"
                app:elevation="0dp"
                app:rippleColor="@color/translate_white" />
    
        </LinearLayout>
    

    在回调方法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 之 Botto

        本文链接:https://www.haomeiwen.com/subject/fdeulctx.html