注册

Android全局的通知的弹窗

需求分析


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


image.png


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


首先需要明确的是:

1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activity或Service,但是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) >= 15) { //滑动超过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
private var mIsActive:Boolean = false

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

fun setActive(isActive:Boolean){
mIsActive = isActive
}

fun getActive():Boolean=mIsActive


}

监听所有Activity的生命周期,并判断当前页面是否可以显示Dialog,参考LiveData的源码,判断当前是否是Active状态,如果是Activie状态则可以显示Dialog,如果非Active状态则等待下次Active时显示Dialog

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)
//设置当前Active状态为true
ForegroundActivityManager.getInstance().setActive(true)
}

override fun onActivityPaused(activity: Activity) {
//设置当前Active状态为false
ForegroundActivityManager.getInstance().setActive(false)
}

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 contentMap = mutableListOf<NotificationInfo>()
private var dialogList = mutableListOf<NotificationDialog>()

companion object {
const val channelId = "app"
const val description = "my application"

@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 filterActivityByClassNameList 过滤哪些类不显示Dialog
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
filterActivityByClassNameList: MutableList<String>? = null,
listener: OnNotificationCallback? = null
) {
val currentActivity = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
//判断是否需要过滤页面不显示Dialog
filterActivityByClassNameList?.forEach {
val className = currentActivity.javaClass.simpleName
if (className == it) {
return
}
}

val isActive = ForegroundActivityManager.getInstance()?.getActive() ?: false
if (isActive) { //Active状态
val dialog = NotificationDialog(currentActivity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else { //如果当前Activity非Active状态则把要显示的内容存储到集合中
//存到集合中
contentMap.add(NotificationInfo(title, content))
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialogWithNotLifecycle(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* set dialog click
*/
private fun setDialogClick(
dialog: NotificationDialog?,
listener: OnNotificationCallback?
) {
if (listener != null) {
dialog?.setOnNotificationClickListener(object :
NotificationDialog.OnNotificationClick {
override fun onClick() = listener.onCallback()
})
}
}

/**
* 显示没有显示过的Dialog
*/
fun showDialogNeverVisible() {
if (contentMap.isNotEmpty()) {
val iterator = contentMap.iterator()
while (iterator.hasNext()) {
val info = iterator.next()
val currentActivity =
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val dialog =
NotificationDialog(currentActivity, info.title, info.content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
}
}
}

/**
* dismiss Dialog
*/
fun dismissDialogWithLifecycle() {
if (dialogList.size > 0) {
val iterator = dialogList.iterator()
while (iterator.hasNext()) {
val dialog = iterator.next()
if (dialog != null && dialog.isShowing) {
dialog.dismiss()
}
iterator.remove()
}
}
}


interface OnNotificationCallback {
fun onCallback()
}

}
//根据需求封装
data class NotificationInfo(var title:String,var content:String)

需要注意的点是:

1、Activity处于转场时是不能显示Dialog的,此时会回调onPause方法,isActive处于false状态(这一点参考LiveData的源码),将需要显示的数据存储于集合中,待BaseActivity回调onResume时显示没有显示的Dialog。

override fun onResume() {
super.onResume()
NotificationControlManager.getInstance()?.showDialogNeverVisible()
}

2、因为dialog是延迟关闭的,可能用户立刻退出Activity,导致延迟时间到时dialog退出时报错,解决办法可以在BaseActivity的onPause方法中尝试关闭Dialog:

override fun onPause() {
super.onPause()
NotificationControlManager.getInstance()?.dismissDialogWithLifecycle()
}

作者:TimeFine
链接:https://juejin.cn/post/7119049874175164453
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册