用更优雅的技术方案实现应用内多弹窗效果!
作者:首席网管地址:https://www.jianshu.com/u/e81db6c18dd0
1.背景
通过观察众多知名app我们可以发现,在app启动进入首页的时候,我们一般会遇到以下几种弹窗:app更新升级提示弹窗、青少年模式切换弹窗、某活动引导弹窗、某新功能引导弹窗、白日\黑夜模式切换弹窗......弹出一个,点击消失,又弹出另一个......针对单个弹窗而言,它既有自身弹出的条件,又有弹出时机的优先级......在开发中面对众多弹窗的时候,我们该如何实现呢?有人说这好办,在DialogA注册onDismissListener编写DialogB弹出的条件、在DialogB注册onDismissListener编写DialogC弹出的条件、以此类推实现DialogD、E、F......伪代码如下:
classDialogAextendDialog
protecd voidonCreate()
//...
if
new
new
classDialogBextendDialog
protecd voidonCreate()
//...
if
new
new
// ......
{
protecd voidonCreate()
{
//...
setOnDismissListener{
if
(条件成立){
new
DialogB().show();
}
else{
new
DialogC().show();
}
}
}
}
classDialogBextendDialog
{
protecd voidonCreate()
{
//...
setOnDismissListener{
if
(条件成立){
new
DialogC().show();
}
else{
new
DialogD().show();
}
}
}
}
// ......
以上案例仅仅是要Dialog能弹出来才能走到setOnDismissListener里的逻辑,那要是连Dialog都没能弹出来,那情况岂不是更揪心???就算最后你能凭借超强的if/else套娃能力勉强实现了,相信我,此时工程代码已然一坨屎了!首页弹出远比想象的复杂!
2.解决方案
我们先看首页弹出的整个业务流程,弹窗是一个接着一个出现的,这非常容易让人联想到这是一条“链”的流程,什么链?责任链嘛!呼之即出!(有对责任链不熟悉的同学我建议先学习《设计模式》,重点参透它的设计思想。)
关于责任链,就不得不提一嘴名世之作----okhttp,它的每一个节点叫做拦截器(Interceptor)。于是乎我们有样学样,将我们的每一个弹窗(Dialog)看作是责任链的一个节点(DialogInterceptor),将所有弹窗组织成一条弹窗链(DialogChain),链头的节点优先级最高,依次递减。话不多说,上代码!
定义DialogInterceptor
interfaceDialogInterceptor
{
fun
intercept(chain: DialogChain)}
定义DialogChain
classDialogChainprivateconstructor
valactivity
private
@JvmStatic
fun create(initialCapacity: Int = 0): Builder
return
@JvmStatic
fun openLog(isOpen:Boolean)
private
// 执行拦截器。
fun process()
// 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
"===> clearAllInterceptors"
private fun clearAllInterceptors()
// 构建者模式。
open class Builder(private val initialCapacity: Int = 0)
private val interceptors by lazy(LazyThreadSafetyMode.NONE)
private
private
// 添加一个拦截器。
fun addInterceptor(interceptor: DialogInterceptor): Builder
if
returnthis
// 关联Fragment。
fun attach(fragment: Fragment): Builder
this
returnthis
// 关联Activity。
fun attach(activity: FragmentActivity): Builder
this
returnthis
fun build(): DialogChain
return
(
// 弹窗的时候可能需要
Activity/
Fragment环境。
valactivity
:
FragmentActivity?
=
null,
val fragment: Fragment? =
null,
private
var interceptors: MutableList<DialogInterceptor>?
) {
companion object {
@JvmStatic
fun create(initialCapacity: Int = 0): Builder
{
return
Builder(initialCapacity)
}
@JvmStatic
fun openLog(isOpen:Boolean)
{
isOpenLog=isOpen
}
}
private
var index: Int =
0// 执行拦截器。
fun process()
{
interceptors ?:
return when (index) {
in interceptors!!.indices -> {
val interceptor = interceptors!![index]
index++
interceptor.intercept(
this)
}
// 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
interceptors!!.size -> {
"===> clearAllInterceptors"
.logI(
this)
clearAllInterceptors()
}
}
}
private fun clearAllInterceptors()
{
interceptors?.clear()
interceptors =
null }
// 构建者模式。
open class Builder(private val initialCapacity: Int = 0)
{
private val interceptors by lazy(LazyThreadSafetyMode.NONE)
{
ArrayList<DialogInterceptor>(
initialCapacity
)
}
private
var activity: FragmentActivity? =
nullprivate
var fragment: Fragment? =
null// 添加一个拦截器。
fun addInterceptor(interceptor: DialogInterceptor): Builder
{
if
(!interceptors.contains(interceptor)) {
interceptors.add(interceptor)
}
returnthis
}
// 关联Fragment。
fun attach(fragment: Fragment): Builder
{
this
.fragment = fragment
returnthis
}
// 关联Activity。
fun attach(activity: FragmentActivity): Builder
{
this
.activity = activity
returnthis
}
fun build(): DialogChain
{
return
DialogChain(activity, fragment, interceptors)
}
}
是的,就两个类,整个解决方案就俩类!下面我们先看一下用例,而后再结合用例梳理一遍框架的逻辑流程。
3.用例
step1:在app主工程新建一个BaseDialog并实现DialogInterceptor接口
abstract class BaseDialog(context: Context):AlertDialog(context),DialogInterceptor
private
/*下一个拦截器*/
fun chain(): DialogChain?
@CallSuper
override fun intercept(chain: DialogChain)
override fun onCreate(savedInstanceState: Bundle?)
super
{
private
var mChain: DialogChain? =
null/*下一个拦截器*/
fun chain(): DialogChain?
= mChain
@CallSuper
override fun intercept(chain: DialogChain)
{
mChain = chain
}
override fun onCreate(savedInstanceState: Bundle?)
{
super
.onCreate(savedInstanceState)
window?.attributes?.width=
800 window?.attributes?.height=
900 }
}
注意看intercept(chain: DialogChain)方法,我们将其传进来的DialogChain对象保存起来,再提供chain()方法供子类访问。
step2:衍生出ADialog
classADialog(context: Context) : BaseDialog(context), View.OnClickListener
override fun onCreate(savedInstanceState: Bundle?)
super
// 注释1:弹窗消失时把请求移交给下一个拦截器。
override fun onClick(p0: View?)
override fun intercept(chain: DialogChain)
super
if
this
{
override fun onCreate(savedInstanceState: Bundle?)
{
super
.onCreate(savedInstanceState)
setContentView(R.layout.dialog_a)
findViewById<View>(R.id.tv_confirm)?.setOnClickListener(
this)
findViewById<View>(R.id.tv_cancel)?.setOnClickListener(
this)
// 注释1:弹窗消失时把请求移交给下一个拦截器。
setOnDismissListener {
chain()?.process()
}
}
override fun onClick(p0: View?)
{
dismiss()
}
override fun intercept(chain: DialogChain)
{
super
.intercept(chain)
val isShow =
true// 注释2:这里可根据实际业务场景来定制dialog 显示条件。if
(isShow) {
this
.show()
}
else {
// 注释3:当自己不具备弹出条件的时候,可以立刻把请求转交给下一个拦截器。 chain.process()
}
}
}
附dialog_a.xml:
<?xml version="1.0" encoding="utf-8"?>
android:layout_width
android:layout_height
xmlns:app
android:layout_width
android:layout_height
android:text
android:textSize
app:layout_constraintTop_toTopOf
app:layout_constraintStart_toStartOf
app:layout_constraintEnd_toEndOf
app:layout_constraintBottom_toBottomOf
android:textColor
android:id
android:layout_width
android:layout_height
android:text
android:textSize
android:gravity
app:layout_constraintStart_toStartOf
app:layout_constraintEnd_toStartOf
app:layout_constraintBottom_toBottomOf
android:textColor
android:id
android:layout_width
android:layout_height
android:background
app:layout_constraintStart_toStartOf
app:layout_constraintEnd_toEndOf
app:layout_constraintBottom_toBottomOf
android:id
android:layout_width
android:layout_height
android:text
android:textSize
android:gravity
app:layout_constraintStart_toEndOf
app:layout_constraintEnd_toEndOf
app:layout_constraintBottom_toBottomOf
android:textColor
</androidx.constraintlayout.widget.ConstraintLayout>
<
androidx.constraintlayout.widget.ConstraintLayoutxmlns:android=
"http://schemas.android.com/apk/res/android"android:layout_width
=
"match_parent"android:layout_height
=
"match_parent"xmlns:app
=
"http://schemas.android.com/apk/res-auto">
<
TextViewandroid:layout_width
=
"wrap_content"android:layout_height
=
"wrap_content"android:text
=
"我是Dialog A"android:textSize
=
"23sp"app:layout_constraintTop_toTopOf
=
"parent"app:layout_constraintStart_toStartOf
=
"parent"app:layout_constraintEnd_toEndOf
=
"parent"app:layout_constraintBottom_toBottomOf
=
"parent"android:textColor
=
"@android:color/black"/>
<
TextViewandroid:id
=
"@+id/tv_cancel"android:layout_width
=
"wrap_content"android:layout_height
=
"40dp"android:text
=
"取消"android:textSize
=
"23sp"android:gravity
=
"center"app:layout_constraintStart_toStartOf
=
"parent"app:layout_constraintEnd_toStartOf
=
"@id/line"app:layout_constraintBottom_toBottomOf
=
"parent"android:textColor
=
"@android:color/holo_orange_dark"/>
<
Viewandroid:id
=
"@+id/line"android:layout_width
=
"1dp"android:layout_height
=
"20dp"android:background
=
"@android:color/darker_gray"app:layout_constraintStart_toStartOf
=
"parent"app:layout_constraintEnd_toEndOf
=
"parent"app:layout_constraintBottom_toBottomOf
=
"parent"/>
<
TextViewandroid:id
=
"@+id/tv_confirm"android:layout_width
=
"wrap_content"android:layout_height
=
"40dp"android:text
=
"确定"android:textSize
=
"23sp"android:gravity
=
"center"app:layout_constraintStart_toEndOf
=
"@id/line"app:layout_constraintEnd_toEndOf
=
"parent"app:layout_constraintBottom_toBottomOf
=
"parent"android:textColor
=
"@android:color/holo_orange_dark"/>
</androidx.constraintlayout.widget.ConstraintLayout>
附dialog_a.xml界面效果图:
我们先看ADialog注释1处,就是在Dialog消失的时候拿到DialogChain对象,调用其process()方法,这里注意关联BaseDialog中的逻辑——我们是利用了intercept(chain: DialogChain)方法传进的DialogChain对象做文章。此外还要注意关联DialogChain process()方法中的逻辑:
funprocess()
when
in
val
// 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
"===> clearAllInterceptors"
{
interceptors ?:
returnwhen
(index) {
in
interceptors!!.indices -> {
val
interceptor = interceptors!![index]
// 注释1 index++
// 注释2 interceptor.intercept(
this)
// 注释3 }
// 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
interceptors!!.size -> {
"===> clearAllInterceptors"
.logI(
this)
clearAllInterceptors()
}
}
}
我们先看注释1处,当DialogChain第一次调用process()时(此时index值为0),注释1拿到的就是interceptors集合中的第一个元素(我们假设ADialog 就是interceptors集合中的第一个元素),经过注释2处之后,index值自增1,再经注释3处将DialogChain对象传进ADialog去,那么此时ADialog中拿到的就是index==1的DialogChain对象,那么在ADialog中任意地方再调用DialogChain process()方法就又拿到interceptors集合中的第二个元素继续做文章,以此类推......
我们继续回到ADialog代码中,对于ADialog ,我们期望的业务逻辑是:
- 当注释2条件为true时才能弹出ADialog,否则就走注释3处的逻辑,把“请求”交给下一个DialogInterceptor处理......
- 假设注释2处条件为true,ADialog成功弹了出来,那么不管是点击了“取消”还是“确定”,我们希望在它消失的时候将“请求”转交给下一个DialogInterceptor处理。
而实际业务场景也是常常如此,ADialog关闭之后再弹出BDialog......
step3:衍生出BDialog
classBDialog
privatevardata
// 注释1:这里注意:intercept(chain: DialogChain)方法与 onDataCallback(data: String)方法被调用的先后顺序是不确定的
funonDataCallback(data: String)
this
overridefunonCreate(savedInstanceState: Bundle?)
super
// 弹窗消失时把请求移交给下一个拦截器。
overridefunonClick(p0: View?)
// 注释2 这里注意:intercept(chain: DialogChain)方法与 onDataCallback(data: String)方法被调用的先后顺序是不确定的
overridefunintercept(chain: DialogChain)
super
privatefuntryToShow()
// 只有同时满足这俩条件才能弹出来。
if
this
(context: Context) : BaseDialog(context), View.OnClickListener {
privatevardata
: String? =
null// 注释1:这里注意:intercept(chain: DialogChain)方法与 onDataCallback(data: String)方法被调用的先后顺序是不确定的
funonDataCallback(data: String)
{
this
.
data =
data tryToShow()
}
overridefunonCreate(savedInstanceState: Bundle?)
{
super
.onCreate(savedInstanceState)
setContentView(R.layout.dialog_b)
findViewById<View>(R.id.tv_confirm)?.setOnClickListener(
this)
findViewById<View>(R.id.tv_cancel)?.setOnClickListener(
this)
// 弹窗消失时把请求移交给下一个拦截器。
setOnDismissListener {
chain()?.process()
}
}
overridefunonClick(p0: View?)
{
dismiss()
}
// 注释2 这里注意:intercept(chain: DialogChain)方法与 onDataCallback(data: String)方法被调用的先后顺序是不确定的
overridefunintercept(chain: DialogChain)
{
super
.intercept(chain)
tryToShow()
}
privatefuntryToShow()
{
// 只有同时满足这俩条件才能弹出来。
if
(
data !=
null && chain() !=
null) {
this
.show()
}
}
}
附dialog_b.xml:
<?xml version="1.0" encoding="utf-8"?>
android:layout_width
android:layout_height
android:background
xmlns:app
android:layout_width
android:layout_height
android:text
android:textSize
app:layout_constraintTop_toTopOf
app:layout_constraintStart_toStartOf
app:layout_constraintEnd_toEndOf
app:layout_constraintBottom_toBottomOf
android:textColor
android:id
android:layout_width
android:layout_height
android:text
android:textSize
android:gravity
app:layout_constraintStart_toStartOf
app:layout_constraintEnd_toStartOf
app:layout_constraintBottom_toBottomOf
android:textColor
android:id
android:layout_width
android:layout_height
android:background
app:layout_constraintStart_toStartOf
app:layout_constraintEnd_toEndOf
app:layout_constraintBottom_toBottomOf
android:id
android:layout_width
android:layout_height
android:text
android:textSize
android:gravity
app:layout_constraintStart_toEndOf
app:layout_constraintEnd_toEndOf
app:layout_constraintBottom_toBottomOf
android:textColor
</androidx.constraintlayout.widget.ConstraintLayout>
<
androidx.constraintlayout.widget.ConstraintLayoutxmlns:android=
"http://schemas.android.com/apk/res/android"android:layout_width
=
"match_parent"android:layout_height
=
"match_parent"android:background
=
"@android:color/darker_gray"xmlns:app
=
"http://schemas.android.com/apk/res-auto">
<
TextViewandroid:layout_width
=
"wrap_content"android:layout_height
=
"wrap_content"android:text
=
"我是Dialog B"android:textSize
=
"23sp"app:layout_constraintTop_toTopOf
=
"parent"app:layout_constraintStart_toStartOf
=
"parent"app:layout_constraintEnd_toEndOf
=
"parent"app:layout_constraintBottom_toBottomOf
=
"parent"android:textColor
=
"@android:color/black"/>
<
TextViewandroid:id
=
"@+id/tv_cancel"android:layout_width
=
"wrap_content"android:layout_height
=
"40dp"android:text
=
"取消"android:textSize
=
"23sp"android:gravity
=
"center"app:layout_constraintStart_toStartOf
=
"parent"app:layout_constraintEnd_toStartOf
=
"@id/line"app:layout_constraintBottom_toBottomOf
=
"parent"android:textColor
=
"@android:color/holo_orange_dark"/>
<
Viewandroid:id
=
"@+id/line"android:layout_width
=
"1dp"android:layout_height
=
"20dp"android:background
=
"@android:color/darker_gray"app:layout_constraintStart_toStartOf
=
"parent"app:layout_constraintEnd_toEndOf
=
"parent"app:layout_constraintBottom_toBottomOf
=
"parent"/>
<
TextViewandroid:id
=
"@+id/tv_confirm"android:layout_width
=
"wrap_content"android:layout_height
=
"40dp"android:text
=
"确定"android:textSize
=
"23sp"android:gravity
=
"center"app:layout_constraintStart_toEndOf
=
"@id/line"app:layout_constraintEnd_toEndOf
=
"parent"app:layout_constraintBottom_toBottomOf
=
"parent"android:textColor
=
"@android:color/holo_orange_dark"/>
</androidx.constraintlayout.widget.ConstraintLayout>
附效果图:
对于BDialog的业务场景就比较复杂一点,当弹窗请求到达的时候(即 intercept(chain: DialogChain) 被调用),可能由于网络数据没回来或者其他一些异步原因导致自己不能立刻弹出来,而是需要“等一会儿”才能弹出来,又或者网络数据已经回来,但弹窗请求又没达到(即intercept(chain: DialogChain) 尚未被调用)。
总而言之就是注释2处和注释2处被调用的顺序是不确定的,但可以确定的是,假设注释1先被调用,则data字段必不为null,假设注释2先被调用,则chain()也必不为null,这时候就需要这两处都要触发一次tryToShow()方法,从而完成弹窗。
从这可以看到,我们设计的框架就很好的满足了我们的需求,代码也很优雅,很内聚!
step 4:衍生出CDialog作为链的最后一个节点
classCDialog
overridefunonCreate(savedInstanceState: Bundle?)
super
// 弹窗消失时把请求移交给下一个拦截器。
overridefunonClick(p0: View?)
overridefunintercept(chain: DialogChain)
super
val
if
this
(context: Context) : BaseDialog(context), View.OnClickListener {
overridefunonCreate(savedInstanceState: Bundle?)
{
super
.onCreate(savedInstanceState)
setContentView(R.layout.dialog_c)
findViewById<View>(R.id.tv_confirm)?.setOnClickListener(
this)
findViewById<View>(R.id.tv_cancel)?.setOnClickListener(
this)
// 弹窗消失时把请求移交给下一个拦截器。
setOnDismissListener {
chain()?.process()
}
}
overridefunonClick(p0: View?)
{
dismiss()
}
overridefunintercept(chain: DialogChain)
{
super
.intercept(chain)
val
isShow =
true// 这里可根据实际业务场景来定制dialog 显示条件。if
(isShow) {
this
.show()
}
else {
// 当自己不具备弹出条件的时候,可以立刻把请求转交给下一个拦截器。 chain.process()
}
}
}
附效果图:
对于CDialog就没啥复杂业务场景了,如同ADialog。不过值得一提的是,由于CDialog作为DialogChain的最后一个节点,那么当它调用chain()?.process() 方法时,将走到如下代码注释4处的逻辑:
funprocess()
when
in
val
// 注释4 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
"===> clearAllInterceptors"
{
interceptors ?:
returnwhen
(index) {
in
interceptors!!.indices -> {
val
interceptor = interceptors!![index]
// 注释1 index++
// 注释2 interceptor.intercept(
this)
// 注释3 }
// 注释4 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
interceptors!!.size -> {
"===> clearAllInterceptors"
.logI(
this)
clearAllInterceptors()
}
}
}
附clearAllInterceptors()方法代码:
privatefunclearAllInterceptors()
{
interceptors?.clear()
interceptors =
null }
很显然,当最后一个Dialog关闭时,我们释放了整条链的内存。
step5:在MainActivity里最终完成用法示例
classMainActivity : AppCompatActivity
privatelateinitvar
privateval
overridefunonCreate(savedInstanceState: Bundle?)
super
// 模拟延迟数据回调。
//创建 DialogChain
privatefuncreateDialogChain()
overridefunonStart()
super
// 开始从链头弹窗。
() {
privatelateinitvar
dialogChain: DialogChain
privateval
bDialog
by lazy { BDialog(
this) }
overridefunonCreate(savedInstanceState: Bundle?)
{
super
.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DialogChain.openLog(
true)
createDialogChain()
//创建 DialogChain// 模拟延迟数据回调。
Handler().postDelayed({
bDialog.onDataCallback(
"延迟数据回来了!!")
},
10000)
}
//创建 DialogChain
privatefuncreateDialogChain()
{
dialogChain = DialogChain.create(
3)
.attach(
this)
.addInterceptor(ADialog(
this))
.addInterceptor(bDialog)
.addInterceptor(CDialog(
this))
.build()
}
overridefunonStart()
{
super
.onStart()
// 开始从链头弹窗。
dialogChain.process()
}
}
点击运行,如图:
gitee地址如下所示:
https://gitee.com/cen-shengde/dialog-chain
---END---
最新评论
推荐文章
作者最新文章
你可能感兴趣的文章
Copyright Disclaimer: The copyright of contents (including texts, images, videos and audios) posted above belong to the User who shared or the third-party website which the User shared from. If you found your copyright have been infringed, please send a DMCA takedown notice to [email protected]. For more detail of the source, please click on the button "Read Original Post" below. For other communications, please send to [email protected].
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。