美文网首页
Android全局通知弹窗实现

Android全局通知弹窗实现

作者: 程序员Android1 | 来源:发表于2022-11-01 23:18 被阅读0次

    和你一起终身学习,这里是程序员 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
    链接:https://juejin.cn/post/7119049874175164453
    至此,本篇已结束。转载网络的文章,小编觉得很优秀,欢迎点击阅读原文,支持原创作者,如有侵权,恳请联系小编删除,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

    相关文章

      网友评论

          本文标题:Android全局通知弹窗实现

          本文链接:https://www.haomeiwen.com/subject/sobdtdtx.html