Bottom Sheet使用教程
什么是Bottom Sheet?
Bottom Sheet是Design Support Library23.2 版本引入的一个类似于对话框的控件,可以暂且叫做底部弹出框吧。 Bottom Sheet中的内容默认是隐藏起来的,只显示很小一部分,可以通过在代码中设置其状态或者手势操作将其完全展开,或者完全隐藏,或者部分隐藏。对于Bottom Sheet的描述可以在官网查询:https://material.io/guidelines/components/bottom-sheets.html#
其实在 Bottom Sheet出现之前已经有人实现了相同的功能,最早的一个可靠版本应该是AndroidSlidingUpPanel,当然它实现的原理跟谷歌的方式完全不一样。
Bottom Sheet的类型
有两种类型的Bottom Sheet:
1.Persistent bottom sheet :- 通常用于显示主界面之外的额外信息,它是主界面的一部分,只不过默认被隐藏了,其深度(elevation)跟主界面处于同一级别;还有一个重要特点是在Persistent bottom sheet打开的时候,主界面仍然是可以操作的。ps:Persistent bottom sheet该如何翻译呢?我觉得翻译为普通bottom sheet就好了,还看到有人翻译为“常驻bottom sheet”,可能更接近于英语的字面意思,可是反而不易理解。
2.模态bottom sheet :- 顾名思义,模态的bottom sheet在打开的时候会阻止和主界面的交互,并且在视觉上会在bottom sheet背后加一层半透明的阴影,使得看上去深度(elevation)更深。
总结起来这两种Bottom Sheet的区别主要在于视觉和交互上,当然使用方法也是不一样的。
基本用法
不管是普通bottom sheet还是模态的bottom sheet,都需要依赖:
dependencies {
...
compile 'com.android.support:design:24.1.1'
}
当然现在的app一般都要依赖这个兼容库,版本号只要保证是在23.2.0及其以后就可以了。
Persistent bottom sheet的用法
其实Persistent bottom sheet不能算是一个控件,因为它只是一个普通的布局在CoordinatorLayout这个布局之下所表现出来的特殊行为。所以其使用方式跟普通的控件也很不一样,它必须在CoordinatorLayout中,并且是CoordinatorLayout的直接子view。
定义主界面与bottom sheet的布局
为了让xml代码看起来不那么长,我们把布局分为content_main和content_bottom_sheet两部分,content_main主要是一些按钮,用于切换bottom sheet的状态,content_bottom_sheet才是bottom sheet的内容。
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.androidtutorialshub.bottomsheets.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<!-- Main Content -->
<include layout="@layout/content_main" />
<!-- Bottom Sheet Content -->
<include layout="@layout/content_bottom_sheet" />
</android.support.design.widget.CoordinatorLayout>
content_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.androidtutorialshub.bottomsheets.MainActivity"
tools:showIn="@layout/activity_main">
<Button
android:id="@+id/expand_bottom_sheet_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/text_expand_bottom_sheet" />
<Button
android:id="@+id/collapse_bottom_sheet_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/expand_bottom_sheet_button"
android:text="@string/text_collapse_bottom_sheet" />
<Button
android:id="@+id/hide_bottom_sheet_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/collapse_bottom_sheet_button"
android:text="@string/text_hide_bottom_sheet" />
<Button
android:id="@+id/show_bottom_sheet_dialog_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/hide_bottom_sheet_button"
android:text="@string/text_show_bottom_sheet_dialog" />
</RelativeLayout>
content_bottom_sheet.xml
这里定义的布局就是bottom sheet的界面。这里是一个相对布局,其实你可以定义任意布局,唯一的要求是需要定义app:layout_behavior="@string/bottom_sheet_behavior",定义了这个属性就相当于告诉了CoordinatorLayout这个布局是一个bottom sheet,它的显示和交互都和普通的view不同。@string/bottom_sheet_behavior是一个定义在支持库中的字符串,等效于android.support.design.widget.BottomSheetBehavior。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bottomSheetLayout"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@android:color/holo_orange_light"
android:padding="@dimen/activity_vertical_margin"
app:behavior_hideable="true"
app:behavior_peekHeight="60dp"
app:layout_behavior="@string/bottom_sheet_behavior">
<TextView
android:id="@+id/bottomSheetHeading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/text_expand_me"
android:textAppearance="@android:style/TextAppearance.Large" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/bottomSheetHeading"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:text="@string/text_welcome_message"
android:textAppearance="@android:style/TextAppearance.Large" />
</RelativeLayout>
其实你还可以看到这里除了app:layout_behavior之外,还有两个属性
app:behavior_hideable="true"
app:behavior_peekHeight="60dp"
其中app:behavior_hideable="true"表示你可以让bottom sheet完全隐藏,默认为false;app:behavior_peekHeight="60dp"表示当为STATE_COLLAPSED(折叠)状态的时候bottom sheet残留的高度,默认为0。
当我们按照上面得代码配置好布局之后,其实一个bottom sheet就已经完成了,在CoordinatorLayout和bottom_sheet_behavior的共同作用下,content_bottom_sheet布局就成了一个bottom sheet, 但是我们还需要知道如何控制它。
控制Persistent bottom sheet
我们在MainActivity.java中添加一些代码,以处理bottom sheet,以及监听bottom sheet状态变化。
bottom sheet有以下5种状态
-
STATE_COLLAPSED: 默认的折叠状态, bottom sheets只在底部显示一部分布局。显示高度可以通过 app:behavior_peekHeight 设置(默认是0)
-
STATE_DRAGGING : 过渡状态,此时用户正在向上或者向下拖动bottom sheet
-
STATE_SETTLING: 视图从脱离手指自由滑动到最终停下的这一小段时间
-
STATE_EXPANDED: bottom sheet 处于完全展开的状态:当bottom sheet的高度低于CoordinatorLayout容器时,整个bottom sheet都可见;或者CoordinatorLayout容器已经被bottom sheet填满。
-
STATE_HIDDEN : 默认无此状态(可通过app:behavior_hideable 启用此状态),启用后用户将能通过向下滑动完全隐藏 bottom sheet
bottom sheet的状态是通过BottomSheetBehavior来设置的,因此需要先得到BottomSheetBehavior对象,然后调用BottomSheetBehavior.setState()来设置状态,比如设置为折叠状态:
BottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
我们还可以通过BottomSheetBehavior.getState() 来获得状态。
要监听bottom sheet的状态变化则使用setBottomSheetCallback方法,之所以需要监听是因为bottom sheet的状态还可以通过手势来改变。
具体使用见下面的代码:
MainActivity.java
package com.androidtutorialshub.bottomsheets;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// BottomSheetBehavior variable
private BottomSheetBehavior bottomSheetBehavior;
// TextView variable
private TextView bottomSheetHeading;
// Button variables
private Button expandBottomSheetButton;
private Button collapseBottomSheetButton;
private Button hideBottomSheetButton;
private Button showBottomSheetDialogButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initListeners();
}
/**
* method to initialize the views
*/
private void initViews() {
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottomSheetLayout));
bottomSheetHeading = (TextView) findViewById(R.id.bottomSheetHeading);
expandBottomSheetButton = (Button) findViewById(R.id.expand_bottom_sheet_button);
collapseBottomSheetButton = (Button) findViewById(R.id.collapse_bottom_sheet_button);
hideBottomSheetButton = (Button) findViewById(R.id.hide_bottom_sheet_button);
showBottomSheetDialogButton = (Button) findViewById(R.id.show_bottom_sheet_dialog_button);
}
/**
* method to initialize the listeners
*/
private void initListeners() {
// register the listener for button click
expandBottomSheetButton.setOnClickListener(this);
collapseBottomSheetButton.setOnClickListener(this);
hideBottomSheetButton.setOnClickListener(this);
showBottomSheetDialogButton.setOnClickListener(this);
// Capturing the callbacks for bottom sheet
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetHeading.setText(getString(R.string.text_collapse_me));
} else {
bottomSheetHeading.setText(getString(R.string.text_expand_me));
}
// Check Logs to see how bottom sheets behaves
switch (newState) {
case BottomSheetBehavior.STATE_COLLAPSED:
Log.e("Bottom Sheet Behaviour", "STATE_COLLAPSED");
break;
case BottomSheetBehavior.STATE_DRAGGING:
Log.e("Bottom Sheet Behaviour", "STATE_DRAGGING");
break;
case BottomSheetBehavior.STATE_EXPANDED:
Log.e("Bottom Sheet Behaviour", "STATE_EXPANDED");
break;
case BottomSheetBehavior.STATE_HIDDEN:
Log.e("Bottom Sheet Behaviour", "STATE_HIDDEN");
break;
case BottomSheetBehavior.STATE_SETTLING:
Log.e("Bottom Sheet Behaviour", "STATE_SETTLING");
break;
}
}
@Override
public void onSlide(View bottomSheet, float slideOffset) {
}
});
}
/**
* onClick Listener to capture button click
*
* @param v
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.collapse_bottom_sheet_button:
// Collapsing the bottom sheet
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
break;
case R.id.expand_bottom_sheet_button:
// Expanding the bottom sheet
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
break;
case R.id.hide_bottom_sheet_button:
// Hiding the bottom sheet
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
break;
case R.id.show_bottom_sheet_dialog_button:
break;
}
}
}
模态bottom sheet的用法
模态bottom sheet用法跟传统的dialog很类似,它是一个BottomSheetDialogFragment。
首先定义好BottomSheetDialogFragment的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bottomSheetLayout"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@android:color/holo_red_light"
android:padding="@dimen/activity_vertical_margin"
>
<TextView
android:id="@+id/bottomSheetHeading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/text_dialog_bottom_sheet"
android:textAppearance="@android:style/TextAppearance.Large" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/bottomSheetHeading"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:text="@string/text_welcome_message"
android:textAppearance="@android:style/TextAppearance.Large" />
</RelativeLayout>
注意这里不再需要定义behavior 和peekHeight之类的东西了。
创建一个继承了BottomSheetDialogFragment的CustomBottomSheetDialogFragment 类,在onCreateView方法中把上面的布局传递进去
package com.androidtutorialshub.bottomsheets;
import android.os.Bundle;
import android.support.design.widget.BottomSheetDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class CustomBottomSheetDialogFragment extends BottomSheetDialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.content_dialog_bottom_sheet, container, false);
return v;
}
}
显示这个模态的bottom sheet
new CustomBottomSheetDialogFragment().show(getSupportFragmentManager(), "Dialog");
与普通bottom sheet不同的是我们不需要处理它的状态了,因为它跟普通bottom sheet机制都不同,只有打开和关闭状态,而且是通过点击bottom sheet之外的区域来取消bottom sheet的。
总结
由此可以看到Persistent bottom sheet是最复杂的,而模态bottom sheet基本没什么新东西。
在Persistent bottom sheet使用方法小节中我们是点击一个item切换一个状态,实际使用肯定不是这样,一般是点击一个按钮,在不同状态之间toggle。
为此我在上面的基础上增加一个按钮,然后在onclick中增加toggle的代码,顺便将BottomSheetDialogFragment的代码也添加到MainActivity.java中:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.collapse_bottom_sheet_button:
// Collapsing the bottom sheet
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
break;
case R.id.expand_bottom_sheet_button:
// Expanding the bottom sheet
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
break;
case R.id.hide_bottom_sheet_button:
// Hiding the bottom sheet
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
break;
case R.id.show_bottom_sheet_dialog_button:
// Opening the Dialog Bottom Sheet
new CustomBottomSheetDialogFragment().show(getSupportFragmentManager(), "Dialog");
break;
case R.id.bottom_sheet_toggle:
if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED ){
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
} else if(bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN || bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED){
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
break;
}
}
整个demo的代码可以在github下载https://github.com/jianghejie/bottom-sheet-tutorial
补充
Persistent bottom sheet xml布局中的
app:behavior_hideable="true"
app:behavior_peekHeight="60dp"
可以用代码实现
mBottomSheetBehavior.setHideable(true);
mBottomSheetBehavior.setPeekHeight(300);
坑
如果连续两次使用bottomSheetBehavior.setState()都设置的是同一状态的话,其状态会变成 STATE_SETTLING,感觉这样很不合理
比如连续2次执行下面的代码(比如我们demo中两次点击同一按钮的情况)
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
状态就会变成STATE_SETTLING,而不是STATE_EXPANDED。
第三方的bottom sheet
参考文章
本文代码来自Android Material Design Bottom Sheets Tutorial一文,有修改。
其它文章参考:
http://www.androidauthority.com/bottom-sheets-707252/