用更优雅的技术方案实现应用内多弹窗效果!

发表于 2年以前  | 总阅读数:330 次

1.背景

通过观察众多知名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套娃能力勉强实现了,相信我,此时工程代码已然一坨屎了!首页弹出远比想象的复杂!

2.解决方案

我们先看首页弹出的整个业务流程,弹窗是一个接着一个出现的,这非常容易让人联想到这是一条“链”的流程,什么链?责任链嘛!呼之即出!(有对责任链不熟悉的同学我建议先学习《设计模式》,重点参透它的设计思想。)

关于责任链,就不得不提一嘴名世之作----okhttp,它的每一个节点叫做拦截器(Interceptor)。于是乎我们有样学样,将我们的每一个弹窗(Dialog)看作是责任链的一个节点(DialogInterceptor),将所有弹窗组织成一条弹窗链(DialogChain),链头的节点优先级最高,依次递减。话不多说,上代码!

定义DialogInterceptor

interface DialogInterceptor {

    fun intercept(chain: DialogChain)

}

定义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)
        }
    }

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

3.用例

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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/LlKUtPFLujHVaxlBnSMGuw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录