首页弹窗优先级编排优雅解决方案

252 篇文章 4 订阅

一、背景

通过观察众多知名app我们可以发现,在app启动进入首页的时候,我们一般会遇到以下几种弹窗:app更新升级提示弹窗、青少年模式切换弹窗、某活动引导弹窗、某新功能引导弹窗、白日\黑夜模式切换弹窗......弹出一个,点击消失,又弹出另一个......针对单个弹窗而言,它既有自身弹出的条件,又有弹出时机的优先级......在开发中面对众多弹窗的时候,我们该如何实现呢?有人说这好办,在DialogA注册onDismissListener编写DialogB弹出的条件、在DialogB注册onDismissListener编写DialogC弹出的条件、以此类推实现DialogD、E、F......伪代码如下:

class DialogA extend Dialog{
protecd void onCreate(){
//...
setOnDismissListener{
   if(条件成立){
     new DialogB().show();
    }else{
     new DialogC().show();
   }
   }
    }
}


class DialogB extend Dialog{
protecd void onCreate(){
//...
setOnDismissListener{
   if(条件成立){
     new DialogC().show();
    }else{
     new DialogD().show();
   }
   }
    }
}
// ......

以上案例仅仅是要Dialog能弹出来才能走到 setOnDismissListener 里的逻辑,那要是连Dialog都没能弹出来,那情况岂不是更揪心???就算最后你能凭借超强的if/else套娃能力勉强实现了,相信我,此时工程代码已然一坨屎了!首页弹出远比想象的复杂!

二、话不多说,上方案!

我们先看首页弹出的整个业务流程,弹窗是一个接着一个出现的,这非常容易让人联想到这是一条“链”的流程,什么链?责任链嘛!呼之即出!(有对责任链不熟悉的同学我建议先学习《设计模式》,重点参透它的设计思想。)
关于责任链,就不得不提一嘴名世之作----okhttp,它的每一个节点叫做拦截器(Interceptor)。于是乎我们有样学样,将我们的每一个弹窗(Dialog)看作是责任链的一个节点(DialogInterceptor),将所有弹窗组织成一条弹窗链(DialogChain),链头的节点优先级最高,依次递减。话不多说,上代码!
1.定义DialogInterceptor:


interface DialogInterceptor {

    fun intercept(chain: DialogChain)

}

2.定义DialogChain:


class DialogChain private constructor(
    // 弹窗的时候可能需要Activity/Fragment环境。
    val activity: 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? = null
        private var fragment: Fragment? = null

       // 添加一个拦截器。
        fun addInterceptor(interceptor: DialogInterceptor): Builder {
            if (!interceptors.contains(interceptor)) {
                interceptors.add(interceptor)
            }
            return this
        }
       // 关联Fragment。
        fun attach(fragment: Fragment): Builder {
            this.fragment = fragment
            return this
        }
       // 关联Activity。
        fun attach(activity: FragmentActivity): Builder {
            this.activity = activity
            return this
        }


        fun build(): DialogChain {
            return DialogChain(activity, fragment, interceptors)
        }
    }


是的,就两个类,整个解决方案就俩类!下面我们先看一下用例,而后再结合用例梳理一遍框架的逻辑流程。

三、用例

step1:在app主工程新建一个BaseDialog并实现DialogInterceptor接口:

abstract class BaseDialog(context: Context):AlertDialog(context),DialogInterceptor {
  
    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:


class ADialog(context: Context) : BaseDialog(context), View.OnClickListener {

    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"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns: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">


    <TextView
        android: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"/>

    <TextView
        android: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"/>
    <View
        android: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"/>
    <TextView
        android: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()方法中的逻辑:

 fun process() {
        interceptors ?: return
        when (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 ,我们期望的业务逻辑是:
1.当注释2条件为true时才能弹出ADialog ,否则就走注释3处的逻辑,把“请求”交给下一个 DialogInterceptor 处理......
2.假设注释2处条件为true,ADialog 成功弹了出来,那么不管是点击了“取消”还是“确定”,我们希望在它消失的时候将“请求”转交给下一个DialogInterceptor 处理。

而实际业务场景也是常常如此,ADialog 关闭之后再弹出BDialog......

step3 衍生出 BDialog:


class BDialog(context: Context) : BaseDialog(context), View.OnClickListener {
    private var data: String? = null

    //  注释1:这里注意:intercept(chain: DialogChain)方法与 onDataCallback(data: String)方法被调用的先后顺序是不确定的
    fun onDataCallback(data: String) {
        this.data = data
        tryToShow()
    }

    override fun onCreate(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()
        }
    }

    override fun onClick(p0: View?) {
        dismiss()
    }
   // 注释2 这里注意:intercept(chain: DialogChain)方法与 onDataCallback(data: String)方法被调用的先后顺序是不确定的
    override fun intercept(chain: DialogChain) {
        super.intercept(chain)
        tryToShow()
    }

    private fun tryToShow() {
        // 只有同时满足这俩条件才能弹出来。
        if (data != null && chain() != null) {
            this.show()
        }
    }
}

附 dialog_b.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns: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">


    <TextView
        android: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"/>

    <TextView
        android: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"/>
    <View
        android: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"/>
    <TextView
        android: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 作为 链的最后一个节点:

class CDialog(context: Context) : BaseDialog(context), View.OnClickListener {


    override fun onCreate(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()
        }
    }

    override fun onClick(p0: View?) {
        dismiss()
    }

    override fun intercept(chain: DialogChain) {
        super.intercept(chain)
        val isShow = true // 这里可根据实际业务场景来定制dialog 显示条件。
        if (isShow) {
            this.show()
        } else { // 当自己不具备弹出条件的时候,可以立刻把请求转交给下一个拦截器。
            chain.process()
        }
    }

}

附近效果图:

对于CDialog 就没啥复杂业务场景了,如同ADialog。不过值得一提的是,由于CDialog 作为 DialogChain 的最后一个节点,那么当它调用 chain()?.process() 方法时,将走到如下代码注释4处的逻辑:

 fun process() {
        interceptors ?: return
        when (index) {
            in interceptors!!.indices -> {

                val interceptor = interceptors!![index] // 注释1
                index++ // 注释2 
                interceptor.intercept(this) // 注释3
            }
            // 注释4 最后一个弹窗关闭的时候,我们希望释放所有弹窗引用。
            interceptors!!.size -> {
                "===> clearAllInterceptors".logI(this)
                clearAllInterceptors()
            }
        }
    }

** 附 clearAllInterceptors() 方法代码:**

 private fun clearAllInterceptors() {
        interceptors?.clear()
        interceptors = null
    }

很显然,当最后一个Dialog关闭时,我们释放了整条链的内存。

step5 在MainActivity 里最终完成用法示例:

class MainActivity : AppCompatActivity() {

    private lateinit var dialogChain: DialogChain

    private val bDialog by lazy { BDialog(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        DialogChain.openLog(true)
        createDialogChain() //创建 DialogChain
        // 模拟延迟数据回调。
        Handler().postDelayed({
            bDialog.onDataCallback("延迟数据回来了!!")
        },10000)
    }

   //创建 DialogChain
    private fun createDialogChain() {
        dialogChain = DialogChain.create(3)
            .attach(this)
            .addInterceptor(ADialog(this))
            .addInterceptor(bDialog)
            .addInterceptor(CDialog(this))
            .build()

    }

    override fun onStart() {
        super.onStart()
        // 开始从链头弹窗。
        dialogChain.process()
    }
}

点击运行之,如图:

附 gitee 地址:https://gitee.com/cen-shengde/dialog-chain 

 

作者:首席网管
链接:https://www.jianshu.com/p/42851f1dc050
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

根据需求,以下是关于手环系统弹窗优先级的测试用例: 1. 测试用例名称:来电弹窗优先级 描述:验证来电弹窗在手环系统中的优先级是否正确 步骤: 1. 模拟来电事件 2. 检查手环系统是否正确显示来电弹窗 预期结果:来电弹窗应具有较高的优先级,能够及时显示并提醒用户 2. 测试用例名称:闹钟弹窗优先级 描述:验证闹钟弹窗在手环系统中的优先级是否正确 步骤: 1. 设置一个闹钟并等待触发 2. 检查手环系统是否正确显示闹钟弹窗 预期结果:闹钟弹窗应具有较高的优先级,能够及时显示并提醒用户 3. 测试用例名称:日程(事件)提醒弹窗优先级 描述:验证日程(事件)提醒弹窗在手环系统中的优先级是否正确 步骤: 1. 设置一个日程事件并等待触发 2. 检查手环系统是否正确显示日程(事件)提醒弹窗 预期结果:日程(事件)提醒弹窗应具有较高的优先级,能够及时显示并提醒用户 4. 测试用例名称:查找我的手表弹窗优先级 描述:验证查找我的手表弹窗在手环系统中的优先级是否正确 步骤: 1. 触发查找我的手表功能 2. 检查手环系统是否正确显示查找我的手表弹窗 预期结果:查找我的手表弹窗应具有较高的优先级,能够立即显示并引起用户的注意 5. 测试用例名称:定时器弹窗优先级 描述:验证定时器弹窗在手环系统中的优先级是否正确 步骤: 1. 设置一个定时器并等待触发 2. 检查手环系统是否正确显示定时器弹窗 预期结果:定时器弹窗应具有较高的优先级,能够及时显示并提醒用户 6. 测试用例名称:跌倒检测弹窗优先级 描述:验证跌倒检测弹窗在手环系统中的优先级是否正确 步骤: 1. 模拟跌倒事件 2. 检查手环系统是否正确显示跌倒检测弹窗 预期结果:跌倒检测弹窗应具有较高的优先级,能够及时显示并提醒用户 7. 测试用例名称:其他系统弹窗优先级 描述:验证其他系统弹窗(如系统更新、低电量等)在手环系统中的优先级是否正确 步骤: 1. 触发其他系统弹窗事件(如系统更新、低电量等) 2. 检查手环系统是否正确显示相应的弹窗 预期结果:其他系统弹窗应具有适当的优先级,能够及时显示并提醒用户 以上是关于手环系统弹窗优先级的测试用例,根据需要可以进一步细化和补充。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值