Skip to content

Commit e3cbe4d

Browse files
committed
Update
1 parent 71f99a5 commit e3cbe4d

2 files changed

Lines changed: 86 additions & 47 deletions

File tree

app/src/main/java/com/omarea/common/ui/BannerNotificationManager.kt

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,22 @@ enum class BannerPosition { TOP, BOTTOM }
2020
* đang có Dialog mở (ví dụ ProgressBarDialog).
2121
*
2222
* 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.
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 (không bấm tắt sớm được), và không thể tùy
30+
* chỉnh thời gian hiển thị (Android giới hạn cứng ở mức LENGTH_LONG ~3.5s cho mọi Toast).
31+
*
32+
* Gọi từ shell: am broadcast -a <applicationId>.broadcast.BANNER --es text "..." --es type "success" --es position "bottom"
2433
*/
2534
object BannerNotificationManager {
2635
private val mainHandler = Handler(Looper.getMainLooper())
36+
37+
// Thời gian hiển thị thực tế của Toast.LENGTH_LONG do hệ thống quy định, dùng để giãn cách
38+
// các banner trong hàng đợi, tránh cái sau ghi đè ngay lên cái trước.
2739
private const val TOAST_LONG_DURATION_MS = 3500L
2840

2941
private data class BannerRequest(
@@ -37,6 +49,12 @@ object BannerNotificationManager {
3749
private val queue = ArrayDeque<BannerRequest>()
3850
private var isShowing = false
3951

52+
/**
53+
* @param icon Tên resource drawable/mipmap tùy chỉnh (vd "ic_my_icon"). Nếu bỏ trống hoặc
54+
* không tìm thấy resource tương ứng, mặc định dùng icon của chính app.
55+
* @param onNoActivity gọi khi không có Activity nào đang foreground (app ở background),
56+
* dùng để nơi gọi có thể fallback sang Toast thường hoặc bỏ qua.
57+
*/
4058
fun show(
4159
title: String? = null,
4260
message: String,
@@ -102,25 +120,33 @@ object BannerNotificationManager {
102120
messageView.text = req.message
103121

104122
val density = activity.resources.displayMetrics.density
105-
val yOffset = (50 * density).toInt()
106123

107124
val toast = Toast(activity.applicationContext)
108125
toast.duration = Toast.LENGTH_LONG
109126
@Suppress("DEPRECATION")
110127
toast.view = view
111128

112-
// Xác định gravity gọn gàng, tránh lỗi ngắt dòng khi gọi hàm
113-
val gravity = when (req.position) {
114-
BannerPosition.TOP -> Gravity.TOP or Gravity.CENTER_HORIZONTAL
115-
BannerPosition.BOTTOM -> Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
129+
when (req.position) {
130+
BannerPosition.TOP -> toast.setGravity(
131+
Gravity.TOP or Gravity.CENTER_HORIZONTAL, 0, (50 * density).toInt()
132+
)
133+
BannerPosition.BOTTOM -> toast.setGravity(
134+
Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, (70 * density).toInt()
135+
)
116136
}
117-
toast.setGravity(gravity, 0, yOffset)
118-
119137
toast.show()
120138

139+
// Toast tự ẩn theo thời lượng hệ thống quy định (LENGTH_LONG), chỉ cần đợi tương ứng
140+
// rồi xử lý banner tiếp theo trong hàng đợi (nếu có).
121141
mainHandler.postDelayed({ showNext() }, TOAST_LONG_DURATION_MS)
122142
}
123143

144+
/**
145+
* Tìm drawable cho icon theo thứ tự ưu tiên:
146+
* 1. Nếu `name` là đường dẫn file ảnh tồn tại trên máy (vd "/sdcard/.../icon.png") -> decode trực tiếp từ file.
147+
* 2. Nếu `name` là tên resource drawable/mipmap có sẵn trong app -> dùng resource đó.
148+
* 3. Nếu không có gì khớp -> mặc định trả về icon của chính app.
149+
*/
124150
private fun resolveIcon(activity: Activity, name: String?): android.graphics.drawable.Drawable? {
125151
if (!name.isNullOrEmpty()) {
126152
if (name.startsWith("/") || name.startsWith("file://")) {
@@ -154,4 +180,4 @@ object BannerNotificationManager {
154180
null
155181
}
156182
}
157-
}
183+
}
Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,63 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3-
android:id="@+id/banner_root"
2+
<!--
3+
Lưu ý: view này được dùng làm Toast.view (xem BannerNotificationManager.kt).
4+
Toast KHÔNG đặt view vào trong 1 ViewGroup cha khi hiển thị, nên các thuộc tính
5+
layout_margin... trên view gốc sẽ bị bỏ qua hoàn toàn. Vì vậy phải dùng 1 FrameLayout
6+
ngoài cùng với padding (padding luôn có tác dụng, không phụ thuộc parent) để tạo
7+
khoảng cách với mép màn hình, thay vì đặt margin trực tiếp lên banner_root.
8+
-->
9+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
410
android:layout_width="match_parent"
511
android:layout_height="wrap_content"
6-
android:orientation="horizontal"
7-
android:gravity="center_vertical"
8-
android:background="@drawable/banner_bg"
9-
android:elevation="8dp"
10-
android:layout_marginStart="16dp"
11-
android:layout_marginEnd="16dp"
1212
android:paddingStart="16dp"
13-
android:paddingEnd="16dp"
14-
android:paddingTop="14dp"
15-
android:paddingBottom="14dp">
16-
17-
<ImageView
18-
android:id="@+id/banner_icon"
19-
android:layout_width="24dp"
20-
android:layout_height="24dp"
21-
android:layout_gravity="top"
22-
android:src="@drawable/ic_banner_info" />
13+
android:paddingEnd="16dp">
2314

2415
<LinearLayout
25-
android:layout_width="0dp"
16+
android:id="@+id/banner_root"
17+
android:layout_width="match_parent"
2618
android:layout_height="wrap_content"
27-
android:layout_weight="1"
28-
android:layout_marginStart="12dp"
29-
android:orientation="vertical">
19+
android:orientation="horizontal"
20+
android:gravity="center_vertical"
21+
android:background="@drawable/banner_bg"
22+
android:elevation="8dp"
23+
android:paddingStart="16dp"
24+
android:paddingEnd="16dp"
25+
android:paddingTop="14dp"
26+
android:paddingBottom="14dp">
3027

31-
<TextView
32-
android:id="@+id/banner_title"
33-
android:layout_width="match_parent"
34-
android:layout_height="wrap_content"
35-
android:textColor="@android:color/white"
36-
android:textSize="15sp"
37-
android:textStyle="bold"
38-
android:visibility="gone" />
28+
<ImageView
29+
android:id="@+id/banner_icon"
30+
android:layout_width="24dp"
31+
android:layout_height="24dp"
32+
android:layout_gravity="top"
33+
android:src="@drawable/ic_banner_info" />
3934

40-
<TextView
41-
android:id="@+id/banner_message"
42-
android:layout_width="match_parent"
35+
<LinearLayout
36+
android:layout_width="0dp"
4337
android:layout_height="wrap_content"
44-
android:layout_marginTop="2dp"
45-
android:textColor="@android:color/white"
46-
android:textSize="14sp"
47-
android:maxLines="4" />
38+
android:layout_weight="1"
39+
android:layout_marginStart="12dp"
40+
android:orientation="vertical">
41+
42+
<TextView
43+
android:id="@+id/banner_title"
44+
android:layout_width="match_parent"
45+
android:layout_height="wrap_content"
46+
android:textColor="@android:color/white"
47+
android:textSize="15sp"
48+
android:textStyle="bold"
49+
android:visibility="gone" />
50+
51+
<TextView
52+
android:id="@+id/banner_message"
53+
android:layout_width="match_parent"
54+
android:layout_height="wrap_content"
55+
android:layout_marginTop="2dp"
56+
android:textColor="@android:color/white"
57+
android:textSize="14sp"
58+
android:maxLines="4" />
59+
</LinearLayout>
60+
4861
</LinearLayout>
4962

50-
</LinearLayout>
63+
</FrameLayout>

0 commit comments

Comments
 (0)