@@ -3,12 +3,15 @@ package com.omarea.common.ui
33import android.animation.Animator
44import android.animation.AnimatorListenerAdapter
55import android.app.Activity
6+ import android.graphics.PixelFormat
67import android.graphics.drawable.GradientDrawable
78import android.os.Handler
89import android.os.Looper
10+ import android.view.Gravity
911import android.view.LayoutInflater
1012import android.view.View
1113import android.view.ViewGroup
14+ import android.view.WindowManager
1215import android.widget.ImageView
1316import android.widget.TextView
1417import androidx.core.view.ViewCompat
@@ -20,7 +23,13 @@ enum class BannerType { INFO, SUCCESS, WARNING, ERROR }
2023/* *
2124 * Hiện 1 banner thông báo đè lên trên cùng của Activity đang foreground.
2225 * Khác với Toast: hiển thị trong nội dung ứng dụng, có thể tùy biến màu/icon/tiêu đề,
23- * và chỉ hoạt động khi app đang mở (cần 1 Activity foreground để addView vào).
26+ * và chỉ hoạt động khi app đang mở (cần 1 Activity foreground để add vào).
27+ *
28+ * Banner được thêm vào dưới dạng 1 Window độc lập (WindowManager.addView), KHÔNG phải
29+ * child view của content Activity. Lý do: nếu chỉ addView vào content, banner sẽ bị các
30+ * Dialog/AlertDialog (vốn là 1 Window riêng nổi trên Window của Activity) che mất, vì
31+ * Dialog luôn ở lớp trên bất kể elevation. Thêm dưới dạng Window riêng, add sau cùng,
32+ * đảm bảo banner luôn nổi trên mọi Dialog đang mở tại thời điểm gọi.
2433 *
2534 * Gọi từ shell: am broadcast -a <applicationId>.broadcast.BANNER --es text "..." --es type "success"
2635 */
@@ -71,12 +80,23 @@ object BannerNotificationManager {
7180 return
7281 }
7382 isShowing = true
74- showOn(activity, req)
83+ try {
84+ showOn(activity, req)
85+ } catch (e: Exception ) {
86+ // Activity có thể vừa bị hủy đúng lúc addView (BadTokenException...) -> bỏ qua, sang cái tiếp theo
87+ showNext()
88+ }
7589 }
7690
7791 private fun showOn (activity : Activity , req : BannerRequest ) {
78- val root = activity.findViewById<ViewGroup >(android.R .id.content)
79- val view = LayoutInflater .from(activity).inflate(R .layout.banner_notification, root, false )
92+ val decorView = activity.window?.decorView
93+ if (decorView == null || ! decorView.isAttachedToWindow) {
94+ showNext()
95+ return
96+ }
97+ val windowManager = activity.windowManager
98+
99+ val view = LayoutInflater .from(activity).inflate(R .layout.banner_notification, null , false )
80100
81101 val bannerRoot = view.findViewById<View >(R .id.banner_root)
82102 val icon = view.findViewById<ImageView >(R .id.banner_icon)
@@ -110,7 +130,21 @@ object BannerNotificationManager {
110130 insets
111131 }
112132
113- root.addView(view)
133+ val params = WindowManager .LayoutParams (
134+ ViewGroup .LayoutParams .MATCH_PARENT ,
135+ ViewGroup .LayoutParams .WRAP_CONTENT ,
136+ WindowManager .LayoutParams .TYPE_APPLICATION ,
137+ WindowManager .LayoutParams .FLAG_NOT_TOUCH_MODAL or
138+ WindowManager .LayoutParams .FLAG_NOT_FOCUSABLE or
139+ WindowManager .LayoutParams .FLAG_LAYOUT_IN_SCREEN or
140+ WindowManager .LayoutParams .FLAG_LAYOUT_NO_LIMITS ,
141+ PixelFormat .TRANSLUCENT
142+ ).apply {
143+ gravity = Gravity .TOP
144+ token = decorView.windowToken
145+ }
146+
147+ windowManager.addView(view, params)
114148 ViewCompat .requestApplyInsets(view)
115149
116150 var dismissed = false
@@ -124,7 +158,11 @@ object BannerNotificationManager {
124158 .setDuration(200 )
125159 .setListener(object : AnimatorListenerAdapter () {
126160 override fun onAnimationEnd (animation : Animator ) {
127- root.removeView(view)
161+ try {
162+ windowManager.removeViewImmediate(view)
163+ } catch (e: Exception ) {
164+ // Window có thể đã bị hệ thống dọn (activity destroyed) -> bỏ qua
165+ }
128166 showNext()
129167 }
130168 })
0 commit comments