美文网首页Android 进阶之旅
Android 进阶学习(二十五) BottomSheetD

Android 进阶学习(二十五) BottomSheetD

作者: Tsm_2020 | 来源:发表于2021-06-09 17:15 被阅读0次

    关于Google在Metrail Design 风格中新推出的dialog,BottomSheetDialog 在现在不少App中都能看到,我就在抖音和网易新闻上看到过,不管是样式还是新颖的交互来说都是不错的,国内设计师习惯将弹窗的最底部增加一个功能性的按键


    image.png

    类似于上面图片这种,弹窗的高度还要根据数据条目的个数去适配,如果使用普通的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 布局文件

    <FrameLayout
       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/container"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:fitsSystemWindows="true">
    
     <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:id="@+id/coordinator"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:fitsSystemWindows="true">
    
       <View
           android:id="@+id/touch_outside"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:importantForAccessibility="no"
           android:soundEffectsEnabled="false"
           tools:ignore="UnusedAttribute"/>
    
       <FrameLayout
           android:id="@+id/design_bottom_sheet"
           style="?attr/bottomSheetStyle"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_gravity="center_horizontal|top"
           app:layout_behavior="@string/bottom_sheet_behavior"/>
    
     </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    </FrameLayout>
    

    CoordinatorLayout 包裹了我们的contentView的容器,也就是design_bottom_sheet 这个FrameLayout,想要给底部增加一个按钮,只需要让CoordinatorLayout 的marginBottom 与 底部的区域高度相等即可,

    修改后的文件如下

    <FrameLayout
       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/container"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:fitsSystemWindows="true">
    
       <androidx.coordinatorlayout.widget.CoordinatorLayout
           android:id="@+id/coordinator"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:fitsSystemWindows="true">
    
           <View
               android:id="@+id/touch_outside"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:importantForAccessibility="no"
               android:soundEffectsEnabled="false"
               tools:ignore="UnusedAttribute"/>
    
           <FrameLayout
               android:id="@+id/design_bottom_sheet"
               style="?attr/bottomSheetStyle"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_gravity="center_horizontal|top"
               app:layout_behavior="@string/bottom_sheet_behavior"/>
    
    
       </androidx.coordinatorlayout.widget.CoordinatorLayout>
       <FrameLayout
           android:id="@+id/bottom_design_bottom_sheet"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_gravity="bottom"/>
    </FrameLayout>
    

    修改后的BottomSheetDialog 如下

    open class BaseBottomSheetDialog : AppCompatDialog {
       private var mBehavior: BottomSheetBehavior<FrameLayout>? = 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<View>(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<FrameLayout>(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<View>(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<View>(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,然后动态添加进去


    GIF 2021-6-9 17-00-38.gif

    此时修改过后的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<View>?=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<String, ZiRoomQuickViewHolder>(R.layout.item_simple_test, getList(22)) {
               @Override
               protected void convert(ZiRoomQuickViewHolder helper, String item) {
                   helper.setText(R.id.tv_item,item);
               }
           });
       }
       private List<String> getList(int count) {
           List<String> 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

    相关文章

      网友评论

        本文标题:Android 进阶学习(二十五) BottomSheetD

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