1 底部可拖动Dialog
1.1 效果展示
1.2 实现细节
说在前面,我们使用了开源库去实现,感谢作者Kongzue。
如上图,我们可直接使用BottomDialog,根据UI效果做一定改造即可,实现细节参考如下:
1)自定义Dialog布局:dialog_bottom_subscribe_notification.xml。
2)调用BottomDialog.show(),并设置背景透明(去掉默认白色背景),以及设置压黑遮罩(默认压黑遮罩颜色太浅)。
BottomDialog.show(OnBindView).setBackgroundColorRes(R.color.color_00000000).setMaskColor(R.color.color_99000000)
3)将自定义布局Bind到Dialog,去掉BottomDialog顶部滑动提示条,实现自己的View逻辑。
OnBindView = object : OnBindView<BottomDialog>(R.layout.dialog_bottom_subscribe_notification) {
override fun onBind(dialog: BottomDialog, v: View) {
// 去掉默认滑动提示条
if (dialog.dialogImpl.imgTab != null) {
(dialog.dialogImpl.imgTab.parent as ViewGroup).removeView(dialog.dialogImpl.imgTab)
}
// 实现自己的View逻辑...
}
}
4)可选:将View逻辑处理点击等回调,抛给调用者处理。
下面我们拿视频中的Switch举例:
a、回调声明:之前采用CallBack方式实现,在Kotlin中可采用高阶函数作为方法参数的方式实现。
fun showSubscribe(isChecked: Boolean, selectCallback: (isSelect: Boolean, switch: SwitchCompat) -> Unit)
b、回调抛给调用者处理。
v.findViewById<SwitchCompat>(R.id.notificationSwitch).setOnCheckedChangeListener { _, isChecked ->
selectCallback(isChecked, notificationSwitch)
}
c、调用者处理回调。
showSubscribe(isChecked) { isSelect: Boolean, switch: SwitchCompat ->
if (isSelect) {
switch.isChecked = false
// ...
} else {
// ...
}
}
1.3 代码参考
/**
* Description: Notifications Subscribe的弹窗
* CreateDate: 2022/6/10 14:55
* Author: agg
*/
object SubscribeNotificationDialog {
/**
* Notifications Subscribe的弹窗
*/
@JvmStatic
fun showSubscribe(
name: String,
isChecked: Boolean,
selectCallback: (isSelect: Boolean, switch: SwitchCompat) -> Unit
) {
BottomDialog.show(object :
OnBindView<BottomDialog>(R.layout.dialog_bottom_subscribe_notification) {
override fun onBind(dialog: BottomDialog, v: View) {
// 去掉默认滑动提示条
if (dialog.dialogImpl.imgTab != null) {
(dialog.dialogImpl.imgTab.parent as ViewGroup).removeView(dialog.dialogImpl.imgTab)
}
val notificationDesc = v.findViewById<CustomStrokeTextView>(R.id.notificationDesc)
notificationDesc.text = String.format(
ApplicationUtils.getApplication().getString(R.string.a_notification_desc), name
)
val notificationSwitch = v.findViewById<SwitchCompat>(R.id.notificationSwitch)
notificationSwitch.isChecked = isChecked
notificationSwitch.setOnCheckedChangeListener { _, isChecked ->
selectCallback(isChecked, notificationSwitch)
}
}
}).setBackgroundColorRes(R.color.color_transparent)
.setMaskColor(ApplicationUtils.getApplication().resources.getColor(R.color.color_99000000))
}
}
2 居中Dialog
2.1 效果展示
2.2 实现细节
居中Dialog使用CustomDialog即可,实现细节参考如下:
1)自定义Dialog布局:layout_airdrop_dialog.xml。
2)调用CustomDialog.show(),并设置压黑遮罩,以及设置全屏显示。
CustomDialog.build().setCustomView(OnBindView).setMaskColor(R.color.color_99000000).setCancelable(true).setFullScreen(true)
3)将自定义布局Bind到Dialog,并实现自己的View逻辑。
OnBindView = object : OnBindView<CustomDialog>(R.layout.layout_airdrop_dialog) {
override fun onBind(dialog: CustomDialog, v: View) {
binding.parent.setOnCustomClickListener {
dialog.dismiss()
}
binding.bg.setOnCustomClickListener {
}
// 实现自己的View逻辑...
}
}
4)可选:居中布局,需要考虑Android碎片化问题,尽可能使用比例去动态布局,减少使用具体的dp、px或sp值。
a、比如,布局文件的ImageView-bg控件,使用layout_constraintDimensionRatio比例属性:
<ImageView
android:id="@+id/bg"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="36dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="303:339"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
b、再比如,ConstraintLayout的另一个属性,layout_constraintWidth_percent可以限定控件所占父控件的尺寸比例:
2.3 代码参考
/**
* Description:
* CreateDate: 2022/8/30 11:06
* Author: agg
*/
object AirdropRewardsDialog {
fun show(
airdropInfo: AirdropInfo,
isFromPopup: Boolean = false,
callback: () -> Unit
) {
CustomDialog.build().setCustomView(object :
OnBindView<CustomDialog>(R.layout.layout_airdrop_dialog) {
override fun onBind(dialog: CustomDialog, v: View) {
binding.parent.setOnCustomClickListener {
dialog.dismiss()
}
binding.bg.setOnCustomClickListener {
}
// 实现自己的View逻辑...
}
}).setMaskColor(ColorUtils.getColor(R.color.color_99000000))
.setCancelable(true)
.setFullScreen(true)
.show()
}
}
3 Dialog闲谈
3.1 内存泄漏与Dialog
从步骤(1)-(5),我们可以看到DialogX内部会调用application.registerActivityLifecycleCallbacks,在activity销毁时会清除Dialog,并消除对它的引用。
由此可见,开源者已考虑过此处可能引起的内存泄漏。
3.2 Context与Dialog
我们可以看到,show一个Dialog可以传参也可不传,这两种方式有什么区别呢?
3.2.1 先来看看带Activity参数的show方法
-
由于初始化并未设置dialogImplMode,所以此处会走到default分支;
-
然后通过Activity拿到getDecorView,并把Dialog的view通过addView(view)添加到根布局中。
3.2.2 再来看看不带参数的show方法
可以看到Dialog的view通过addView(view)被添加到rootFrameLayout布局中。那么rootFrameLayout是什么布局呢?这就得从源头一步一步展开看看。
1)show方法中调用父类beforeShow方法,在beforeShow获取topActivity
public void show() {
// 调用父类beforeShow方法
super.beforeShow();
if (getDialogView() == null) {
dialogView = createView(R.layout.layout_dialogx_custom);
dialogImpl = new DialogImpl(dialogView);
if (dialogView != null) dialogView.setTag(me);
}
show(dialogView);
}
protected void beforeShow() {
dismissAnimFlag = false;
// 获取topActivity
if (getTopActivity() == null) {
init(null);
if (getTopActivity() == null) {
error("DialogX 未初始化。\n请检查是否在启动对话框前进行初始化操作,使用以下代码进行初始化:\nDialogX.init(context);\n\n另外建议您前往查看 DialogX 的文档进行使用:https://github.com/kongzue/DialogX");
return;
}
}
// ...
}
2)getTopActivity方法中,activityWeakReference为null,会去调用init(null)方法
public static Activity getTopActivity() {
if (activityWeakReference == null) {
// 调用init(null)方法
init(null);
if (activityWeakReference == null) {
return ActivityLifecycleImpl.getTopActivity();
}
return activityWeakReference.get();
}
return activityWeakReference.get();
}
3)init(null)方法会调用ActivityLifecycleImpl.getTopActivity()方法,并调用initActivityContext方法
public static void init(Context context) {
if (context == null) {
// 调用ActivityLifecycleImpl.getTopActivity()方法
context = ActivityLifecycleImpl.getTopActivity();
}
if (context instanceof Activity) {
// 调用initActivityContext方法
initActivityContext((Activity) context);
}
ActivityLifecycleImpl.init(context, new ActivityLifecycleImpl.onActivityResumeCallBack() {
@Override
public void getActivity(Activity activity) {
initActivityContext(activity);
}
});
}
4)initActivityContext方法,将当前Activity的decorView赋值给rootFrameLayout变量
private static void initActivityContext(Activity activity) {
try {
uiThread = Looper.getMainLooper().getThread();
activityWeakReference = new WeakReference<>(activity);
// 将当前Activity的decorView赋值给rootFrameLayout变量
rootFrameLayout = new WeakReference<>((FrameLayout) activity.getWindow().getDecorView());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
publicWindowInsets(rootFrameLayout.get().getRootWindowInsets());
}
} catch (Exception e) {
e.printStackTrace();
error("DialogX.init: 初始化异常,找不到Activity的根布局");
}
}
从上面源码中,我们知道了rootFrameLayout其实就是,应用当前顶部Activity的decorView。
再回过头来看看不带参数的show方法,一切就明了了,当不传Activity时,就会拿当前当前顶部Activity作为dialog的根布局。
3.2.3 小结
-
调用带Activity参数的show方法时,Dialog布局依赖于传入Activity,布局将添加进此Activity中;
-
调用不带Activity参数的show方法时,Dialog布局依赖于应用当前顶部Activity,布局添加进应用当前顶部Activity中。
3.3 后台弹Dialog
基于3.2的小结,我们知道:
-
如果Dialog不希望在其他界面弹出,则需要指定Activity;
-
如果Dialog不依赖于某个具体页面,可不传Activity,在任何界面都可弹出。
简单来说,当Dialog不传Activity时,此Dialog可依赖于应用当前顶部Activity而弹出,从而实现后台触发任意界面弹Dialog。