Android全局通知彈窗的實(shí)現(xiàn)方法

從手機(jī)頂部劃入园匹,短暫停留后,再從頂部劃出戚丸。
首先需要明確的是:
1划址、這個(gè)彈窗的彈出邏輯不一定是當(dāng)前界面編寫的扔嵌,比如用戶上傳文件,用戶可能繼續(xù)瀏覽其他頁面的內(nèi)容夺颤,但是監(jiān)聽文件是否上傳完成還是在原來的Activity,但是Dialog的彈出是需要當(dāng)前頁面的上下文Context的痢缎。
2、Dialog彈窗必須支持手勢,用戶在Dialog上向上滑時(shí),Dialog需要退出,點(diǎn)擊時(shí)可能需要處理點(diǎn)擊事件世澜。

一独旷、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)
        //初始化界面數(shù)據(jù)
        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認(rèn)定為滑動事件
                        //Dialog消失
                    } else {                //認(rèn)定為點(diǎn)擊事件
                        //Dialog的點(diǎn)擊事件
                        mListener?.onClick()
                    }
                    dismiss()
                }
            }
        }
        return false
    }

    /**
     * 點(diǎn)擊是否在范圍外
     */
    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)
        }
    }

    //處理通知的點(diǎn)擊事件
    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>
        <!--設(shè)置背景透明-->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!--設(shè)置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">
        <!-- 進(jìn)入時(shí)的動畫 -->
        <item name="android:windowEnterAnimation">@anim/dialog_enter</item>
        <!-- 退出時(shí)的動畫 -->
        <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>

二寥裂、獲取當(dāng)前顯示的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)
    }

}

監(jiān)聽所有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:
 *     //發(fā)系統(tǒng)通知
 *    NotificationControlManager.getInstance()?.notify("文件上傳完成", "文件上傳完成,請點(diǎn)擊查看詳情")
 *    //發(fā)應(yīng)用內(nèi)通知
 *     NotificationControlManager.getInstance()?.showNotificationDialog("文件上傳完成","文件上傳完成,請點(diǎn)擊查看詳情",
 *           object : NotificationControlManager.OnNotificationCallback {
 *                override fun onCallback() {
 *                   Toast.makeText(this@MainActivity, "被點(diǎn)擊了", 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()
    }


    /**
     * 跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面去打開通知,注意在這之前應(yīng)該有個(gè)Dialog提醒用戶
     */
    fun openNotificationInSys() {
        val context = ForegroundActivityManager.getInstance().getCurrentActivity()!!
        val intent: Intent = Intent()
        try {
            intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS

            //8.0及以后版本使用這兩個(gè)extra.  >=API 26
            intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
            intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)

            //5.0-7.1 使用這兩個(gè)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()

            //其他低版本或者異常情況封恰,走該節(jié)點(diǎn)麻养。進(jìn)入APP設(shè)置界面
            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)
        }
    }

    /**
     * 發(fā)通知
     * @param title 標(biāo)題
     * @param content 內(nèi)容
     * @param cls 通知點(diǎn)擊后跳轉(zhuǎn)的Activity,默認(rèn)為null跳轉(zhuǎn)到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())
    }


    /**
     * 顯示應(yīng)用內(nèi)通知的Dialog,需要自己處理點(diǎn)擊事件。listener默認(rèn)為null,不處理也可以俭驮。dialog會在3000毫秒后自動消失
     * @param title 標(biāo)題
     * @param content 內(nèi)容
     * @param listener 點(diǎn)擊的回調(diào)
     */
    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()
    }

}

另外需要注意的點(diǎn)是,因?yàn)閐ialog是延遲關(guān)閉的,可能用戶立刻退出Activity,導(dǎo)致延遲時(shí)間到時(shí)dialog退出時(shí)報(bào)錯,解決辦法可以在BaseActivity的onDestroy方法中嘗試關(guān)閉Dialog:

override fun onDestroy() {
    super.onDestroy()
    NotificationControlManager.getInstance()?.dismissDialog()
}

來自:https://juejin.cn/post/7119049874175164453

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末回溺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子混萝,更是在濱河造成了極大的恐慌遗遵,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逸嘀,死亡現(xiàn)場離奇詭異车要,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)崭倘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門翼岁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人司光,你說我怎么就攤上這事琅坡。” “怎么了残家?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵榆俺,是天一觀的道長。 經(jīng)常有香客問我坞淮,道長茴晋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任回窘,我火速辦了婚禮诺擅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘啡直。我一直安慰自己烁涌,他們只是感情好苍碟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著烹玉,像睡著了一般驰怎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上二打,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天县忌,我揣著相機(jī)與錄音,去河邊找鬼继效。 笑死症杏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瑞信。 我是一名探鬼主播厉颤,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼凡简!你這毒婦竟也來了逼友?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤秤涩,失蹤者是張志新(化名)和其女友劉穎帜乞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筐眷,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡黎烈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匀谣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片照棋。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖武翎,靈堂內(nèi)的尸體忽然破棺而出烈炭,到底是詐尸還是另有隱情,我是刑警寧澤宝恶,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布符隙,位于F島的核電站,受9級特大地震影響卑惜,放射性物質(zhì)發(fā)生泄漏膏执。R本人自食惡果不足惜驻售,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一露久、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧欺栗,春花似錦毫痕、人聲如沸征峦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栏笆。三九已至,卻和暖如春臊泰,著一層夾襖步出監(jiān)牢的瞬間蛉加,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工缸逃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留针饥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓需频,卻偏偏與公主長得像丁眼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子昭殉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容