一、前期基础知识储备
Android Support Library 23.2里的 Design Support Library新加了一个Bottom Sheets控件,Bottom Sheets顾名思义就是底部操作控件,用于在屏幕底部创建一个可滑动关闭的视图,可以替代对话框和菜单。其中包含BottomSheets、BottomSheetDialog和BottomSheetDialogFragment三种可以使用。
BottomSheets常见的效果如图,并且在国内的知乎、网易云上也是可以看到效果。
上图是云村,可以看见播放页里面,查看歌单列表,视图是从底部升起来的。
下图是知乎,可以看见用户评论页,查看内容时,评论列表页也是从底部升起来的。
其实,如果仔细观察,会发现上面的实现要比BottomSheets复杂,里面的列表把触摸事件给拦截掉了。
具体可参考文章实现《实现一个网易云音乐的 BottomSheetDialog》
二、上代码,具体实现
使用BottomSheet
1)添加依赖:
compile 'com.android.support:design:28.0.0'
2)使用BottomSheet,创建布局文件:
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:layout_width="120dp"
android:layout_height="wrap_content"
android:background="@drawable/onclick_btn_shape"
android:onClick="onClick"
android:text="BottomSheet"
android:textSize="12sp" />
<Button
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="@drawable/onclick_btn_shape"
android:onClick="onClickDialog"
android:text="BottomSheetDialog"
android:textSize="12sp" />
</LinearLayout>
<android.support.v4.widget.NestedScrollView
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="@string/bottom_sheet_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:scrollbars="none" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
在布局文件xml中的使用,BottomSheets需要配合CoordinatorLayout控件;其中包含三个属性:
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="@string/bottom_sheet_behavior"
app:behavior_peekHeight="50dp" peekHeight是当Bottom Sheets关闭的时候,底部我们能看到的高度,默认是0不可见。
app:behavior_hideable="true" hideable是当我们拖拽下拉的时候,bottom sheet是否能全部隐藏。
app:layout_behavior指向bottom_sheet_behavior,代表这是一个bottom Sheets
3)Activity代码控制
private BottomSheetBehavior behavior;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View bottomSheet = findViewById(R.id.bottom_sheet);
behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
//这里是bottomSheet 状态的改变
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
//这里是拖拽中的回调,根据slideOffset可以做一些动画
}
});
}
setBottomSheetCallback可以监听回调的状态:
onSlide()是拖拽的回调;
onStateChanged()监听状态的改变,onStateChanged可以监听到的回调一共有5种:
- STATE_HIDDEN: 隐藏状态。默认是false,可通过app:behavior_hideable属性设置。
- STATE_COLLAPSED: 折叠关闭状态。可通过app:behavior_peekHeight来设置显示的高度,peekHeight默认是0。
- STATE_DRAGGING: 被拖拽状态
- STATE_SETTLING: 拖拽松开之后到达终点位置(collapsed or expanded)前的状态。
- STATE_EXPANDED: 完全展开的状态。
BottomSheets控件配合NestedScrollView、RecyclerView使用效果会更好,合理的使用让APP逼格满满。
4)按钮控制BottomSheet的显示与隐藏状态
public void onClick(View view) {
/*
* STATE_COLLAPSED: 折叠关闭状态 app:behavior_peekHeight - 设置折叠时的高度
* STATE_EXPANDED: 完全展开的状态
* 同时设置 —> 如果展开则折叠;如果折叠则展开
* */
if(behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}else {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
BottomSheetBehavior,主要是用来和CoordinatorLayout
配合来实现底部展示效果的。
主要使用的函数是setState()
方法,状态就是上面所列出的5个。
这里实现的逻辑是:点击按钮,如果BottomSheet是折叠状态,则打开,如果是展开状态,则折叠。
实现效果如下:
使用BottomSheetDialog
1)接下来使用BottomSheetDialog, 同样先给出布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#363636"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="@string/bottom_sheet_behavior">
<!--在使用BottomSheetDialog时 根布局只能是NestedScrollView-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/dialog_recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:scrollbars="none" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
注意,在使用BottomSheetDialog时,布局的根View必须使用NestedScrollView,使用其他容器,均不会正常显示。
2)Activity代码实现,同样适用列表展示数据,需要先填充适配器
public void onClickDialog(View view) {
BottomSheetDialog mBottomSheetDialog = new BottomSheetDialog(this);
View layout = getLayoutInflater().inflate(R.layout.dialog_recyclerview, null);
RecyclerView recyclerView = layout.findViewById(R.id.dialog_recyclerView);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayout.VERTICAL);
recyclerView.setLayoutManager(manager);
recyclerView.setAdapter(adapter);
adapter.openLoadAnimation();
initListener(this);
mBottomSheetDialog.setContentView(layout);
mBottomSheetDialog.show();
}
效果如下:
三、知识延伸,万能适配器BaseQuickAdapter的使用
BottomSheets搭配RecyclerView,可以很好地展示大量内容,这里主要给出文中使用的万能适配器BaseQuickAdapter的简单用法。帮助快速创建展示列表。
1)引入依赖:
allprojects {
repositories {
google()
jcenter()
maven { url "https://jitpack.io" }
}
}
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.40'
2)布局中放入RecyclerView:
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:scrollbars="none" />
3)创建实体Bean:
public class RecyclerEntity {
private String title;
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
4)创建列表Item的布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rootview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="15dp"
android:background="@drawable/onshow_btn_shape"
app:cardCornerRadius="10dp"
app:cardElevation="5dp"
app:contentPadding="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp"
android:textColor="@android:color/black"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp"
android:textColor="@android:color/darker_gray"
android:textSize="12sp" />
<!--此处做分割线,RecyclerView有开放的分割线方法-->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray" />
</LinearLayout>
</android.support.v7.widget.CardView>
5)创建万能适配器:
public class RecyclerViewAdapter extends BaseQuickAdapter<RecyclerEntity, BaseViewHolder> {
private int position;
public RecyclerViewAdapter(@LayoutRes int layoutResId, @Nullable List<RecyclerEntity> data) {
super(layoutResId, data);
}
@Override
protected void convert(final BaseViewHolder holder, RecyclerEntity item) {
holder.setText(R.id.tv_title, item.getTitle())
.setText(R.id.tv_desc, item.getDesc())
.addOnClickListener(R.id.tv_title)
.addOnClickListener(R.id.tv_desc);
/* 通过Activity传递position参数 做Item背景状态的切换*/
holder.setBackgroundRes(R.id.rootview, holder.getLayoutPosition() == position ? R.drawable.onclick_btn_shape : R.drawable.onshow_btn_shape);
/* adapter内的点击 没有直接的position参数 可做一些跳转操作
* 但没有必要在adapter中做点击处理,Activity内有完善的处理
* 在这里写的点击事件会拦截Activity内的,甚至会阻拦position参数传递
* */
// holder.getView(R.id.rootview).setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// Log.d(TAG, "onClick: 来自adapter内的点击");
// }
// });
}
public void setSelection(int pos) {
this.position = pos;
notifyDataSetChanged();
}
}
6)Activity代码初始化,填充适配器数据,传入子Item布局,写入Item的点击方法:
private RecyclerView recyclerView;
private RecyclerViewAdapter adapter;
private BottomSheetBehavior behavior;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
... ...
//创建列表布局管理
recyclerView = findViewById(R.id.recyclerView);
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayout.VERTICAL);
recyclerView.setLayoutManager(manager);
//创建适配器
adapter = new RecyclerViewAdapter(R.layout.item_recyclerview, initData());
//设置适配器
recyclerView.setAdapter(adapter);
//打开默认动画
adapter.openLoadAnimation();
initListener(this);
}
// 为适配器填充数据
private ArrayList<RecyclerEntity> initData() {
ArrayList<RecyclerEntity> list = new ArrayList<>();
for (int i = 0; i < 12; i++) {
RecyclerEntity entify = new RecyclerEntity();
entify.setTitle("测试专用标题" + i);
entify.setDesc("测试专用描述" + i);
list.add(entify);
}
return list;
}
// 初始化列表相关的点击事件
private void initListener(final Context context) {
/* 该接口针对整个Item布局 可传递position参数*/
adapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter baseAdapter, View view, int position) {
adapter.setSelection(position);
Toast.makeText(context, "点击了第" + position + "条", Toast.LENGTH_SHORT).show();
}
});
/* 该接口针对Item内控件 可传递position参数*/
adapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
if (view.getId() == R.id.tv_title) {
Toast.makeText(context, "点击了子布局第" + position + "条title", Toast.LENGTH_SHORT).show();
} else if (view.getId() == R.id.tv_desc) {
Toast.makeText(context, "点击了子布局第" + position + "条desc", Toast.LENGTH_SHORT).show();
}
}
});
}
可以看到,使用万能适配器的好处:
①适配器内部省略了大量代码;主要复写convert()方法,在里面对Item布局进行初始化和填充;
②点击事件的逻辑也清晰明确,可分别设置不同布局元素的点击事件(不建议在适配器内部处理点击事件);
③可以根据Activity回传的Position参数,完成Item项状态的变化(也在convert()方法内部进行)。
如果只是为了通过列表简单展示数据和完成点击,那么直接使用万能适配器BaseQuickAdapter,非常方便,代码非常简洁;
如果是为了列表的重交互,那么可以查看万能适配器更加详尽的用法,满足你所有的需求。