Android全局的通知的弹窗

4,860 阅读6分钟

需求分析

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

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