前面已经介绍了如何在地图上自定义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的效果了
项目地址:https://github.com/good-good-study/MaterialApp/blob/master/app/src/main/java/com/sxt/chat/ui/material/bottomSheet/MapSheetActivity.java