11package com.omarea.common.ui
22
3- import android.animation.Animator
4- import android.animation.AnimatorListenerAdapter
53import android.app.Activity
64import android.graphics.drawable.GradientDrawable
75import android.os.Handler
86import android.os.Looper
7+ import android.view.Gravity
98import android.view.LayoutInflater
109import android.view.View
1110import android.view.ViewGroup
1211import android.widget.ImageView
1312import android.widget.TextView
14- import androidx.core.view.ViewCompat
15- import androidx.core.view.WindowInsetsCompat
13+ import android.widget.Toast
1614import com.tool.tree.R
1715
1816enum class BannerType { INFO , SUCCESS , WARNING , ERROR }
1917
2018/* *
21- * Hiện 1 banner thông báo đè lên trên cùng của Activity (hoặc Dialog) đang hiển thị.
22- * 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 để add vào).
19+ * Hiện 1 banner thông báo đè lên trên cùng của Activity đang foreground -- KỂ CẢ khi
20+ * đang có Dialog mở (ví dụ ProgressBarDialog).
2421 *
25- * Nếu đang có Dialog mở (đăng ký qua [CurrentDialogHolder], ví dụ [ProgressBarDialog]),
26- * banner sẽ được add vào chính Window của Dialog đó để chắc chắn nổi lên trên — vì Dialog
27- * là 1 Window riêng biệt luôn nổi trên Window của Activity, add vào content Activity sẽ bị
28- * Dialog che mất bất kể elevation. Nếu không có Dialog nào mở, banner add vào content
29- * của Activity như bình thường.
22+ * Kỹ thuật: dùng chính cơ chế của Toast (custom view Toast, type cửa sổ TYPE_TOAST) thay vì
23+ * tự add view vào content của Activity/Dialog. Lý do: Dialog là 1 Window riêng, kích thước
24+ * chỉ vừa đủ khung dialog (không phải full màn hình), nên add view thường vào bên trong nó
25+ * sẽ bị bó hẹp/cắt theo đúng khung dialog nhỏ đó. TYPE_TOAST là loại cửa sổ đặc biệt của hệ
26+ * thống luôn nổi trên mọi Dialog/Activity của app (đây cũng chính là lý do ToastReceiver có
27+ * sẵn của bạn luôn hiện được dù đang có dialog hay không).
28+ *
29+ * Đánh đổi: cửa sổ Toast KHÔNG nhận sự kiện chạm, nên banner không thể bấm để tắt sớm --
30+ * sẽ tự động biến mất sau đúng thời gian `durationMs` được yêu cầu.
3031 *
3132 * Gọi từ shell: am broadcast -a <applicationId>.broadcast.BANNER --es text "..." --es type "success"
3233 */
@@ -42,10 +43,11 @@ object BannerNotificationManager {
4243
4344 private val queue = ArrayDeque <BannerRequest >()
4445 private var isShowing = false
46+ private var currentToast: Toast ? = null
4547
4648 /* *
4749 * @param onNoActivity gọi khi không có Activity nào đang foreground (app ở background),
48- * dùng để nơi gọi có thể fallback sang Toast hoặc bỏ qua.
50+ * dùng để nơi gọi có thể fallback sang Toast thường hoặc bỏ qua.
4951 */
5052 fun show (
5153 title : String? = null,
@@ -72,7 +74,6 @@ object BannerNotificationManager {
7274 }
7375 val activity = CurrentActivityHolder .get()
7476 if (activity == null ) {
75- // Không còn Activity nào để hiện -> bỏ qua item này, thử item tiếp theo
7677 showNext()
7778 return
7879 }
@@ -84,30 +85,13 @@ object BannerNotificationManager {
8485 }
8586 }
8687
87- /* * Tìm ViewGroup gốc để add banner vào: ưu tiên Window của Dialog đang mở, nếu không có thì dùng content của Activity. */
88- private fun resolveRoot (activity : Activity ): ViewGroup ? {
89- val topDialog = CurrentDialogHolder .getTopVisible()
90- val dialogWindow = topDialog?.window
91- if (dialogWindow != null && dialogWindow.decorView.isAttachedToWindow) {
92- val dialogContent = dialogWindow.findViewById<ViewGroup >(android.R .id.content)
93- if (dialogContent != null ) return dialogContent
94- }
95- return activity.findViewById(android.R .id.content)
96- }
97-
9888 private fun showOn (activity : Activity , req : BannerRequest ) {
99- val root = resolveRoot(activity)
100- if (root == null ) {
101- showNext()
102- return
103- }
104- val view = LayoutInflater .from(activity).inflate(R .layout.banner_notification, root, false )
89+ val view = LayoutInflater .from(activity).inflate(R .layout.banner_notification, null , false )
10590
10691 val bannerRoot = view.findViewById<View >(R .id.banner_root)
10792 val icon = view.findViewById<ImageView >(R .id.banner_icon)
10893 val titleView = view.findViewById<TextView >(R .id.banner_title)
10994 val messageView = view.findViewById<TextView >(R .id.banner_message)
110- val closeButton = view.findViewById<View >(R .id.banner_close)
11195
11296 val (colorRes, iconRes) = when (req.type) {
11397 BannerType .INFO -> R .color.banner_info to R .drawable.ic_banner_info
@@ -128,54 +112,30 @@ object BannerNotificationManager {
128112 }
129113 messageView.text = req.message
130114
131- // Tránh banner bị che bởi status bar / notch
132- ViewCompat .setOnApplyWindowInsetsListener(view) { v, insets ->
133- val top = insets.getInsets(WindowInsetsCompat .Type .systemBars()).top
134- v.setPadding(v.paddingLeft, top, v.paddingRight, v.paddingBottom)
135- insets
136- }
137-
138- root.addView(view)
139- ViewCompat .requestApplyInsets(view)
140-
141- var dismissed = false
142- val dismiss = {
143- if (! dismissed) {
144- dismissed = true
145- mainHandler.removeCallbacksAndMessages(view)
146- view.animate()
147- .translationY(- (view.height.takeIf { it > 0 } ? : 300 ).toFloat())
148- .alpha(0f )
149- .setDuration(200 )
150- .setListener(object : AnimatorListenerAdapter () {
151- override fun onAnimationEnd (animation : Animator ) {
152- try {
153- root.removeView(view)
154- } catch (e: Exception ) {
155- // root (vd. Window của Dialog) có thể đã bị đóng -> bỏ qua
156- }
157- showNext()
158- }
159- })
160- .start()
115+ // Toast mặc định wrap_content theo nội dung -> ép chiều rộng gần bằng màn hình
116+ // (trừ lề 2 bên) để có dáng banner tràn ngang như mong muốn.
117+ val density = activity.resources.displayMetrics.density
118+ val marginPx = (12 * density).toInt()
119+ val screenWidth = activity.resources.displayMetrics.widthPixels
120+ view.layoutParams = ViewGroup .LayoutParams (screenWidth - marginPx * 2 , ViewGroup .LayoutParams .WRAP_CONTENT )
121+
122+ val toast = Toast (activity.applicationContext)
123+ toast.duration = Toast .LENGTH_LONG
124+ @Suppress(" DEPRECATION" )
125+ toast.view = view
126+ // Offset nhỏ để không dính sát mép trên / bị status bar che
127+ toast.setGravity(Gravity .TOP or Gravity .CENTER_HORIZONTAL , 0 , (24 * density).toInt())
128+ toast.show()
129+ currentToast = toast
130+
131+ val runnable = Runnable {
132+ try {
133+ toast.cancel()
134+ } catch (e: Exception ) {
161135 }
136+ currentToast = null
137+ showNext()
162138 }
163-
164- closeButton.setOnClickListener { dismiss() }
165- view.setOnClickListener { dismiss() }
166-
167- view.alpha = 0f
168- view.translationY = - 200f
169- view.post {
170- view.translationY = - (view.height.takeIf { it > 0 } ? : 300 ).toFloat()
171- view.animate()
172- .translationY(0f )
173- .alpha(1f )
174- .setDuration(250 )
175- .start()
176- }
177-
178- val runnable = Runnable { dismiss() }
179- mainHandler.postAtTime(runnable, view, android.os.SystemClock .uptimeMillis() + req.durationMs)
139+ mainHandler.postDelayed(runnable, req.durationMs)
180140 }
181141}
0 commit comments