前言
在产品经理提需求时分说到,app在接收到报警信息时分能不能弹出一个弹框,告知用户报警信息,这个弹框要在app的恣意界面能够弹出,并且用户点击概况时分,会跳转到报警概况界面,检查具体信息,当用户将app至于后台的时分,接收到报警信息,app发送告诉,当用户点击告诉时分,跳转到报警概况界面。 功用大体总结如上,在完成弹框与告诉在跳转界面时遇到一些问题,在此记录一下。效果图如下:
功用分析
弹框完成,运用DialogFragment。
前后台判别则是,创立一个承继自ActivityLifecycleCallbacks接口和Application的类,承继ActivityLifecycleCallbacks接口是为了前后台判别,承继Application则是方便在基类BaseActivity获取前后台相关数据。
项目本来选用单Activity多Fragment完成,后边由于增加了视频相关功用,改为了多Activity多Fragment。
原单Activity时分,完成比较简单。后边修改为多Activity,就有些头疼,最终用思路是创立基类BaseActivity,后边增加Activity时都要承继基类BaseActivity。运用基类原因是把相同的功用抽取出来,且若每个Activity都自己完成弹框和告诉的话太简单犯错,也太简单漏下代码了。
代码完成
弹框
在完成承继自DialogFragment的弹框时,需求在onCreateDialog办法内设置dialog的宽高模式以及布景,否则弹框会有默认的边距,导致显现效果与预期不符,未去边距与去掉边距的弹框效果如下: 关于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…