Android开发-BottomSheet的使用和说明

介绍

在官方提供的android.support.design.widget包中,加入bottom sheet能够很方便的实现一些酷炫的功能。官方blog说明得比较简单,中文博客上也有一些介绍和说明 Android Bottom Sheet详解本文就是从这些博文中开始探索,最后总结一些使用经验和封装方便之后的使用。

简单的代码

由于Android新加入的behavior注入机制,我们可以只写简单的xml布局文件就能够实现bottomSheet的基础功能。
下文最关键的代码就是app:layout_behavior="@string/bottom_sheet_behavior"

官方特别说明: Keep in mind that scrolling containers in your bottom sheet must support nested scrolling (for example, NestedScrollView, RecyclerView, or ListView/ScrollView on API 21+).
翻译:如果你的buttonsheet内部需要滑动,那你的bottomsheet必须使用支持嵌套滑动的控件,比如NestedScrollView,RecyclerView或者API21以上的 ListView/ScrollView 。

xml代码

首先我定义activity_bottom_sheet.xml布局文件

<?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:id="@+id/activity_bottom_sheet"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.licola.myandroiddemo.BottomSheetActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center|top"
        android:text="main"
        android:id="@+id/txt_main"
        android:textColor="@android:color/black"
        android:textSize="30sp" />

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:behavior_peekHeight="200dp"
        android:id="@+id/nested_view"
        app:layout_behavior="@string/bottom_sheet_behavior">

        <LinearLayout 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:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@color/colorAccent"
                android:gravity="center|top"
                android:text="sheet_first" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@color/colorPrimaryDark"
                android:gravity="center|top"
                android:text="sheet_second" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@color/colorAccent"
                android:gravity="center|top"
                android:text="sheet_third" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="@color/colorPrimaryDark" />

        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

运行结果

运行结果

运行结果就可以看到NestedScrollView作为具有bottom_sheet_behavior行为能力的buttomsheet恰好位于主页的底部,且露出的部分刚好就是app:behavior_peekHeight="200dp"的高度,并且是支持嵌套滑动的。

特别说明:peekHeight是一个很关键的参数,他决定buttomsheet的悬浮高度,在design-24里面定义资源定义<dimen name="design_bottom_sheet_modal_peek_height">256dp</dimen>

加入控制

上面的代码全部由系统控制buttomsheet的状态,如果我们想要自己手动控制它就需要一些java代码

public class BottomSheetActivity extends AppCompatActivity {


    private BottomSheetBehavior behavior;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bottom_sheet);
        final View view = findViewById(R.id.nested_view);

        View viewButtom = findViewById(R.id.txt_main);
        //通过点击 控制展开或者隐藏
        viewButtom.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                boolean operate = !view.isSelected();
                if (operate) {
                    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                } else {
                    behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
                }
                view.setSelected(operate);
            }
        });
        //静态方法得到实例
        behavior = BottomSheetBehavior.from(view);
        //设置它能够有隐藏功能
        behavior.setHideable(true);
        //状态切换的回调
        behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                String log;
                switch (newState) {
                    case BottomSheetBehavior.STATE_DRAGGING:
                        log = "STATE_DRAGGING";
                        break;
                    case BottomSheetBehavior.STATE_SETTLING:
                        log = "STATE_SETTLING";
                        break;
                    case BottomSheetBehavior.STATE_EXPANDED:
                        log = "STATE_EXPANDED";
                        break;
                    case BottomSheetBehavior.STATE_COLLAPSED:
                        log = "STATE_COLLAPSED";
                        break;
                    case BottomSheetBehavior.STATE_HIDDEN:
                        log = "STATE_HIDDEN";
                        break;
                    default:
                        log = "default";
                        break;
                }
                Logger.d(log);
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {

            }
        });

    }
}

说明:关键步骤的说明全局在相应的注释里面了。

关键参数说明

  • STATE_COLLAPSED(悬浮): this collapsed state is the default and shows just a portion of the layout along the bottom. The height can be controlled with the app:behavior_peekHeight attribute (defaults to 0)
  • STATE_DRAGGING(手势拖动): the intermediate state while the user is directly dragging the bottom sheet up or down
  • STATE_SETTLING(自动滑动): that brief time between when the View is released and settling into its final position
  • STATE_EXPANDED(展开): the fully expanded state of the bottom sheet, where either the whole bottom sheet is visible (if its height is less than the containing CoordinatorLayout) or the entire CoordinatorLayout is filled
  • STATE_HIDDEN(隐藏): disabled by default (and enabled with the app:behavior_hideable attribute), enabling this allows users to swipe down on the bottom sheet to completely hide the bottom sheet

共5个状态,其中STATE_COLLAPSED悬浮就和peek_height的有直接的关系。

举例说明:当我们的视图就像上图那样。
1. 点击直接改变状态,展开或者隐藏:STATE_SETTLING–>STATE_HIDDEN(或者STATE_EXPANDED)
2. 通过滑动改变状态,向上滑动:STATE_DRAGGING–>STATE_SETTLING–>STATE_EXPANDED

BottomSheetDialogFragment

上面简单介绍一些基础功能的使用,当我们真正使用到项目中有时就需要BottomSheetDialogFragment。在确定统一特性之后还可以对其进行封装。、

首先看一下部分效果图吧:
购买弹框 底部功能弹框

这两个实现都是基于BottomSheetDialogFragment的封装使用,并且解决部分使用的特殊问题。特别指出的是,在我的实际项目中BottomSheetDialogFragment不作为固定在某个界面底部的视图,而是由某个功能按钮引出。所以就BottomSheetDialogFragment就退化成只有展开或者收起的两个状态,但是还是保有它的动画和手势控制特性。

BaseBottomSheetFrag抽象父类

先上代码:

import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.BottomSheetDialog;
import android.support.design.widget.BottomSheetDialogFragment;
import android.support.v4.app.FragmentManager;
import android.view.View;
import android.view.ViewGroup;
import com.licola.logger.Logger;

/**
 * Created by 李可乐 on 2016/9/8 0008.
 */
public abstract class BaseBottomSheetFrag extends BottomSheetDialogFragment {

  protected Context mContext;

  protected View rootView;
  protected BottomSheetBehavior mBehavior;

  protected Dialog dialog;

  @Override public void onAttach(Context context) {
    super.onAttach(context);
    this.mContext = context;
  }

  @Override public void onStart() {
    super.onStart();
    mBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
  }

  @Override public void onDestroyView() {
    super.onDestroyView();
    //解除缓存View和当前ViewGroup的关联
    ((ViewGroup) (rootView.getParent())).removeView(rootView);
  }

  @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
    //每次打开都调用该方法 类似于onCreateView 用于返回一个Dialog实例
    dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
    if (rootView == null) {
      //缓存下来的View 当为空时才需要初始化 并缓存
      rootView = View.inflate(mContext, getLayoutResId(), null);
      initView();
    }

    setContentView(dialog);//设置View重新关联
    mBehavior = BottomSheetBehavior.from((View) rootView.getParent());
    mBehavior.setHideable(true);
    //让父View背景透明 是圆角边的关键
    ((View) rootView.getParent()).setBackgroundColor(Color.TRANSPARENT);

    rootView.post(new Runnable() {
      @Override public void run() {
        /**
         * PeekHeight默认高度256dp 会在该高度上悬浮
         * 设置等于view的高 就不会卡住
         */
        mBehavior.setPeekHeight(rootView.getHeight());
      }
    });

    resetView();

    return dialog;
  }

  /**
   * 设置显示的View到Dialog中
   * 抽象方法 子类可重写
   * 默认添加的View 高度为Wrap 某些场景需要固定高度
   * @param dialog
   */
  protected void setContentView(Dialog dialog) {
    dialog.setContentView(rootView);
  }

  public abstract int getLayoutResId();

  /**
   * 初始化View和设置数据等操作的方法
   */
  public abstract void initView();

  /**
   * 重置的View和数据的空方法 子类可以选择实现
   * 为避免多次inflate 父类缓存rootView
   * 所以不会每次打开都调用{@link #initView()}方法
   * 但是每次都会调用该方法 给子类能够重置View和数据
   */
  public void resetView() {

  }

  public boolean isShowing() {
    return dialog != null && dialog.isShowing();
  }

  /**
   * 使用关闭弹框 是否使用动画可选
   * 使用动画 同时切换界面Aty会卡顿 建议直接关闭
   */
  public void close(boolean isAnimation) {
    if (isAnimation) {
      if (mBehavior != null) {
        mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
      }
    } else {
      dismiss();
    }
  }

  @Override public void show(FragmentManager manager, String tag) {
    if (!this.isAdded()) {
      super.show(manager, tag);
    } else {
      Logger.d(this + " has add to FragmentManager");
    }
  }
}

上面的BaseBottomSheetFrag的抽象父类已经定义好了各个子类实现步骤,采用设计模式的模板方法模式。每个关键操作都有注释说明。

更新记录

  • 抽象设置View方法,让子类能够动态控制显示高度。

某些时候我们需要动态的或者根据设计需求实现Dialog的高度。抽象出方法实现该功能。
示例代码如下:

@Override protected void setContentView(Dialog dialog) {
    int screenHeight = PixelUtils.getScreenHeight(mContext);
    int height = (screenHeight * 3) >> 2;//屏幕高的75%
    ViewGroup.LayoutParams layoutParams =
        new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, height);
    dialog.setContentView(rootView, layoutParams);//设置View 并带有布局参数的
  }

其中两个关键部分还是值得说明的

圆角边的透明背景色

在上图中明显的看到,我的实现是有圆角边的。圆角的实现不在本文的讨论范围,假设你已经配置好圆角边给了具体的某个View,然后作为视图添加到BaseBottomSheetFrag中,你会发现在实现得有些奇怪,圆角边之外不是想要的透明。

源码分析

这里就需要到源码中找究竟了。在BottomSheetDialog中我们看到视图相关的实现代码是:


private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
        final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
                R.layout.design_bottom_sheet_dialog, null);
        if (layoutResId != 0 && view == null) {
            view = getLayoutInflater().inflate(layoutResId, coordinator, false);
        }
        FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
        BottomSheetBehavior.from(bottomSheet).setBottomSheetCallback(mBottomSheetCallback);
        //我们子类的添加View是直接添加到bottomSheet中的 
        if (params == null) {
            bottomSheet.addView(view);
        } else {
            bottomSheet.addView(view, params);
        }
        // We treat the CoordinatorLayout as outside the dialog though it is technically inside
        if (shouldWindowCloseOnTouchOutside()) {
            coordinator.findViewById(R.id.touch_outside).setOnClickListener(
                    new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            if (isShowing()) {
                                cancel();
                            }
                        }
                    });
        }
        return coordinator;
    }

再看design_bottom_sheet_dialog布局文件

<?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">

    <View
            android:id="@+id/touch_outside"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:soundEffectsEnabled="false"/>

    <FrameLayout
            android:id="@+id/design_bottom_sheet"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal|top"
            android:clickable="true"
            app:layout_behavior="@string/bottom_sheet_behavior"
            style="?attr/bottomSheetStyle"/>
           <!--子类添加的View被直接add给它,如果他背景不透明,子类怎么修改都没有用--> 
           <!--顺带说明:这里的app:layout_behavior是不是很眼熟-->

</android.support.design.widget.CoordinatorLayout>

解决方案

看到源码就知道怎么解决透明背景色的问题了。设置父View的背景透明。让圆角边真正实现,就简单一行代码。

((View) rootView.getParent()).setBackgroundColor(Color.TRANSPARENT);

动态PeekHeight

前文说到PeekHeight是悬浮的关键,那如果我们不需要悬浮,只有显示隐藏两个状态。那就要动态设置PeekHeight的高度,也就是view的高度。而怎么取高度就是实现的关键。

具体的讨论在这里:http://stackoverflow.com/questions/3591784/getwidth-and-getheight-of-view-returns-0
里面提供了很多的解决方法。
我这里就很简单的post执行,让取高度方法在主线程较后面执行,在View的各个关键方法执行之后再执行我们的取高度方法。


 rootView.post(new Runnable() {
            @Override
            public void run() {
                /**
                 * PeekHeight默认高度256dp 会在该高度上悬浮
                 * 设置等于view的高 就不会卡住
                 */
                mBehavior.setPeekHeight(rootView.getHeight());
            }
        });

就是这样的post方法到主线程队列中。保证方法执行顺序。

BottomDialogFrag底部对话弹框

最后使用构造器方法封装了一个统一样式的的底部弹框,动态添加功能模块和item。

/**
 * Created by 李可乐 on 2016/10/18 0018.
 * 底部对话框
 */

public class BottomDialogFrag extends BaseBottomSheetFrag {

    public static final String TAG = "BottomDialogFrag";

    private ArrayList<String> titles;
    private ArrayList<OnItemClickListener> listeners;
    private ArrayList<String> hideTitles;

    TextView txtBottomCancel;
    LinearLayout layoutItemGroup;

    public BottomDialogFrag() {

    }

    @SuppressLint("ValidFragment")
    private BottomDialogFrag(BottomSheetBuilder builder) {
        this.titles = builder.titles;
        this.listeners = builder.listeners;
        hideTitles = new ArrayList<>(builder.titles.size());
    }


    @Override
    public int getLayoutResId() {
        return R.layout.fragment_base_bottomsheet;
    }

    @Override
    public void initView() {
        LayoutInflater inflater = LayoutInflater.from(mContext);
        layoutItemGroup = ButterKnife.findById(rootView, R.id.layout_item_group);
        txtBottomCancel = ButterKnife.findById(rootView, R.id.txt_bottom_cancel);
        txtBottomCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                close(true);
            }
        });

        for (int i = 0, size = titles.size(); i < size; i++) {
            String title = titles.get(i);
            boolean isBreak = false;
            for (String hideTitle : hideTitles) {
                if (hideTitle.equals(title)) {
                    isBreak = true;
                    break;
                }
            }
            if (!isBreak) {
                TextView txtItem = (TextView) inflater.inflate(R.layout.item_base_bottomsheet, layoutItemGroup, false);
                txtItem.setText(title);
                final OnItemClickListener listener = listeners.get(i);
                txtItem.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (listener != null) {
                            listener.OnClick();
                        }
                    }
                });
                layoutItemGroup.addView(txtItem);
            }

        }
    }


    /**
     * 隐藏某个item
     * @param title
     */
    public void hideTitleItem(String title) {
        hideTitles.add(title);
    }

    /**
     * 清除隐藏集合
     */
    public void clearHideTitle() {
        hideTitles.clear();
    }

    public interface OnItemClickListener {
        void OnClick();
    }

    public static class BottomSheetBuilder {

        //必要参数 标题集合
        private ArrayList<String> titles;
        //必要参数 监听器集合
        private ArrayList<OnItemClickListener> listeners;

        //非必要参数 配置默认参数


        /**
         * 构造器的构造方法 初始化参数
         */
        public BottomSheetBuilder() {
            titles = new ArrayList<>();
            listeners = new ArrayList<>();
        }

        public BottomDialogFrag build() {
            if (titles == null || titles.isEmpty()) {
                Logger.e("can not empty titles");
            }

            if (listeners == null || listeners.isEmpty()) {
                Logger.e("can not empty listeners");
            }

            return new BottomDialogFrag(this);
        }

        public BottomSheetBuilder appendItem(String title, OnItemClickListener clickListener) {
            this.titles.add(title);
            this.listeners.add(clickListener);
            return this;
        }
    }
}

封装之后使用代码就比较简单了 ,使用前全部定义好,具体使用再调用hideTitleItem()去隐藏某个不需要的item项。思想类似AlertDialog.Builder()

BottomDialogFrag bottomDialogFrag = new BottomDialogFrag.BottomSheetBuilder()
                .appendItem("标题1", new BottomDialogFrag.OnItemClickListener() {
                    @Override
                    public void OnClick() {

                    }
                })
                .appendItem("标题2", new BottomDialogFrag.OnItemClickListener() {
                    @Override
                    public void OnClick() {

                    }
                })
                .build();

总结

好久没有更新博客,最近项目完成,终于有时间更新了。

参考:
1. http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0823/6560.html
2. http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0228/4009.html
3. http://android-developers.blogspot.jp/2016/02/android-support-library-232.html

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值