Android 中Material Design 库提供了一种可以从底部弹出对话框的控件,叫做BottomSheetDialog ,与它同一族的还有BottomSheet、BottomSheetDialogFragment ,它们的特点是可以在CoordinatorLayout 中通过设置的BottomSheetBehavior 实现从底部弹出、滑出View 的布局效果。这种效果在应用中使用的非常多,但由于不同开发者对BottomSheetDialog 的理解有点点不同,导致了一个比较麻烦的BottomSheetBehavior 冲突。
使用与基本的逻辑 简单显示BottomSheetDialog 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 private lateinit var dialog : BottomSheetDialog override fun onViewCreated (view: View, savedInstanceState: Bundle?) { super .onViewCreated(view, savedInstanceState) dialog = BottomSheetDialog(requireContext()) dialog.setContentView(R.layout.dialog) binding.buttonFirst.setOnClickListener { if (dialog.isShowing) { return @setOnClickListener } dialog.show() } }
可以看到,我们并没有去设置BottomSheetBehavior ,以及它需要的CoordinatorLayout 父布局,那怎么把它们加载进来的呢?加载代码在setContentView 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public void setContentView (@LayoutRes int layoutResId) { super .setContentView(wrapInBottomSheet(layoutResId, null , null )); } private View wrapInBottomSheet ( int layoutResId, @Nullable View view, @Nullable ViewGroup.LayoutParams params) { ensureContainerAndBehavior(); CoordinatorLayout coordinator = (CoordinatorLayout) container.findViewById(R.id.coordinator); ....... return container; } private FrameLayout ensureContainerAndBehavior () { if (container == null ) { container = (FrameLayout) View.inflate(getContext(), R.layout.design_bottom_sheet_dialog, null ); coordinator = (CoordinatorLayout) container.findViewById(R.id.coordinator); bottomSheet = (FrameLayout) container.findViewById(R.id.design_bottom_sheet); behavior = BottomSheetBehavior.from(bottomSheet); behavior.addBottomSheetCallback(bottomSheetCallback); behavior.setHideable(cancelable); } return container; }
最后调用到ensureContainerAndBehavior ,在这里加载了layout 文件夹的design_bottom_sheet_dialog 布局,R.layout.design_bottom_sheet_dialog 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?xml version="1.0" encoding="utf-8" ?> <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:focusable="false" 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 ,以及通过自定义layout_behavior 属性设置了一个bottom_sheet_behavior ,它的具体值如下:
1 <string name="bottom_sheet_behavior" translatable="false" >com.google.android.material.bottomsheet.BottomSheetBehavior</string>
是BottomSheetBehavior 类的全名,结合ensureContainerAndBehavior 中的加载behavior的代码,我们不难猜出它是通过反射这个类的构造器实例化BottomSheetBehavior 的,具体的代码就不展示了。
BottomSheetBehavior冲突的原因 通过上面的代码分析,可以发现Material 包中设置BottomSheetBehavior 的方式有点奇怪: 在string.xml中配置BottomSheetBehavior的具体类 ,这就给了开发者修改layout_behavior 的余地。我们知道,Android工程中不同module 的res 中的资源文件是可以重名的,最后在编译的时候,依赖方的重名文件会覆盖掉被依赖module 中的重名资源文件,所以有些开发者会直接在主module 的string.xml 中直接复写bottom_sheet_behavior ,达到复写BottomSheetBehavior 实例的目的,更有甚者,直接覆盖了R.layout.design_bottom_sheet_dialog 文件,导致最后打包出来的app,全局的BottomSheetDialog 的都变了。。。 如果是一个SDK这么干我还能忍一忍,在此基础上修修补补还能用用,巧的是咱工程用到的两个SDK都这么干了,大家真的非常喜欢自定义弹出Dialog的样式😂,导致应用直接闪退,原因要么是复写了R.layout.design_bottom_sheet_dialog ,导致其他代码加载到这布局时直接闪退;要么是修改了全局bottom_sheet_behavior 导致View 强转失败闪退。
解决冲突 首先明确一点,这种冲突本质原因是自定义BottomSheetDialog 导致的,自定义的BottomSheetDialog 应该和BottomSheetDialog 是同一级别才对,而不是继承关系,因为一旦自定义BottomSheetDialog 继承了原生BottomSheetDialog ,会导致其他要使用原生BottomSheetDialog 的代码不得不和自定义的属性产生瓜葛,导致无法使用原汁原味的BottomSheetDialog 。道理我们都懂,但有些时候推动别人修改问题比自己修改问题困难太多了,必须在自己实在没招才出此招。
复现这种场景,假设有两个自定义的BottomSheetDialog :
1 2 3 4 5 class A_BottomSheetDialog (context :Context ) : BottomSheetDialog (context )//B SDK class B_BottomSheetDialog (context :Context ) : BottomSheetDialog (context )
它们分别对应的布局是: A SDK中的design_bottom_sheet_dialog.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <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:focusable="false" 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>
B SDK中的design_bottom_sheet_dialog.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <BBBBFrameLayout 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" > <ColorView android:id="@+id/touch_outside" android:layout_width="match_parent" android:layout_height="match_parent" android:focusable="false" 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> </BBBBFrameLayout>
A SDK中bottom_sheet_behavior
1 <string name="bottom_sheet_behavior" translatable="false" >com.a.a.BottomSheetBehavior</string>
B SDK中bottom_sheet_behavior
1 <string name="bottom_sheet_behavior" translatable="false" >com.b.b.BottomSheetBehavior</string>
观察这种情况,可以发现一切冲突的源头实际上在R.layout.design_bottom_sheet_dialog ,一是它可能被SDK覆盖修改,二是layout_behavior 属性也是在这布局中定义的,不管SDK怎么修改string.xml文件中的bottom_sheet_behavior ,我们都可以在R.layout.design_bottom_sheet_dialog 给它重新赋值让它加载其他类。再者,源码中的ensureContainerAndBehavior()都是通过 findViewById 来找View 的,它不在乎View 怎么嵌套的。所以,作为主module ,我们完全可以复写这个布局,以其人之道还治其人之身,让所有的BottomSheetDialog 都加载我们的布局,我们自定义布局MyBottomSheetDialog ,在这个自定义的MyBottomSheetDialog 中想办法让对应SDK的Dialog去加载对应的R.layout.design_bottom_sheet_dialog 。
难点在于,我们怎么在MyBottomSheetDialog 中判断代码调用来自哪个SDK的BottomSheetDialog ?观察实例化BottomSheetDialog 并调用BottomSheetDialog#setContentView 方法的过程,这是创建Dialog的必要过程,那么在Java调用栈中一定存在A_BottomSheetDialog#setContentView 或者B_BottomSheetDialog#setContentView 这样的StackElement ,所以我们可以在实例化MyBottomSheetDialog 中判断调用栈中是不是包含A Or B 这样的字符串,而且自定义View的类名不会被混淆,我们可以放心地以此区分来自哪个SDK,进而加载它们想要的布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class MyBottomSheetDialog (context : Context ) : FrameLayout (context ) { init { val stack = Log.getStackTraceString(Throwable()) when { stack.contains("A_BottomSheetDialog" ) -> { View.inflate(context,R.layout.a_bottom_sheet_dialog,this ) } stack.contains("B_BottomSheetDialog" ) -> { View.inflate(context,R.layout.b_bottom_sheet_dialog,this ) } } } }
主module 的R.layout.design_bottom_sheet_dialog
1 2 3 4 <com.msisuzney.bottomsheetdialog.MyBottomSheetDialog xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" />
其他 这是解决这类冲突的套路方法,利用主module 可复写其他module 资源,利用调用栈来判断是谁调用的,利用不同布局内可以使用相同的全局view id,可以巧妙化解冲突。