关于Google在Metrail Design 风格中新推出的dialog,BottomSheetDialog 在现在不少App中都能看到,我就在抖音和网易新闻上看到过,不管是样式还是新颖的交互来说都是不错的,国内设计师习惯将弹窗的最底部增加一个功能性的按键
类似于上面图片这种,弹窗的高度还要根据数据条目的个数去适配,如果使用普通的dialog 即没有好的交互体验,弹出的高度也需要我们自己计算,这无疑是增加了开发难度,但是如果使用BottomSheetDialog ,底部去支付的按钮始终是在数据的下方,如果只有两三条数据还好,如果数据过多则需要将列表滑动到最下方才能看到功能键,这无疑是一个非常糟糕的交互,今天我们就通过查看BottomSheetDialog 来修改他,让他可以存放一个始终放下最下方的区域,先来看一下他的源码
public class BottomSheetDialog extends AppCompatDialog {
....省略部分代码
@Override
public void setContentView(@LayoutRes int layoutResId) {
super.setContentView(wrapInBottomSheet(layoutResId, null, null));
}
/**
* 我们知道要实现BottomSheetDialog 离不开BottomSheetBehavior 的支持,
*这里将 contentView 添加到 一个拥有 BottomSheetBehavior 的布局当中
**/
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
R.layout.design_bottom_sheet_dialog, null);//事先加载一个拥有BottomSheetBehavior 的布局
if (layoutResId != 0 && view == null) {
view = getLayoutInflater().inflate(layoutResId, coordinator, false);
}
FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
mBehavior = BottomSheetBehavior.from(bottomSheet);//获取到布局BottomSheetBehavior
mBehavior.setBottomSheetCallback(mBottomSheetCallback);
mBehavior.setHideable(mCancelable);
if (params == null) {
bottomSheet.addView(view);//将contentView 添加入容器中
} else {
bottomSheet.addView(view, params);//将contentView 添加入容器中
}
// We treat the CoordinatorLayout as outside the dialog though it is technically inside
coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
cancel();
}
}
});
return coordinator;
}
....省略部分代码
}
其实从上面这段代码根本看不出来什么,只是知道contentView被放入的一个拥有BottomSheetBehavior 的容器总,想要在原有的基础上增加一个可拓展的底部功能区域,就需要修改原始的layout布局,
design_bottom_sheet_dialog.xml 布局文件
CoordinatorLayout 包裹了我们的contentView的容器,也就是design_bottom_sheet 这个FrameLayout,想要给底部增加一个按钮,只需要让CoordinatorLayout 的marginBottom 与 底部的区域高度相等即可,
修改后的文件如下
修改后的BottomSheetDialog 如下
open class BaseBottomSheetDialog : AppCompatDialog {
private var mBehavior: BottomSheetBehavior? = null
private var mCancelable = true
private var mCanceledOnTouchOutside = true
private var mCanceledOnTouchOutsideSet = false
protected var mContext: Activity? = null
constructor(context: Activity) : this(context, 0) {
this.mContext = context
}
constructor(context: Context, @StyleRes theme: Int) : super(context, getThemeResId(context, theme)) {
// We hide the title bar for any style configuration. Otherwise, there will be a gap
// above the bottom sheet when it is expanded.
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
}
protected constructor(context: Context, cancelable: Boolean,
cancelListener: DialogInterface.OnCancelListener?) : super(context, cancelable, cancelListener) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
mCancelable = cancelable
}
override fun setContentView(@LayoutRes layoutResId: Int) {
super.setContentView(wrapInBottomSheet(layoutResId, 0, null, null))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
override fun setContentView(view: View) {
super.setContentView(wrapInBottomSheet(0, 0, view, null))
}
override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {
super.setContentView(wrapInBottomSheet(0, 0, view, params))
}
fun setContentView(view: View?, @LayoutRes bottom: Int) {
super.setContentView(wrapInBottomSheet(0, bottom, view, null))
}
override fun setCancelable(cancelable: Boolean) {
super.setCancelable(cancelable)
if (mCancelable != cancelable) {
mCancelable = cancelable
if (mBehavior != null) {
mBehavior!!.isHideable = cancelable
}
}
}
override fun setCanceledOnTouchOutside(cancel: Boolean) {
super.setCanceledOnTouchOutside(cancel)
if (cancel && !mCancelable) {
mCancelable = true
}
mCanceledOnTouchOutside = cancel
mCanceledOnTouchOutsideSet = true
}
////同setContentView一样,增加一个底部布局的layoutid,
////在ViewTree layout成功后设置 CoordinatorLayout 的marginBottom 即可达到我们想要的效果
private fun wrapInBottomSheet(layoutResId: Int, bottomLayoutId: Int, view: View?, params: ViewGroup.LayoutParams?): View {
var view = view
val parent = View.inflate(context, R.layout.zr_bottom_sheet_dialog_with_bottom, null)
val coordinator = parent.findViewById(R.id.coordinator) as CoordinatorLayout
if (layoutResId != 0 && view == null) {
view = layoutInflater.inflate(layoutResId, coordinator, false)
}
if (bottomLayoutId != 0) {
val bottomView = layoutInflater.inflate(bottomLayoutId, coordinator, false)
val fl = parent.findViewById(R.id.bottom_design_bottom_sheet)
fl.addView(bottomView)
coordinator.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
coordinator.viewTreeObserver.removeOnGlobalLayoutListener(this)
val p2 = coordinator.layoutParams as FrameLayout.LayoutParams
p2.setMargins(0, DeviceUtil.dp2px(70f), 0, bottomView.height)
coordinator.layoutParams = p2
val p1 = fl.layoutParams
p1.height = bottomView.height
fl.layoutParams = p1
}
})
}
val bottomSheet = coordinator.findViewById(R.id.design_bottom_sheet) as FrameLayout
bottomSheet.setOnClickListener { }
mBehavior = BottomSheetBehavior.from(bottomSheet)
mBehavior?.setBottomSheetCallback(mBottomSheetCallback)
mBehavior?.setHideable(mCancelable)
if (params == null) {
bottomSheet.addView(view)
} else {
bottomSheet.addView(view, params)
}
// We treat the CoordinatorLayout as outside the dialog though it is technically inside
coordinator.findViewById(R.id.touch_outside).setOnClickListener {
if (mCancelable && isShowing && shouldWindowCloseOnTouchOutside()) {
cancel()
}
}
return parent
}
private fun shouldWindowCloseOnTouchOutside(): Boolean {
if (!mCanceledOnTouchOutsideSet) {
if (Build.VERSION.SDK_INT < 11) {
mCanceledOnTouchOutside = true
} else {
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.windowCloseOnTouchOutside))
mCanceledOnTouchOutside = a.getBoolean(0, true)
a.recycle()
}
mCanceledOnTouchOutsideSet = true
}
return mCanceledOnTouchOutside
}
private val mBottomSheetCallback: BottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View,
@BottomSheetBehavior.State newState: Int) {
// if (newState == BottomSheetBehavior.STATE_HIDDEN) {
// dismiss();
// }
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
}
companion object {
private fun getThemeResId(context: Context, themeId: Int): Int {
var themeId = themeId
if (themeId == 0) {
// If the provided theme is 0, then retrieve the dialogTheme from our theme
val outValue = TypedValue()
themeId = if (context.theme.resolveAttribute(
R.attr.bottomSheetDialogTheme, outValue, true)) {
outValue.resourceId
} else {
// bottomSheetDialogTheme is not provided; we default to our light theme
R.style.Theme_Design_Light_BottomSheetDialog
}
}
return themeId
}
}
}
此时写完之后的效果是
底部的高度是通过布局计算的,不需要指定高度,但是需要单独传递一个底部固定部分的id,然后动态添加进去
此时修改过后的BottomSheetDialog 还有一些粗陋,再次封装一下即可使用
abstract class TsmBottomSheetDialog : BaseBottomSheetDialog {
constructor(context: Context) :super(context, R.style.bottom_sheet_dilog){
initDialog()
}
private fun initDialog() {
val view = LayoutInflater.from(context).inflate(layoutId, null)
setContentView(view, bottomLayoutId)
behaver = BottomSheetBehavior.from(view.parent as View)
behaver?.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss()
behaver?.state = BottomSheetBehavior.STATE_EXPANDED
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
})
}
/**
* 可以控制菜单状态
*/
protected var behaver: BottomSheetBehavior?=null
/**
* 滑动不可关闭
* @return
*/
fun dragCloseEnable(): ZRBottomSheetDialog {
behaver?.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onStateChanged(view: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) { //判断关闭的时候,则强制设定状态为展开
behaver?.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
override fun onSlide(view: View, v: Float) {}
})
return this
}
override fun show() {
initViews()
super.show()
}
/**
* 是否展开BottomSheetDialog
* @return
*/
fun expendBottomSheet(): ZRBottomSheetDialog {
behaver?.state = BottomSheetBehavior.STATE_EXPANDED
return this
}
protected abstract val layoutId: Int
protected open val bottomLayoutId: Int
protected get() = 0
protected abstract fun initViews()
}
再次封装后使用则会方便很多
封装后使用如下
public class TestBottomSheetDialog extends ZRBottomSheetDialog {
public TestBottomSheetDialog (@NonNull Activity context) {
super(context);
}
@Override
protected int getLayoutId() {
return R.layout.dialog_tsm_bottom_sheet;
}
@Override
protected void initViews() {
RecyclerView recycler_view=findViewById(R.id.recycler_view);
recycler_view.setAdapter(new ZiRoomQuicekAdapter(R.layout.item_simple_test, getList(22)) {
@Override
protected void convert(ZiRoomQuickViewHolder helper, String item) {
helper.setText(R.id.tv_item,item);
}
});
}
private List getList(int count) {
List list = new ArrayList<>();
for (int i = 0; i < count; i++) {
list.add(String.valueOf(i));
}
return list;
}
@Override
protected int getBottomLayoutId() {
return R.layout.botttom_sheet_bottom_view;
}
}
第二篇 BottomSheetDialog 改造(二) 去除中间折叠状态
https://www.jianshu.com/p/d3b2edc27fc4
由于修改这个代码比较多,所以还是分享一个github 的地址吧,这样方便大家使用
https://github.com/tsm1991/TsmBottomSheetDialog