好了, 通过之前几篇笔记, 对于CoordinatorLayout和AppbarLayout等有了个大概的认识, 特别是对toolbar和放置于AppbarLayout内的控件有了一定的认识, 会做一些个性化的界面.

但还不够, 现在让我们回到Behavior, 再看看Google还给我们留下了什么系统级的Behavior.

BottomSheetBehavior

BottomSheetBehavior实现的效果在我们的项目中用的比较多, 它的作用就是从底部弹出一个布局, 在很多的应用中, 分享功能都有这样一个交互. 有很多种方法可以实现它, 例如PopupWindow等. Google在给我们提供了BottomSheetBehavior之后, 我们有了更简单的实现方式.

先看个效果图, 在底部弹出了一个小视窗, 填充了一些社交ICON, 明显就是可以用于分享的. 我们来看看怎么实现的.

 
image.png

在布局文件中为弹出布局绑定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();
    }
}

我们来看效果图:

 
share_01.gif

底部视图弹出来了, 好像还不错的样子哦. 不过有几个特征要注意:

  • 底部视图虽然弹出来了, 但还是可以与主界面交互, 如点击菜单, 点击图标等, 且在做这些操作的时候, 底部视图不消失.
  • 点击弹出视图之外的其它位置, 弹出视图不消失

当然, 我们可以在点击弹窗或弹窗上的控件后, 让弹窗隐藏.

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);
        }
    }
}
 
bottom_01.gif

补充: 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消失.

 
dialog_1.gif

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_2.gif

我们来实现一个略微复杂的Dialog, 还是做一个分享页面, 但页面内容是用RecyclerView导入的.

  1. 先把页面布局放出来, 方便后面看代码时进行对照. 顶部就是一个标题, 内容由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>
  1. 既然是用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>

形成的效果大致如下:

 
image.png
  1. 新建一个实体类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;
    }
}

好, 基本准备工作做好了,

  1. 还是新建一个类, 继承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);
        }
    }

}
  1. 就剩下调用它了,

private void showDiaglogFragment() {
    MyBottomSheetDialogFragment dialogFragment =
        MyBottomSheetDialogFragment.newInstance("分享");
    dialogFragment.show(getSupportFragmentManager(),"share");
}

嗯, 应该写完了, 来看下效果.

 
dialog_3.gif

可以看出, 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"
    ...>
 
dialog_4.gif

BottomSheetDialogFragment更是坑, 高度设了也白设, 还是全部弹出来.

<LinearLayout 
    ...
    android:layout_width="match_parent"
    android:layout_height="100dp"
    ...>
 
image.png

SwipeDismissBehavior

这个Behavior用的机会比较小, 我们大致介绍下.

顾名思义, SwipeDismissBehavior是用来实现滑动删除的. FloatingActionButton自带了这个功能, 但非常僵硬, 没有跟随手指的感觉. 我们来看下.

 
swipe_1.gif

我们自己做个例子来体验一下:

做个布局, 将一个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以后会不会更新这个效果吧.

 
swipe_2.gif

这里将滑动方向设置为”ANY”, 实际发现

  • 手指从左向右滑, 文本是向右滑除,
  • 手指从右向左滑, 文本也是向右滑除

说明, SwipeDirection指的是手指动作方向, 而不是View的滑除方向.

总的来说, SwipeDismissBehavior目前实用性不强.

总结:

好了, 至此几个系统级的Behavior都讲过一遍, 算是有了个大致印象. 但实际使用中, 这几个Behavior远远不够, 要想实现一些比较炫酷的操作, 还得自定义Behavior. 后面章节我们开始着手自定义Behavior的学习.

 
 

8人点赞

 
 

作者:朋朋彭哥
链接:https://www.jianshu.com/p/d3dacc58077d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。