Android 進(jìn)階學(xué)習(xí)(二十五) BottomSheetDialog 改造,添加底部固定區(qū)域

關(guān)于Google在Metrail Design 風(fēng)格中新推出的dialog,BottomSheetDialog 在現(xiàn)在不少App中都能看到,我就在抖音和網(wǎng)易新聞上看到過(guò),不管是樣式還是新穎的交互來(lái)說(shuō)都是不錯(cuò)的,國(guó)內(nèi)設(shè)計(jì)師習(xí)慣將彈窗的最底部增加一個(gè)功能性的按鍵


image.png

類(lèi)似于上面圖片這種,彈窗的高度還要根據(jù)數(shù)據(jù)條目的個(gè)數(shù)去適配,如果使用普通的dialog 即沒(méi)有好的交互體驗(yàn),彈出的高度也需要我們自己計(jì)算,這無(wú)疑是增加了開(kāi)發(fā)難度,但是如果使用BottomSheetDialog ,底部去支付的按鈕始終是在數(shù)據(jù)的下方,如果只有兩三條數(shù)據(jù)還好,如果數(shù)據(jù)過(guò)多則需要將列表滑動(dòng)到最下方才能看到功能鍵,這無(wú)疑是一個(gè)非常糟糕的交互,今天我們就通過(guò)查看BottomSheetDialog 來(lái)修改他,讓他可以存放一個(gè)始終放下最下方的區(qū)域,先來(lái)看一下他的源碼

public class BottomSheetDialog extends AppCompatDialog {
   ....省略部分代碼

   @Override
   public void setContentView(@LayoutRes int layoutResId) {
       super.setContentView(wrapInBottomSheet(layoutResId, null, null));
   }
   /**
    *  我們知道要實(shí)現(xiàn)BottomSheetDialog 離不開(kāi)BottomSheetBehavior 的支持,
    *這里將 contentView 添加到 一個(gè)擁有 BottomSheetBehavior 的布局當(dāng)中
    **/
   private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
       final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
               R.layout.design_bottom_sheet_dialog, null);//事先加載一個(gè)擁有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;
   }
   ....省略部分代碼
}

其實(shí)從上面這段代碼根本看不出來(lái)什么,只是知道contentView被放入的一個(gè)擁有BottomSheetBehavior 的容器總,想要在原有的基礎(chǔ)上增加一個(gè)可拓展的底部功能區(qū)域,就需要修改原始的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 這個(gè)FrameLayout,想要給底部增加一個(gè)按鈕,只需要讓CoordinatorLayout 的marginBottom 與 底部的區(qū)域高度相等即可,

修改后的文件如下

<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一樣,增加一個(gè)底部布局的layoutid,  
////在ViewTree layout成功后設(shè)置 CoordinatorLayout 的marginBottom 即可達(dá)到我們想要的效果
   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
       }
   }
}

此時(shí)寫(xiě)完之后的效果是
底部的高度是通過(guò)布局計(jì)算的,不需要指定高度,但是需要單獨(dú)傳遞一個(gè)底部固定部分的id,然后動(dòng)態(tài)添加進(jìn)去


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

此時(shí)修改過(guò)后的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) {}
       })
   }


   /**
    * 可以控制菜單狀態(tài)
    */
   protected var behaver: BottomSheetBehavior<View>?=null

   /**
    * 滑動(dòng)不可關(guān)閉
    * @return
    */
   fun dragCloseEnable(): ZRBottomSheetDialog {
       behaver?.setBottomSheetCallback(object : BottomSheetCallback() {
           override fun onStateChanged(view: View, newState: Int) {
               if (newState == BottomSheetBehavior.STATE_HIDDEN) { //判斷關(guān)閉的時(shí)候,則強(qiáng)制設(shè)定狀態(tài)為展開(kāi)
                   behaver?.state = BottomSheetBehavior.STATE_COLLAPSED
               }
           }

           override fun onSlide(view: View, v: Float) {}
       })
       return this
   }

   override fun show() {
       initViews()
       super.show()
   }

   /**
    * 是否展開(kāi)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()
}

再次封裝后使用則會(huì)方便很多
封裝后使用如下

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 改造(二) 去除中間折疊狀態(tài)
http://www.itdecent.cn/p/d3b2edc27fc4
由于修改這個(gè)代碼比較多,所以還是分享一個(gè)github 的地址吧,這樣方便大家使用
https://github.com/tsm1991/TsmBottomSheetDialog

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容