好了, 通过之前几篇笔记, 对于CoordinatorLayout和AppbarLayout等有了个大概的认识, 特别是对toolbar和放置于AppbarLayout内的控件有了一定的认识, 会做一些个性化的界面.
但还不够, 现在让我们回到Behavior, 再看看Google还给我们留下了什么系统级的Behavior.
BottomSheetBehavior
BottomSheetBehavior实现的效果在我们的项目中用的比较多, 它的作用就是从底部弹出一个布局, 在很多的应用中, 分享功能都有这样一个交互. 有很多种方法可以实现它, 例如PopupWindow等. Google在给我们提供了BottomSheetBehavior之后, 我们有了更简单的实现方式.
先看个效果图, 在底部弹出了一个小视窗, 填充了一些社交ICON, 明显就是可以用于分享的. 我们来看看怎么实现的.
在布局文件中为弹出布局绑定BottomSheetBehavior
<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.truly.scrolldemo.MainActivity">
<android.support.design.widget.AppBarLayout
...
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main" />
<android.support.design.widget.FloatingActionButton
... />
<android.support.v4.widget.NestedScrollView
android:id="@+id/share_layout_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/bottom_sheet_behavior"
app:behavior_peekHeight = "0dp"
app:behavior_hideable = "true">
<include layout="@layout/share_layout" />
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
关键属性有两个:
app:behavior_peekHeight = "0dp"
peekHeight属性设定的是bottomSheet折叠时的高度, 为0的话表示完全折叠(隐藏), 若不为0, 则布局会显示在屏幕的底部某个高度上.
app:layout_behavior="@string/bottom_sheet_behavior"
这句是核心, 给其绑定behavior.
再看下<include> 拉进来的真正share_layout, 其实就是个简单的GridLayout.
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alignmentMode="alignBounds"
android:background="@android:color/white"
android:columnCount="3">
<ImageView
android:id="@+id/img_weibo"
style="@style/MyImgStyle"
android:contentDescription="weibo"
android:src="@drawable/weibo" />
<ImageView
android:id="@+id/img_weixin"
style="@style/MyImgStyle"
android:contentDescription="weixin"
android:src="@drawable/weixin" />
<ImageView
android:id="@+id/img_qq"
style="@style/MyImgStyle"
android:contentDescription="qq"
android:src="@drawable/qq" />
<ImageView
android:id="@+id/img_pyq"
style="@style/MyImgStyle"
android:contentDescription="friend circle"
android:src="@drawable/pyq" />
<ImageView
android:id="@+id/img_email"
style="@style/MyImgStyle"
android:contentDescription="email"
android:src="@drawable/email" />
<ImageView
android:id="@+id/img_evernote"
style="@style/MyImgStyle"
android:contentDescription="evernote"
android:src="@drawable/evernote" />
</GridLayout>
<style name="MyImgStyle">
<item name="android:layout_columnWeight">1</item>
<item name="android:layout_gravity">fill_vertical|fill_horizontal</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:layout_marginBottom">16dp</item>
</style>
然后在代码中看下怎么实现它.
private BottomSheetBehavior sheetBehavior;
private void showBottomSheetView() {
if(sheetBehavior == null){
View view = findViewById(R.id.share_layout_container);
//获取behavior
sheetBehavior = BottomSheetBehavior.from(view);
//sheetBehavior.setHideable(true);
sheetBehavior.setSkipCollapsed(true);
//设置起始时默认隐藏, 正常默认是折叠BottomSheetBehavior.STATE_COLLAPSED
sheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
//设置回调监听
sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
});
MyImgClickListener listener = new MyImgClickListener();
//简单实现布局上点击事件监听
view.findViewById(R.id.img_weibo).setOnClickListener(listener);
view.findViewById(R.id.img_weixin).setOnClickListener(listener);
view.findViewById(R.id.img_qq).setOnClickListener(listener);
view.findViewById(R.id.img_pyq).setOnClickListener(listener);
view.findViewById(R.id.img_email).setOnClickListener(listener);
view.findViewById(R.id.img_evernote).setOnClickListener(listener);
}
if(sheetBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN){
sheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}else{
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
//简单实现布局上点击事件监听
class MyImgClickListener implements View.OnClickListener{
@Override
public void onClick(View view) {
Toast.makeText(mContext,view.getContentDescription().toString(),Toast.LENGTH_SHORT).show();
}
}
我们来看效果图:
底部视图弹出来了, 好像还不错的样子哦. 不过有几个特征要注意:
- 底部视图虽然弹出来了, 但还是可以与主界面交互, 如点击菜单, 点击图标等, 且在做这些操作的时候, 底部视图不消失.
- 点击弹出视图之外的其它位置, 弹出视图不消失
当然, 我们可以在点击弹窗或弹窗上的控件后, 让弹窗隐藏.
class MyImgClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
Toast.makeText(mContext, view.getContentDescription().toString(), Toast.LENGTH_SHORT).show();
if(sheetBehavior != null){
sheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
}
补充: BottomSheetBehavior的5种状态:
- STATE_EXPANDED 展开状态, 显示完整布局
- STATE_COLLAPSED 折叠状态,折叠到peekHeight所指定的高度
- STATE_DRAGGING 拖拽时的状态
- STATE_HIDDEN 隐藏状态
- STATE_SETTLING 释放状态
从以上两点出发, 底部弹出视图BottomSheet可使用的地方还是比较受限的. 这个时候我们可以使用
BottomSheetDialog
BottomSheetDialog和常用Dialog差不多, 它对BottomSheetBehavior进行了封装, 改成了从底部弹出一个Dialog. 它的使用方式比BottomSheetBehavior更方便, 效果更好.
来看代码, 注意: 弹出布局用的是同一个share_layout.xml
private BottomSheetDialog bsDialog;
private void showBottomSheetDialog() {
if (bsDialog == null) {
bsDialog = new BottomSheetDialog(mContext);
//默认Cancelable和CanceledOnTouchOutside均为true
//bsDialog.setCancelable(true);
//bsDialog.setCanceledOnTouchOutside(true);
//为Dialog设置布局
bsDialog.setContentView(R.layout.share_layout);
MyImgClickListener listener = new MyImgClickListener();
bsDialog.findViewById(R.id.img_weibo).setOnClickListener(listener);
bsDialog.findViewById(R.id.img_weixin).setOnClickListener(listener);
bsDialog.findViewById(R.id.img_qq).setOnClickListener(listener);
bsDialog.findViewById(R.id.img_pyq).setOnClickListener(listener);
bsDialog.findViewById(R.id.img_email).setOnClickListener(listener);
bsDialog.findViewById(R.id.img_evernote).setOnClickListener(listener);
}
bsDialog.show();
}
class MyImgClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
Toast.makeText(mContext, view.getContentDescription().toString(), Toast.LENGTH_SHORT).show();
if(bsDialog != null){
bsDialog.dismiss();
}
}
}
效果: 可以看到, Dialog弹出的时候, 主界面暗淡下去, 在Dialog之外点击, Dialog消失.
BottomSheetDialogFragment
Google对Fragment情有独钟, 在底部弹窗这块, 也不忘加个Fragment功能的Dialog.
BottomSheetDialogFragment的用法也比较简单, 不过要绑定一个Fragment, 稍微复杂一些.
新建一个类, 继承BottomSheetDialogFragment, 如果页面布局是一个静态的布局, 复写onCreateView时将弹出窗的布局传入进去, 就差不多完成了, 最多在里面加几个监听方法. 看下代码:
public class SimpleDialogFragment extends BottomSheetDialogFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.share_layout,container, false);
final ImageView imgWeibo = view.findViewById(R.id.img_weibo);
imgWeibo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getActivity(),"weibo",Toast.LENGTH_SHORT).show();
dismiss();
}
});
return view;
}
}
在需要的位置调用它.
private void showSimpleDialogFragment() {
SimpleDialogFragment dialogFragment = new SimpleDialogFragment();
dialogFragment.show(getSupportFragmentManager(),"TAG");
}
效果和BottomSheetDialog差不多.
我们来实现一个略微复杂的Dialog, 还是做一个分享页面, 但页面内容是用RecyclerView导入的.
- 先把页面布局放出来, 方便后面看代码时进行对照. 顶部就是一个标题, 内容由RecyclerView提供.
dialog_fragment_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
...
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="2dp">
<TextView
...
android:id="@+id/dialog_title"
android:text="标题"/>
</android.support.v7.widget.CardView>
<android.support.v7.widget.RecyclerView
android:id="@+id/dialog_content_recycler"
... />
</LinearLayout>
- 既然是用RecyclerView, 那还得为它准备一个行布局
share_item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
android:orientation="horizontal">
<ImageView
android:id="@+id/item_icon"
... />
<TextView
android:id="@+id/item_description"
... />
</LinearLayout>
形成的效果大致如下:
- 新建一个实体类ShareBean.java
public class ShareBean {
private int imgResId;
private String imgDescription;
public ShareBean(int imgResId, String imgDescription) {
this.imgResId = imgResId;
this.imgDescription = imgDescription;
}
public int getImgResId() {
return imgResId;
}
public void setImgResId(int imgResId) {
this.imgResId = imgResId;
}
public String getImgDescription() {
return imgDescription;
}
public void setImgDescription(String imgDescription) {
this.imgDescription = imgDescription;
}
}
好, 基本准备工作做好了,
- 还是新建一个类, 继承BottomSheetDialogFragment, 实现以下方法.
public class MyBottomSheetDialogFragment extends BottomSheetDialogFragment {
private Context mContext;
private TextView tvTitle;
private RecyclerView dialogRecycler;
//入口方法, 方便将Title传入
public static MyBottomSheetDialogFragment newInstance(String title) {
Bundle args = new Bundle();
args.putString("title", title);
MyBottomSheetDialogFragment dialogFragment = new MyBottomSheetDialogFragment();
dialogFragment.setArguments(args);
return dialogFragment;
}
//获取Context上下文
@Override
public void onAttach(Context context) {
mContext = context;
super.onAttach(context);
}
//传入布局
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_fragment_layout, container, false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
//初始化数据
initDialogContentData();
//初始化布局中的控件
tvTitle = view.findViewById(R.id.dialog_title);
dialogRecycler = view.findViewById(R.id.dialog_content_recycler);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
String title = getArguments().getString("title", "Title not Set");
tvTitle.setText(title);
//配置RecyclerView, 设置相应的适配器和LayoutManager
MyAdapter adapter = new MyAdapter();
//recycler的每行item高度(尺寸)都是固定的时候用, 可以节约计算时间
dialogRecycler.setHasFixedSize(true);
dialogRecycler.setLayoutManager(new GridLayoutManager(mContext, 2));
dialogRecycler.setAdapter(adapter);
super.onActivityCreated(savedInstanceState);
}
...
}
考虑到篇幅, 我们把初始化数据放在这个类的内部, 做内部调用.
private List<ShareBean> shares;
private int[] iconResIds = {
R.drawable.weibo, R.drawable.weixin, R.drawable.qq,
R.drawable.pyq, R.drawable.email, R.drawable.evernote};
private String[] iconDescriptions = {
"新浪微博", "微信", "腾讯QQ",
"微信朋友圈", "126邮箱", "印象笔记"
};
private void initDialogContentData() {
shares = new ArrayList<>();
for (int i = 0; i < iconResIds.length; i++) {
ShareBean bean = new ShareBean(iconResIds[i], iconDescriptions[i]);
shares.add(bean);
}
}
同样的, 我们把RecyclerView相应的适配器放在类内部, 形成内部类.
class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = getLayoutInflater().inflate(R.layout.share_item_layout, parent, false);
MyViewHolder holder = new MyViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
holder.ivIcon.setImageDrawable(mContext.getDrawable(shares.get(position).getImgResId()));
holder.tvDescription.setText(shares.get(position).getImgDescription());
View view = holder.ivIcon.getRootView();
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(mContext, shares.get(position).getImgDescription(), Toast.LENGTH_SHORT).show();
dismiss();
}
});
}
@Override
public int getItemCount() {
return shares.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
public ImageView ivIcon;
public TextView tvDescription;
public MyViewHolder(View itemView) {
super(itemView);
ivIcon = itemView.findViewById(R.id.item_icon);
tvDescription = itemView.findViewById(R.id.item_description);
}
}
}
- 就剩下调用它了,
private void showDiaglogFragment() {
MyBottomSheetDialogFragment dialogFragment =
MyBottomSheetDialogFragment.newInstance("分享");
dialogFragment.show(getSupportFragmentManager(),"share");
}
嗯, 应该写完了, 来看下效果.
可以看出, BottomSheetDialogFragment实现的效果和BottomSheetDialog其实差不多, 所以在逻辑要求简单的情况下, 尽量使用BottomSheetDialog.
坑
BottomSheetDialog和BottomSheetDialogFragment也是有些坑的, 两者都是弹出来就是全部弹出来, 不像BottomSheetBehavior的例子一样, 可以设置peekHeight.
BottomSheetdialog可以在布局阶段设置高度, 但拉不起来, 只能往下拉隐藏.
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
...>
BottomSheetDialogFragment更是坑, 高度设了也白设, 还是全部弹出来.
<LinearLayout
...
android:layout_width="match_parent"
android:layout_height="100dp"
...>
SwipeDismissBehavior
这个Behavior用的机会比较小, 我们大致介绍下.
顾名思义, SwipeDismissBehavior是用来实现滑动删除的. FloatingActionButton自带了这个功能, 但非常僵硬, 没有跟随手指的感觉. 我们来看下.
我们自己做个例子来体验一下:
做个布局, 将一个TextView放置于CoordinatorLayout下.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ...>
...
<TextView
android:id="@+id/swipe_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:lineSpacingMultiplier="2"
android:text="@string/little_text" />
<android.support.design.widget.FloatingActionButton... />
</android.support.design.widget.CoordinatorLayout>
代码:
private void initSwipeDismissBehavior() {
TextView swipeLayout = findViewById(R.id.swipe_layout);
SwipeDismissBehavior behavior = new SwipeDismissBehavior();
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_ANY);
behavior.setSensitivity(0.8f);
behavior.setDragDismissDistance(0.2f);
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
@Override
public void onDismiss(View view) {
//Todo
}
@Override
public void onDragStateChanged(int state) {
//Todo
}
});
CoordinatorLayout.LayoutParams layoutParams =
(CoordinatorLayout.LayoutParams) swipeLayout.getLayoutParams();
layoutParams.setBehavior(behavior);
}
效果, 可以看出还是很生涩, 不跟手. 看看Google以后会不会更新这个效果吧.
这里将滑动方向设置为”ANY”, 实际发现
- 手指从左向右滑, 文本是向右滑除,
- 手指从右向左滑, 文本也是向右滑除
说明, SwipeDirection指的是手指动作方向, 而不是View的滑除方向.
总的来说, SwipeDismissBehavior目前实用性不强.
总结:
好了, 至此几个系统级的Behavior都讲过一遍, 算是有了个大致印象. 但实际使用中, 这几个Behavior远远不够, 要想实现一些比较炫酷的操作, 还得自定义Behavior. 后面章节我们开始着手自定义Behavior的学习.
8人点赞
作者:朋朋彭哥
链接:https://www.jianshu.com/p/d3dacc58077d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。