Android 全局通知弹窗示例分析详解

需求分析

如何创建一个全局通知的弹窗?如下图所示。

从手机顶部划入,短暂停留后,再从顶部划出。

首先需要明确的是:

1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activity,但是Dialog的弹出是需要当前页面的上下文Context的。

2、Dialog弹窗必须支持手势,用户在Dialog上向上滑时,Dialog需要退出,点击时可能需要处理点击事件。

一、Dialog的编写

/**
 * 通知的自定义Dialog
 */
class NotificationDialog(context: Context, var title: String, var content: String) :
 Dialog(context, R.style.dialog_notifacation_top) {
 private var mListener: OnNotificationClick? = null
 private var mStartY: Float = 0F
 private var mView: View? = null
 private var mHeight: Int? = 0
 init {
 mView = LayoutInflater.from(context).inflate(R.layout.common_layout_notifacation, null)
 }
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(mView!!)
 window?.setGravity(Gravity.TOP)
 val layoutParams = window?.attributes
 layoutParams?.width = ViewGroup.LayoutParams.MATCH_PARENT
 layoutParams?.height = ViewGroup.LayoutParams.WRAP_CONTENT
 layoutParams?.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
 window?.attributes = layoutParams
 window?.setWindowAnimations(R.style.dialog_animation)
 //按空白处不能取消
 setCanceledOnTouchOutside(false)
 //初始化界面数据
 initData()
 }
 private fun initData() {
 val tvTitle = findViewById<TextView>(R.id.tv_title)
 val tvContent = findViewById<TextView>(R.id.tv_content)
 if (title.isNotEmpty()) {
 tvTitle.text = title
 }
 if (content.isNotEmpty()) {
 tvContent.text = content
 }
 }
 override fun onTouchEvent(event: MotionEvent): Boolean {
 when (event.action) {
 MotionEvent.ACTION_DOWN -> {
 if (isOutOfBounds(event)) {
 mStartY = event.y
 }
 }
 MotionEvent.ACTION_UP -> {
 if (mStartY > 0 && isOutOfBounds(event)) {
 val moveY = event.y
 if (abs(mStartY - moveY) >= 20) { //滑动超过20认定为滑动事件
 //Dialog消失
 } else { //认定为点击事件
 //Dialog的点击事件
 mListener?.onClick()
 }
 dismiss()
 }
 }
 }
 return false
 }
 /**
 * 点击是否在范围外
 */
 private fun isOutOfBounds(event: MotionEvent): Boolean {
 val yValue = event.y
 if (yValue > 0 && yValue <= (mHeight ?: (0 + 40))) {
 return true
 }
 return false
 }
 private fun setDialogSize() {
 mView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
 mHeight = v?.height
 }
 }
 /**
 * 显示Dialog但是不会自动退出
 */
 fun showDialog() {
 if (!isShowing) {
 show()
 setDialogSize()
 }
 }
 /**
 * 显示Dialog,3000毫秒后自动退出
 */
 fun showDialogAutoDismiss() {
 if (!isShowing) {
 show()
 setDialogSize()
 //延迟3000毫秒后自动消失
 Handler(Looper.getMainLooper()).postDelayed({
 if (isShowing) {
 dismiss()
 }
 }, 3000L)
 }
 }
 //处理通知的点击事件
 fun setOnNotificationClickListener(listener: OnNotificationClick) {
 mListener = listener
 }
 interface OnNotificationClick {
 fun onClick()
 }
}

Dialog的主题

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
 <style name="dialog_notifacation_top">
 <item name="android:windowIsTranslucent">true</item>
 <!--设置背景透明-->
 <item name="android:windowBackground">@android:color/transparent</item>
 <!--设置dialog浮与activity上面-->
 <item name="android:windowIsFloating">true</item>
 <!--去掉背景模糊效果-->
 <item name="android:backgroundDimEnabled">false</item>
 <item name="android:windowNoTitle">true</item>
 <!--去掉边框-->
 <item name="android:windowFrame">@null</item>
 </style>
 <style name="dialog_animation" parent="@android:style/Animation.Dialog">
 <!-- 进入时的动画 -->
 <item name="android:windowEnterAnimation">@anim/dialog_enter</item>
 <!-- 退出时的动画 -->
 <item name="android:windowExitAnimation">@anim/dialog_exit</item>
 </style>
</resources>

Dialog的动画

<set xmlns:android="http://schemas.android.com/apk/res/android">
 <translate
 android:duration="600"
 android:fromYDelta="-100%p"
 android:toYDelta="0%p" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
 <translate
 android:duration="300"
 android:fromYDelta="0%p"
 android:toYDelta="-100%p" />
</set>

Dialog的布局,通CardView包裹一下就有立体阴影的效果

<androidx.cardview.widget.CardView
 android:id="@+id/cd"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:layout_margin="@dimen/size_15dp"
 app:cardCornerRadius="@dimen/size_15dp"
 app:cardElevation="@dimen/size_15dp"
 app:layout_constraintTop_toTopOf="parent">
 <androidx.constraintlayout.widget.ConstraintLayout
 android:id="@+id/et_name"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:layout_margin="@dimen/size_15dp"
 app:layout_constraintTop_toTopOf="parent">
 <androidx.appcompat.widget.AppCompatTextView
 android:id="@+id/tv_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:textColor="#000000"
 android:textSize="@dimen/font_14sp" android:textStyle="bold"
 app:layout_constraintLeft_toLeftOf="parent"
 app:layout_constraintTop_toTopOf="parent" />
 <androidx.appcompat.widget.AppCompatTextView
 android:id="@+id/tv_content"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_marginTop="@dimen/size_15dp"
 android:textColor="#333"
 android:textSize="@dimen/font_12sp"
 app:layout_constraintLeft_toLeftOf="parent"
 app:layout_constraintTop_toBottomOf="@id/tv_title" />
 </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

二、获取当前显示的Activity的弱引用

/**
 * 前台Activity管理类
 */
class ForegroundActivityManager {
 private var currentActivityWeakRef: WeakReference<Activity>? = null
 companion object {
 val TAG = "ForegroundActivityManager"
 private val instance = ForegroundActivityManager()
 @JvmStatic
 fun getInstance(): ForegroundActivityManager {
 return instance
 }
 }
 fun getCurrentActivity(): Activity? {
 var currentActivity: Activity? = null
 if (currentActivityWeakRef != null) {
 currentActivity = currentActivityWeakRef?.get()
 }
 return currentActivity
 }
 fun setCurrentActivity(activity: Activity) {
 currentActivityWeakRef = WeakReference(activity)
 }
}

监听所有Activity的生命周期

class AppLifecycleCallback:Application.ActivityLifecycleCallbacks {
 companion object{
 val TAG = "AppLifecycleCallback"
 }
 override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
 //获取Activity弱引用
 ForegroundActivityManager.getInstance().setCurrentActivity(activity)
 }
 override fun onActivityStarted(activity: Activity) {
 }
 override fun onActivityResumed(activity: Activity) {
 //获取Activity弱引用
 ForegroundActivityManager.getInstance().setCurrentActivity(activity)
 }
 override fun onActivityPaused(activity: Activity) {
 }
 override fun onActivityStopped(activity: Activity) {
 }
 override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
 }
 override fun onActivityDestroyed(activity: Activity) {
 }
}

在Application中注册

//注册Activity生命周期
registerActivityLifecycleCallbacks(AppLifecycleCallback())

三、封装和使用

/**
 * 通知的管理类
 * example:
 * //发系统通知
 * NotificationControlManager.getInstance()?.notify("文件上传完成", "文件上传完成,请点击查看详情")
 * //发应用内通知
 * NotificationControlManager.getInstance()?.showNotificationDialog("文件上传完成","文件上传完成,请点击查看详情",
 * object : NotificationControlManager.OnNotificationCallback {
 * override fun onCallback() {
 * Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
 * }
 * })
 */
class NotificationControlManager {
 private var autoIncreament = AtomicInteger(1001)
 private var dialog: NotificationDialog? = null
 companion object {
 const val channelId = "aaaaa"
 const val description = "描述信息"
 @Volatile
 private var sInstance: NotificationControlManager? = null
 @JvmStatic
 fun getInstance(): NotificationControlManager? {
 if (sInstance == null) {
 synchronized(NotificationControlManager::class.java) {
 if (sInstance == null) {
 sInstance = NotificationControlManager()
 }
 }
 }
 return sInstance
 }
 }
 /**
 * 是否打开通知
 */
 fun isOpenNotification(): Boolean {
 val notificationManager: NotificationManagerCompat =
 NotificationManagerCompat.from(
 ForegroundActivityManager.getInstance().getCurrentActivity()!!
 )
 return notificationManager.areNotificationsEnabled()
 }
 /**
 * 跳转到系统设置页面去打开通知,注意在这之前应该有个Dialog提醒用户
 */
 fun openNotificationInSys() {
 val context = ForegroundActivityManager.getInstance().getCurrentActivity()!!
 val intent: Intent = Intent()
 try {
 intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
 //8.0及以后版本使用这两个extra. >=API 26
 intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
 intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)
 //5.0-7.1 使用这两个extra. <= API 25, >=API 21
 intent.putExtra("app_package", context.packageName)
 intent.putExtra("app_uid", context.applicationInfo.uid)
 context.startActivity(intent)
 } catch (e: Exception) {
 e.printStackTrace()
 //其他低版本或者异常情况,走该节点。进入APP设置界面
 intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
 intent.putExtra("package", context.packageName)
 //val uri = Uri.fromParts("package", packageName, null)
 //intent.data = uri
 context.startActivity(intent)
 }
 }
 /**
 * 发通知
 * @param title 标题
 * @param content 内容
 * @param cls 通知点击后跳转的Activity,默认为null跳转到MainActivity
 */
 fun notify(title: String, content: String, cls: Class<*>) {
 val context = ForegroundActivityManager.getInstance().getCurrentActivity()!!
 val notificationManager =
 context.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
 val builder: Notification.Builder
 val intent = Intent(context, cls)
 val pendingIntent: PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
 } else {
 PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
 }
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 val notificationChannel =
 NotificationChannel(channelId, description, NotificationManager.IMPORTANCE_HIGH)
 notificationChannel.enableLights(true);
 notificationChannel.lightColor = Color.RED;
 notificationChannel.enableVibration(true);
 notificationChannel.vibrationPattern =
 longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
 notificationManager.createNotificationChannel(notificationChannel)
 builder = Notification.Builder(context, channelId)
 .setSmallIcon(R.drawable.jpush_notification_icon)
 .setContentIntent(pendingIntent)
 .setContentTitle(title)
 .setContentText(content)
 } else {
 builder = Notification.Builder(context)
 .setSmallIcon(R.drawable.jpush_notification_icon)
 .setLargeIcon(
 BitmapFactory.decodeResource(
 context.resources,
 R.drawable.jpush_notification_icon
 )
 )
 .setContentIntent(pendingIntent)
 .setContentTitle(title)
 .setContentText(content)
 }
 notificationManager.notify(autoIncreament.incrementAndGet(), builder.build())
 }
 /**
 * 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
 * @param title 标题
 * @param content 内容
 * @param listener 点击的回调
 */
 fun showNotificationDialog(
 title: String,
 content: String,
 listener: OnNotificationCallback? = null
 ) {
 val activity = ForegroundActivityManager.getInstance().getCurrentActivity()!!
 dialog = NotificationDialog(activity, title, content)
 if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
 activity.runOnUiThread {
 showDialog(dialog, listener)
 }
 } else {
 showDialog(dialog, listener)
 }
 }
 /**
 * show dialog
 */
 private fun showDialog(
 dialog: NotificationDialog?,
 listener: OnNotificationCallback?
 ) {
 dialog?.showDialogAutoDismiss()
 if (listener != null) {
 dialog?.setOnNotificationClickListener(object :
 NotificationDialog.OnNotificationClick {
 override fun onClick() = listener.onCallback()
 })
 }
 }
 /**
 * dismiss Dialog
 */
 fun dismissDialog() {
 if (dialog != null && dialog!!.isShowing) {
 dialog!!.dismiss()
 }
 }
 interface OnNotificationCallback {
 fun onCallback()
 }
}

另外需要注意的点是,因为dialog是延迟关闭的,可能用户立刻退出Activity,导致延迟时间到时dialog退出时报错,解决办法可以在BaseActivity的onDestroy方法中尝试关闭Dialog:

override fun onDestroy() {
 super.onDestroy()
 NotificationControlManager.getInstance()?.dismissDialog()
}
作者:TimeFine

%s 个评论

要回复文章请先登录注册