Giới thiệu
Bài viết này hướng dẫn cách tạo Notification tùy chỉnh (Custom Notification) trên Android sử dụng RemoteViews và NotificationCompat.Builder. Cho phép kiểm soát hoàn toàn layout, màu chữ, font, icon, hình ảnh trong notification.
Các bước thực hiện
Bước 1: Tạo dự án mới
Mở Android Studio, chọn File → New Project và điền thông tin cần thiết.
Bước 2: Layout chính (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btnStandardNotification"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Notification chuẩn"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnCustomNotification"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Custom Notification (RemoteViews)"
app:layout_constraintTop_toBottomOf="@id/btnStandardNotification"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Bước 3: Layout Custom Notification (res/layout/custom_notification.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="@android:color/white">
<ImageView
android:id="@+id/imagenotileft"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="App icon"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/imagenotileft"
android:layout_marginStart="16dp"
android:layout_alignTop="@id/imagenotileft"
android:text="Custom Title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#212121" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/imagenotileft"
android:layout_marginStart="16dp"
android:layout_below="@id/title"
android:layout_marginTop="4dp"
android:text="Custom message content"
android:textSize="14sp"
android:textColor="#757575" />
<ImageView
android:id="@+id/imagenotiright"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:padding="8dp"
android:contentDescription="Action icon"
android:scaleType="centerCrop" />
</RelativeLayout>
Bước 4: Layout Standard Notification (res/layout/notification_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Notification - 1" />
<Button
android:id="@+id/customnotification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_below="@id/notification"
android:layout_marginTop="16dp"
android:text="Custom Notification - 1" />
</RelativeLayout>
Bước 5: Notification View Activity (res/layout/notification_view.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/lbltitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title:"
android:textStyle="bold"
android:textSize="16sp" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/lbltitle"
android:layout_marginStart="8dp"
android:textSize="16sp" />
<TextView
android:id="@+id/lbltext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/lbltitle"
android:layout_marginTop="8dp"
android:text="Message:"
android:textStyle="bold"
android:textSize="16sp" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_toEndOf="@id/lbltext"
android:layout_marginStart="8dp"
android:textSize="16sp" />
</RelativeLayout>
Bước 6: Activity chính (src/MainActivity.java)
package com.app.sample;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.RemoteViews;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String CHANNEL_ID = "custom_notification_channel";
private static final int NOTIFICATION_ID_STANDARD = 100;
private static final int NOTIFICATION_ID_CUSTOM = 101;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createNotificationChannel();
Button btnStandard = findViewById(R.id.btnStandardNotification);
Button btnCustom = findViewById(R.id.btnCustomNotification);
btnStandard.setOnClickListener(v -> showStandardNotification());
btnCustom.setOnClickListener(v -> showCustomNotification());
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Custom Notifications";
String description = "Kênh cho custom notification demo";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
channel.enableLights(true);
channel.setLightColor(Color.BLUE);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}
private void showStandardNotification() {
Intent intent = new Intent(this, NotificationViewActivity.class);
intent.putExtra("title", "Standard Notification");
intent.putExtra("text", "Đây là notification chuẩn hệ thống");
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("Standard Notification")
.setContentText("Đây là notification chuẩn hệ thống")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.addAction(R.drawable.ic_launcher_foreground, "Action", pendingIntent);
NotificationManagerCompat.from(this).notify(NOTIFICATION_ID_STANDARD, builder.build());
Toast.makeText(this, "Đã gửi Standard Notification", Toast.LENGTH_SHORT).show();
}
private void showCustomNotification() {
// Tạo RemoteViews từ custom layout
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.custom_notification);
// Set content cho RemoteViews
remoteViews.setTextViewText(R.id.title, "Custom Notification Title");
remoteViews.setTextViewText(R.id.text, "Nội dung tùy chỉnh với màu chữ, font, layout riêng");
remoteViews.setTextColor(R.id.title, Color.parseColor("#1565C0")); // Blue đậm
remoteViews.setTextColor(R.id.text, Color.parseColor("#424242")); // Grey đậm
remoteViews.setImageViewResource(R.id.imagenotileft, R.drawable.ic_launcher_foreground);
remoteViews.setImageViewResource(R.id.imagenotiright, R.drawable.ic_launcher_foreground);
Intent intent = new Intent(this, NotificationViewActivity.class);
intent.putExtra("title", "Custom Notification");
intent.putExtra("text", "Bạn đã click vào Custom Notification");
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// PendingIntent cho nút action bên phải (nếu có)
PendingIntent actionPendingIntent = PendingIntent.getBroadcast(
this, 1, new Intent("CUSTOM_ACTION"),
PendingIntent.FLAG_IMMUTABLE
);
remoteViews.setOnClickPendingIntent(R.id.imagenotiright, actionPendingIntent);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground) // Bắt buộc: small icon cho status bar
.setCustomContentView(remoteViews) // Layout cho trạng thái thu gọn
.setCustomBigContentView(remoteViews) // Layout cho trạng thái mở rộng (big)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
NotificationManagerCompat.from(this).notify(NOTIFICATION_ID_CUSTOM, builder.build());
Toast.makeText(this, "Đã gửi Custom Notification", Toast.LENGTH_SHORT).show();
}
}
Bước 7: NotificationViewActivity (src/NotificationViewActivity.java)
package com.app.sample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class NotificationViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.notification_view);
String title = getIntent().getStringExtra("title");
String text = getIntent().getStringExtra("text");
TextView tvTitle = findViewById(R.id.title);
TextView tvText = findViewById(R.id.text);
tvTitle.setText(title);
tvText.setText(text);
}
}
Bước 8: Manifest (androidManifest.xml)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="com.app.sample">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Android 13+ -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
</intent-filter>
</activity>
<activity android:name=".NotificationViewActivity" />
</application>
</manifest>
Chạy ứng dụng
Kết nối thiết bị, nhấn Run. Nhấn 2 nút để so sánh Standard vs Custom Notification. Kéo xuống notification shade để xem custom layout.
RemoteViews - Các phương thức quan trọng
| Phương thức | Chức năng |
|---|---|
setTextViewText(viewId, text) |
Set text cho TextView |
setTextColor(viewId, color) |
Set màu chữ (API 23+) |
setTextViewTextSize(viewId, unit, size) |
Set cỡ chữ |
setImageViewResource(viewId, resId) |
Set image resource cho ImageView |
setImageViewBitmap(viewId, bitmap) |
Set bitmap cho ImageView |
setViewVisibility(viewId, visibility) |
VISIBLE/INVISIBLE/GONE |
setOnClickPendingIntent(viewId, pendingIntent) |
Xử lý click trên view trong notification |
setProgress(viewId, max, progress, indeterminate) |
Cập nhật ProgressBar |
Lưu ý quan trọng về RemoteViews
- Chỉ hỗ trợ subset View/Layout: LinearLayout, RelativeLayout, FrameLayout, GridLayout, TextView, ImageView, Button, ProgressBar, Chronometer, AnalogClock, ViewStub.
- KHÔNG hỗ trợ: ConstraintLayout (API 24+ mới hỗ trợ một phần), RecyclerView, ListView, WebView, EditText, custom View.
- Layout phải đơn giản: Tránh nested quá sâu, animation, selector.
setCustomContentView: Layout thu gọn (collapsed) - thường 1 dòng.setCustomBigContentView: Layout mở rộng (expanded) - khi user kéo xuống.setCustomHeadsUpContentView: Layout cho Heads-up notification (API 24+).
Android 13+ (API 33): Runtime Permission POST_NOTIFICATIONS
// Manifest
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
// Runtime request
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS},
REQUEST_CODE_NOTIFICATION);
}
}
Thư viện hiện đại: NotificationCompat + Material Design
- Dùng
NotificationCompat.Buildertừandroidx.core:core:1.12.0. - Style có sẵn:
NotificationCompat.DecoratedCustomViewStyle- tự động thêm icon, header, actions cho custom view. - MediaStyle, MessagingStyle, BigPictureStyle, InboxStyle cho các trường hợp chuyên biệt.
Kết luận
Custom Notification với RemoteViews cho phép branding mạnh, hiển thị thông tin phức tạp, nhưng bị hạn chế về View hỗ trợ. Luôn test trên nhiều phiên bản Android (đặc biệt API 24+, 26+, 33+).