前言

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

功用分析

弹框完成,运用DialogFragment。
前后台判别则是,创立一个承继自ActivityLifecycleCallbacks接口和Application的类,承继ActivityLifecycleCallbacks接口是为了前后台判别,承继Application则是方便在基类BaseActivity获取前后台相关数据。
项目本来选用单Activity多Fragment完成,后边由于增加了视频相关功用,改为了多Activity多Fragment。
原单Activity时分,完成比较简单。后边修改为多Activity,就有些头疼,最终用思路是创立基类BaseActivity,后边增加Activity时都要承继基类BaseActivity。运用基类原因是把相同的功用抽取出来,且若每个Activity都自己完成弹框和告诉的话太简单犯错,也太简单漏下代码了。

代码完成

弹框

在完成承继自DialogFragment的弹框时,需求在onCreateDialog办法内设置dialog的宽高模式以及布景,否则弹框会有默认的边距,导致显现效果与预期不符,未去边距与去掉边距的弹框效果如下:

开发需求记录:实现app任意界面弹框与app置于后台时通知
关于onCreateDialog的代码如下:

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代码如下:

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()的办法运用下面办法。

/** 跳转报警概况界面 */
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的称号,是否处于后台,代码如下:

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

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

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完好代码如下:

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类。代码如下:

val dialog = AlarmDialogFragment()
dialog.show(supportFragmentManager,"tag")

告诉弹出也不难,若仅仅弹出告诉示例代码如下:

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如下代码所示:

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设置代码如下:

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)增加进去,关于告诉创立的代码如下:

/** 运用告诉 - 经过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界面。

/** 跳转报警概况界面 */
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代码如下:

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…