BottomSheet底部弹窗

作者: 朋朋彭哥 | 来源:发表于2018-01-22 20:35 被阅读0次

    好了, 通过之前几篇笔记, 对于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的学习.

    相关文章

      网友评论

        本文标题:BottomSheet底部弹窗

        本文链接:https://www.haomeiwen.com/subject/tzeraxtx.html