开发需求记录:实现app任意界面弹框与app置于后台时通知

前言

在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户点击通知时候,跳转到报警详情界面。 功能大体总结如上,在实现弹框与通知在跳转界面时遇到一些问题,在此记录一下。效果图如下:

功能分析

弹框实现,使用DialogFragment。

前后台判断则是,创建一个继承自ActivityLifecycleCallbacks接口和Application的类,继承ActivityLifecycleCallbacks接口是为了前后台判断,继承Application则是方便在基类BaseActivity获取前后台相关数据。

项目原本采用单Activity多Fragment实现,后面因为添加了视频相关功能,改为了多Activity多Fragment。

原单Activity时候,实现比较容易。后面修改为多Activity,就有些头疼,最终用思路是创建基类BaseActivity,后面添加Activity时都要继承基类BaseActivity。使用基类原因是把相同的功能抽取出来,且若每个Activity都自己实现弹框和通知的话太容易出错,也太容易漏下代码了。

代码实现

弹框

在实现继承自DialogFragment的弹框时,需要在onCreateDialog方法内设置dialog的宽高模式以及背景,不然弹框会有默认的边距,导致显示效果与预期不符,未去边距与去掉边距的弹框效果如下: 关于onCreateDialog的代码如下:

kotlin 复制代码
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val dialog = Dialog(requireContext())
    dialog.setContentView(R.layout.custom_dialog_layout)
    dialog.window?.apply {
        setLayout(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)
        setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
    }
    dialog.setCancelable(false)
    return dialog
}

此外当弹框出现的时候,弹框背景色还会闪烁。这里采用属性值动画设置弹框背景色控件的透明度变换。完整的Dialog代码如下:

kotlin 复制代码
class AlarmDialogFragment: DialogFragment() {
    private lateinit var binding:CustomDialogLayoutBinding
    private var animator:ObjectAnimator? = null

    override fun show(manager: FragmentManager, tag: String?) {
        try {
            super.show(manager, tag)
        }catch (e:Exception){
            e.printStackTrace()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = CustomDialogLayoutBinding.inflate(inflater)
        return binding.root
    }

    override fun onStart() {
        super.onStart()
        binding.viewAlarmDialogBg
        startAnimation()
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = Dialog(requireContext())
        dialog.setContentView(R.layout.custom_dialog_layout)
        dialog.window?.apply {
            setLayout(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)
            setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
        }
        dialog.setCancelable(false)
        return dialog
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
    }

    override fun onDestroy() {
        super.onDestroy()
        if(animator?.isStarted == true){
            animator?.end()
        }
    }

    private fun initView() {
        binding.btnCloseDialog.setOnClickListener {
            dismiss()
        }

        binding.btnDialogNav.setOnClickListener {
            if(context is MainActivity){
                val bundle = Bundle()
                bundle.putString("alarmId","1")
                findNavController().navigate(R.id.alarmDetailFragment,bundle)
            }else{
                val intent = Intent(context,MainActivity::class.java)
                intent.putExtra("task","toAlarmDetail")
                startActivity(intent)
            }
            dismiss()
        }
    }

    private fun startAnimation() {
        animator = ObjectAnimator.ofFloat(binding.viewAlarmDialogBg, "alpha", 0f, 0.6f, 0f, 0.6f, 0f)
        animator?.duration = 1200
        animator?.interpolator = AccelerateInterpolator()
        animator?.start()
    }
}

需要注意地方是,由于弹框还负责跳转,而跳转有两种情况,一种是在ActivityA内,fragmentA与fragmentB间的跳转,这种情况使用findNavController().navigate()方法进行跳转,另一种是ActivityB到另一个ActivityA内的指定FragmentB界面。这种采用startActivity(intent)方式跳转,并且在ActivityA的onStart()的方法使用下面方法。

kotlin 复制代码
/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
    val task = intent.getStringExtra("task")
    if (task == "toAlarmDetail"){
        val bundle = Bundle()
        bundle.putString("alarmId","1")
        findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
    }
}

这样从ActivityB到另一个ActivityA时候,在onStart()方法内会触发上面的initToAlarmDetail()方法,获取跳转里面的信息,在决定具体跳转到哪个Fragment。这里解释的可能不太清楚,可以在Github下载源码看看可能更好理解些。

弹框对应的xml文件代码,可以在Github内查看,可以自己写一个,这个xml比较简单,只是xml代码比较占地方这里就不粘贴了。

前后台判断

关于前后台判断,需要创建一个继承ActivityLifecycleCallbacks和Application的类,这里命名为CustomApplication,在类里面实现ActivityLifecycleCallbacks接口相关方法,此外需要创建下面三个变量,分别表示activity数量,当前activity的名称,是否处于后台,代码如下:

typescript 复制代码
private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

之后需要在onActivityStarted,onActivityResumed,onActivityStopped方法内进行前后台相关处理,代码如下:

kotlin 复制代码
override fun onActivityStarted(activity: Activity) {
    activityCount++
    if (isInBackground){
        isInBackground = false
    }
    nowActivityName = activity.javaClass.name
}

override fun onActivityStopped(activity: Activity) {
    activityCount--
    if (activityCount == 0 && !isInBackground){
        isInBackground = true
    }
}

上面代码可以看出,当触发onActivityStarted方法时候,activityCount数量加一,且app处于前台。之后记录当前activity名称,这里记录activity名称是后面有个功能是app置于后台时候弹出通知,而通知相关操作,为了每个activity都能实现就放在基类执行,而弹出通知并不需要每个继承基类的activity都执行,到时候需要根据根据nowActivityName判断哪个继承了基类的activity执行通知操作。

当触发onActivityStopped方法时候,activityCount数量减一,且当activityCount数量为零时,app置于后台。 CustomApplication完整代码如下:

kotlin 复制代码
class CustomApplication: Application(),Application.ActivityLifecycleCallbacks {
    companion object{
        const val TAG = "CustomApplication"
        @SuppressLint("CustomContext")
        lateinit var context: Context 
    }

    private var activityCount = 0
    private var nowActivityName:String? = null
    private var isInBackground = true

    fun getNowActivityName(): String? {
        return nowActivityName
    }

    fun getIsInBackground():Boolean{
        return isInBackground
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
        registerActivityLifecycleCallbacks(this)
    }

    override fun onActivityCreated(activity: Activity, p1: Bundle?) {

    }

    override fun onActivityStarted(activity: Activity) {
        activityCount++
        if (isInBackground){
            isInBackground = false
        }
        nowActivityName = activity.javaClass.name
    }

    override fun onActivityResumed(activity: Activity) {

    }

    override fun onActivityPaused(activity: Activity) {

    }

    override fun onActivityStopped(activity: Activity) {
        activityCount--
        if (activityCount == 0 && !isInBackground){
            isInBackground = true
        }
    }

    override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) {

    }

    override fun onActivityDestroyed(activity: Activity) {

    }
}

弹框与通知弹出

开发中弹框与通知弹出的触发条件是,监听Websocket若有信息过来,app处于前台弹框,处于后台弹通知。这里使用Handler来模拟,弹框弹出比较简单,若有继承了DialogFragment的AlarmDialogFragment类。代码如下:

scss 复制代码
val dialog = AlarmDialogFragment()
dialog.show(supportFragmentManager,"tag")

通知弹出也不难,若只是弹出通知示例代码如下:

erlang 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val channel =
        NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
    notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
    .setContentTitle("标题")
    .setContentText("通知次数:${++alarmCount}")
    .setSmallIcon(R.drawable.ic_launcher_background)
    .setTimeoutAfter(5000)
    .setAutoCancel(true)
    .build()
notificationManager?.notify(notificationId,notification)

弹框与通知的特殊要求是,能在界面任意地方弹出且跳转到指定界面。弹框跳转相关代码在上面'弹框'部分,下面来说下通知的跳转,点击通知跳转是通过创建PendingIntent后在设置进NotificationCompat的setContentIntent方法内,不过通知跳转与弹框跳转一样需要分两种情况考虑,第一种同一Activity内Fragment与Fragment跳转,这种情况下PendingIntent如下代码所示:

css 复制代码
var pendingIntent:PendingIntent? = null
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
    .setGraph(R.navigation.main_navigation)
    .setDestination(R.id.alarmDetailFragment)
    .setArguments(bundle)
    .createPendingIntent()

上面代码中使用NavDeepLinkBuilder创建了一个PendingIntent,并且使用setGraph()指向使用的导航图,setDestination()则指向目标Fragment。 另一种情况则是ActivityB到另一个ActivityA内的指定FragmentB界面,这种情况下PendingIntent设置代码如下:

less 复制代码
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
    .addNextIntentWithParentStack(intent)
    .getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)

这种是创建一个跳转到MainActivity的Intent,并添加传递的参数task,接着设置Intent的启动方式,其中Intent.FLAG_ACTIVITY_NEW_TASK,表示启动Activity作为新任务启动,Intent.FLAG_ACTIVITY_CLEAR_TASK,表示清除任务栈中所有现有的Activity。之后调用TaskStackBuilder创建PendingIntent。 上面两种方式创建的PendingIntent可以通过NotificationCompat.setContentIntent(pendingIntent)添加进去,关于通知创建的代码如下:

kotlin 复制代码
/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
    private fun useNotificationPI() {
        var pendingIntent:PendingIntent? = null
        if(javaClass.simpleName == "MainActivity"){//主界面
            val bundle = Bundle()
            bundle.putString("alarmId","1")
            pendingIntent = NavDeepLinkBuilder(this)
                .setGraph(R.navigation.main_navigation)
                .setDestination(R.id.alarmDetailFragment)
                .setArguments(bundle)
                .createPendingIntent()
        }else {//其他界面时候切换后台通知
            val intent = Intent(this@BaseActivity,MainActivity::class.java)
            intent.putExtra("task","toAlarmDetail")
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            pendingIntent = TaskStackBuilder.create(this@BaseActivity)
                .addNextIntentWithParentStack(intent)
                .getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
            notificationManager?.createNotificationChannel(channel)
        }
        val notification = NotificationCompat.Builder(this.applicationContext, "normal")
            .setContentTitle("标题")
            .setContentText("通知次数:${++alarmCount}")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setTimeoutAfter(5000)
            .setAutoCancel(true)
            .setContentIntent(pendingIntent)
            .build()
        notificationManager?.notify(notificationId,notification)
    }

上面代码中if(javaClass.simpleName == "MainActivity"),及第四行代码,该代码用处是当app置于后台时候,pp界面是MainActivity时,pendingIntent使用NavDeepLinkBuilder生成,当是其他Activity时使用TaskStackBuilder生成。之所以这样是因为,在MainActivity的xml,使用了FragmentContainerView用于fragment间跳转,其他的Activity没有FragmentContainerView,因此在生成pendingIntent需要采用不同的方式生成。

这示例代码中,主要涉及到的Avtivity有MainActivity与VideoActivity,MainActivity使用FragmentContainerView,而VideoActivity没有。弹框与通知跳转的界面是AlarmDetailFragment,这个fragment在MainActivity通过Navigation实现导航。

因此在MainActivity界面进入后台时,pendingIntent使用NavDeepLinkBuilder生成,NavDeepLinkBuilder则可以使用导航图中fragment生成深度链接URI,这个URI则可以导航到指定的fragment(关于NavDeepLinkBuilder了解不深入,这里说的可能有错误地方,欢迎大佬指正)。

而VideoActivity界面进入后台时,就需要使用TaskStackBuilder生成一个启动MainActivity的Intent。而在MainActivity的onStart方法内有下面initToAlarmDetail方法,判断跳转时携带参数决定是否跳转到AlarmDetailFragment界面。

kotlin 复制代码
/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
    val task = intent.getStringExtra("task")
    if (task == "toAlarmDetail"){
        val bundle = Bundle()
        bundle.putString("alarmId","1")
        findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
    }
}

至此弹框与通知的功能基本实现,完整的BaseActivity代码如下:

kotlin 复制代码
open class BaseActivity: AppCompatActivity() {
    companion object{
        const val TAG = "BaseActivity"
    }
    private var alarmCount = 0
    private val handler = Handler(Looper.myLooper()!!)
    //为了关闭通知,manager放在外面
    private val notificationId = 1
    private var alarmDialogFragment: AlarmDialogFragment? = null
    private var notificationManager:NotificationManager? = null
    private var bgServiceIntent:Intent? = null//前台服务

    private var nowClassName = ""

    /** 弹框定时任务 */
    private val dialogRunnable = object : Runnable {
        override fun run() {
            //在定时方法里面 javaClass.simpleName 不能获取当前所处Activity的名称
            if (nowClassName == "VideoActivity"){ //视频界面不弹弹框
                CustomLog.d(TAG,"不使用弹框 ${nowClassName}")
            }else{
                CustomLog.d(TAG,"使用弹框 ${nowClassName}")
                useDialog()
                handler.postDelayed(this, 10000)
            }
        }
    }

    /** 通知定时任务 */
    private val notificationRunnable = object :Runnable{
        override fun run() {
            useNotificationPI()
            handler.postDelayed(this,10000)
        }
    }

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
        initWindow()
        return super.onCreateView(name, context, attrs)
    }

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) 当前类:${javaClass.simpleName}")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?) 当前类:${javaClass.simpleName}")
        initData()
    }

    override fun onStart() {
        super.onStart()
        CustomLog.d(TAG,"onStart 当前类:${javaClass.simpleName}")
        nowClassName = javaClass.simpleName
        handler.postDelayed(dialogRunnable, 3000)
        initService()
    }

    override fun onResume() {
        super.onResume()
        CustomLog.d(TAG,"onResume 当前类:${javaClass.simpleName}")
    }

    override fun onRestart() {
        super.onRestart()
        CustomLog.d(TAG,"onRestart 当前类:${javaClass.simpleName}")
    }

    override fun onPause() {
        super.onPause()
        CustomLog.d(TAG,"onPause 当前类:${javaClass.simpleName}")
    }

    override fun onStop() {
        super.onStop()
        CustomLog.d(TAG,"onStop 当前类:${javaClass.simpleName}")
        val customApplication = applicationContext as CustomApplication
        val nowActivityName = customApplication.getNowActivityName()
        val activitySimpleName = nowActivityName?.substringAfterLast(".")
        CustomLog.d(TAG,"activitySimpleName:$activitySimpleName")
        val isInBackground = (this@BaseActivity.applicationContext as CustomApplication).getIsInBackground()
        if (isInBackground && activitySimpleName.equals(javaClass.simpleName)){// 处于后台 且 切换至后台app的activity页面名称等于当前基类里面获取activity类名
            handler.postDelayed(notificationRunnable,3000)
            CustomLog.d(TAG,"使用通知 $nowClassName")
        }else{
            CustomLog.d(TAG,"关闭所有定时任务 $nowClassName")
            closeAllTask()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        CustomLog.d(TAG,"onDestroy 当前类:${javaClass.simpleName}")
        closeAllTask()
        this.stopService(bgServiceIntent)
    }

    /** 关闭所有定时任务 */
    private fun closeAllTask() {
        handler.removeCallbacks(dialogRunnable)
        handler.removeCallbacks(notificationRunnable)
    }

    /** 初始化数据 - 关于弹框*/
    private fun initData() {
        notificationManager = notificationManager ?: this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        alarmDialogFragment = alarmDialogFragment ?: AlarmDialogFragment()
    }

    /** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
    private fun useNotificationPI() {
        var pendingIntent:PendingIntent? = null
        if(javaClass.simpleName == "MainActivity"){//主界面
            CustomLog.d(TAG,">>>通知:MainActivity")
            val bundle = Bundle()
            bundle.putString("alarmId","1")
            pendingIntent = NavDeepLinkBuilder(this)
                .setGraph(R.navigation.main_navigation)
                .setDestination(R.id.alarmDetailFragment)
                .setArguments(bundle)
                .createPendingIntent()
        }else {//其他界面时候切换后台通知
            CustomLog.d(TAG,">>>通知:else")
            val intent = Intent(this@BaseActivity,MainActivity::class.java)
            intent.putExtra("task","toAlarmDetail")
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            pendingIntent = TaskStackBuilder.create(this@BaseActivity)
                .addNextIntentWithParentStack(intent)
                .getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
            notificationManager?.createNotificationChannel(channel)
        }
        val notification = NotificationCompat.Builder(this.applicationContext, "normal")
            .setContentTitle("标题")
            .setContentText("通知次数:${++alarmCount}")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setTimeoutAfter(5000)
            .setAutoCancel(true)
            .setContentIntent(pendingIntent)
            .build()
        notificationManager?.notify(notificationId,notification)
    }

    /** 弹框使用 - 因为此处涉及到fragment等生命周期,进入其他activity内时候,在前的activity使用useDialog会因为生命周期问题闪退*/
    private fun useDialog() {
        //弹出多个同种弹框
//        alarmDialogFragment = AlarmDialogFragment()
//        alarmDialogFragment?.show(supportFragmentManager,"testDialog")

        //不弹出多个同种弹框,一次只弹一个,若弹框存在不弹新框
        if (alarmDialogFragment?.isVisible == false){//如果不加这一句,当弹框存在时候在调用alarmDialogFragment.show的时候会报错,因为alarmDialogFragment已经存在
            alarmDialogFragment?.show(supportFragmentManager,"testDialog")
        }else{
            //更新弹框内信息
        }
    }

    /** 关闭报警弹框 */
    private fun closeAlarmDialog() {
        if (alarmDialogFragment?.isVisible == true) {
            alarmDialogFragment?.dismiss()//要关闭的弹框
        }
    }

    //状态栏透明,且组件占据了状态栏
    private fun initWindow() {
        window.statusBarColor = Color.TRANSPARENT
        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    }

    /** 初始化服务 */
    private fun initService() {
        CustomLog.d(TAG,"开启前台服务")
        bgServiceIntent = bgServiceIntent ?: Intent(this, BackgroundService::class.java)
        this.startService(bgServiceIntent)
    }
}

总结

只是弹出弹框和通知的话,实现很好实现,中间麻烦地方在于当app使用多个Activity,该怎么实现跳转到指定的界面。当然这里麻烦是,从ActivityB跳转到ActivityA的Fragment,如果是只有一个Activity应该会好办些。个人感觉fragment跳转应该有更好的方式实现希望能和大佬们交流下这种情况下,用什么技术实现。

PS:感觉原生Android在写界面和跳转方面写起来不太方便。不知道大家有便捷的方式吗。

代码地址

GitHub:github.com/SmallCrispy...

相关推荐
sunly_2 小时前
Flutter:AnimatedPadding动态修改padding
android·flutter
诸神黄昏EX2 小时前
Android 常用命令和工具解析之GPU相关
android
顾北川_野2 小时前
Android 布局菜单或按钮图标或Menu/Item设置可见和不可见
android
练习本2 小时前
android 动画原理分析
android
别拿曾经看以后~2 小时前
原生Android调用uniapp项目中的方法
android·vue.js·uni-app
Winston Wood2 小时前
Android Binder技术概览
android·binder·进程通信
踏雪羽翼2 小时前
android 使用实现音效--Equalizer
android·音效·eqequalizer·bassboost·presetreverb
老码沉思录2 小时前
Android开发实战班 - Android开发基础之 Kotlin语言基础与特性
android·微信·kotlin
峥嵘life3 小时前
Android adb shell dumpsys audio 信息查看分析详解
android·adb
standxy4 小时前
集成金蝶云星空数据至MySQL的完整案例解析
android·数据库·mysql