Bỏ qua để đến nội dung

Hiển thị nội dung tràn viền (edge-to-edge) trong app dùng View

Khi app target SDK 35 trở lên và chạy trên thiết bị Android 15 trở lên, app sẽ hiển thị edge-to-edge: cửa sổ app chiếm toàn bộ chiều rộng và chiều cao màn hình, vẽ cả phần nằm phía sau các system bar. System bar gồm status bar, caption bar và navigation bar.

Nhiều app có top app bar. Top app bar nên kéo dài lên tận mép trên màn hình và nằm phía sau status bar. Bạn có thể cho top app bar thu nhỏ lại bằng chiều cao status bar khi nội dung được cuộn.

Tương tự, nhiều app có bottom app bar hoặc bottom navigation bar. Các thanh này cũng nên kéo dài xuống mép dưới màn hình và nằm phía sau navigation bar. Nếu app không có các thanh này, nội dung cuộn nên hiển thị phía sau navigation bar.

Hình 1. Các system bar trong bố cục edge-to-edge.

Khi làm bố cục edge-to-edge cho app, bạn cần lưu ý các việc sau:

  • Bật hiển thị edge-to-edge
  • Làm bố cục thích ứng (adaptive layout) để trải nghiệm tốt trên nhiều kiểu thiết bị (điện thoại, tablet, máy gập…)
  • Xử lý các chỗ nội dung bị chồng lấn với system UI
  • Cân nhắc hiển thị scrim (lớp nền mờ) phía sau các system bar

Ảnh minh hoạ của app hiển thị tràn lên phía sau status bar, ngay cả vùng tai thỏ của camera

Hình 2. Ví dụ hình ảnh hiển thị phía sau status bar.

Nếu app target SDK 35 trở lên, edge-to-edge được bật tự động trên thiết bị Android 15 trở lên.

Để bật edge-to-edge trên các phiên bản Android cũ hơn, gọi enableEdgeToEdge trong onCreate của Activity:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.enableEdgeToEdge(window)
...
}

Mặc định, enableEdgeToEdge() làm các system bar trong suốt (transparent). Ngoại lệ là chế độ điều hướng 3 nút: navigation bar khi đó được phủ một scrim bán trong suốt (translucent). Màu của các icon hệ thống và của scrim tự điều chỉnh theo theme sáng hoặc tối của hệ thống.

Nếu muốn bật edge-to-edge mà không dùng hàm enableEdgeToEdge(), xem Manually set up the edge-to-edge display.

Một số view trong app có thể bị vẽ phía sau các system bar, như trong hình 3.

Bạn xử lý chồng lấn bằng cách phản ứng với insets. Insets cho biết những phần nào của màn hình giao với system UI như navigation bar hay status bar. “Giao nhau” có thể là system UI hiển thị đè lên nội dung, nhưng cũng có thể là thông tin về vùng dành cho cử chỉ hệ thống.

Các loại insets liên quan đến việc hiển thị app edge-to-edge:

  • System bars insets: phù hợp nhất cho các view bấm được và không được phép bị system bar che khuất.
  • Display cutout insets: cho các vùng có thể bị khoét trên màn hình (tai thỏ, lỗ camera) do hình dạng thiết bị.
  • System gesture insets: cho các vùng điều hướng bằng cử chỉ mà hệ thống dùng, có độ ưu tiên cao hơn app của bạn.

System bars insets là loại insets được dùng nhiều nhất. Chúng biểu diễn vùng mà system UI hiển thị nằm trên app của bạn theo trục Z. Dùng chúng để dịch chuyển hoặc thêm padding cho các view bấm được và không được phép bị system bar che khuất.

Ví dụ, floating action button (FAB) trong hình 3 bị navigation bar che mất một phần:

Nút FAB màu hồng ở góc dưới bên phải bị navigation bar 3 nút đè lên một phần

Hình 3. Navigation bar chồng lên FAB trong bố cục edge-to-edge.

Để tránh kiểu chồng lấn này ở cả chế độ cử chỉ lẫn chế độ nút, bạn tăng margin của view bằng getInsets(int) với WindowInsetsCompat.Type.systemBars().

Ví dụ sau cho thấy cách áp dụng system bars insets:

ViewCompat.setOnApplyWindowInsetsListener(fab) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
// Áp insets làm margin cho view. Ở đây chỉ đặt các cạnh dưới, trái và phải,
// nhưng bạn có thể áp bất kỳ cạnh nào phù hợp với bố cục của mình.
// Cũng có thể cập nhật padding nếu hợp lý hơn.
v.updateLayoutParams<MarginLayoutParams> {
leftMargin = insets.left
bottomMargin = insets.bottom
rightMargin = insets.right
}
// Trả về CONSUMED nếu bạn không muốn window insets tiếp tục
// được truyền xuống các view con.
WindowInsetsCompat.CONSUMED
}

Áp dụng cách này cho ví dụ ở hình 3, FAB không còn bị che ở chế độ nút, như trong hình 4:

FAB đã được đẩy lên phía trên navigation bar 3 nút, không còn bị che

Hình 4. Hết chồng lấn ở chế độ điều hướng bằng nút.

Chế độ điều hướng bằng cử chỉ cũng vậy, như trong hình 5:

FAB nằm phía trên thanh cử chỉ ở mép dưới màn hình, không bị che

Hình 5. Hết chồng lấn ở chế độ điều hướng bằng cử chỉ.

Một số thiết bị có display cutout (phần màn hình bị khoét). Thường cutout nằm ở đầu màn hình và nằm trong status bar. Khi xoay ngang, cutout có thể nằm ở cạnh dọc. Tuỳ nội dung app hiển thị, bạn nên thêm padding để tránh display cutout, vì mặc định app sẽ vẽ cả vào vùng cutout.

Ví dụ, nhiều màn hình app hiển thị một danh sách. Đừng để các item trong danh sách bị display cutout hoặc system bar che mất.

ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
left = bars.left,
top = bars.top,
right = bars.right,
bottom = bars.bottom,
)
WindowInsetsCompat.CONSUMED
}

Giá trị truyền vào getInsets là phép OR theo bit (logical or) của hai loại system bars và display cutout, nghĩa là lấy vùng hợp của cả hai.

Đặt clipToPadding cho RecyclerView để phần padding cuộn cùng với các item. Nhờ vậy item có thể đi ra phía sau system bar khi người dùng cuộn, như ví dụ sau:

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

System gesture insets là các vùng của cửa sổ mà cử chỉ hệ thống được ưu tiên hơn app. Các vùng này được tô màu cam trong hình 6:

Màn hình điện thoại với dải màu cam dọc hai cạnh trái, phải và ở mép dưới, là vùng cử chỉ hệ thống

Hình 6. System gesture insets.

Giống system bars insets, bạn tránh chồng lấn với system gesture insets bằng getInsets(int) với WindowInsetsCompat.Type.systemGestures().

Dùng các insets này để dịch chuyển hoặc thêm padding, đưa các view vuốt được ra xa mép màn hình. Các trường hợp hay gặp: bottom sheet, thao tác vuốt trong game, và carousel làm bằng ViewPager2.

Trên Android 10 trở lên, system gesture insets gồm một inset ở cạnh dưới cho cử chỉ về màn hình chính (home), và một inset ở cạnh trái, một ở cạnh phải cho cử chỉ quay lại (back):

Sơ đồ kích thước các vùng cử chỉ hệ thống: cạnh trái, cạnh phải cho cử chỉ back và cạnh dưới cho cử chỉ home

Hình 7. Kích thước các system gesture insets.

Ví dụ sau cho thấy cách áp dụng system gesture insets:

ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
// Áp insets làm padding cho view. Ở đây đặt cả bốn cạnh,
// tuỳ bố cục của bạn. Cũng có thể cập nhật margin nếu hợp lý hơn.
view.updatePadding(insets.left, insets.top, insets.right, insets.bottom)
// Trả về CONSUMED nếu bạn không muốn window insets tiếp tục
// được truyền xuống các view con.
WindowInsetsCompat.CONSUMED
}

Nhiều Android Material Components dạng View (com.google.android.material) tự xử lý insets, gồm BottomAppBar, BottomNavigationView, NavigationRailView và NavigationView.

Riêng AppBarLayout không tự xử lý insets. Thêm android:fitsSystemWindows="true" để nó xử lý insets ở cạnh trên.

Cách xử lý insets với Material Components trong Compose xem ở Material Components in Compose.

Để ngừng truyền insets xuống các view con và tránh bị padding hai lần, bạn có thể “tiêu thụ” (consume) insets bằng hằng số WindowInsetsCompat.CONSUMED. Tuy nhiên, trên thiết bị chạy Android 10 (API level 29) trở về trước, sau khi một view trả về WindowInsetsCompat.CONSUMED thì insets không được truyền sang các view anh em (sibling) nữa, có thể gây chồng lấn ngoài ý muốn.

Hai sơ đồ cây view: ở SDK 29, ViewGroup 1 tiêu thụ insets nên ViewGroup 2 không nhận được và TextView 2 bị navigation bar đè; ở SDK 30, cả hai ViewGroup đều nhận và tiêu thụ insets

Hình 8. Ví dụ insets bị truyền sai. Trên Android 10 (API level 29) trở về trước, sau khi ViewGroup 1 tiêu thụ insets thì insets không được truyền sang view anh em, khiến TextView 2 bị navigation bar đè lên. Từ Android 11 (API level 30), insets được truyền sang view anh em đúng như mong đợi.

Để đảm bảo insets được truyền sang các view anh em trên mọi phiên bản Android mà app hỗ trợ, gọi ViewGroupCompat#installCompatInsetsDispatch trước khi tiêu thụ insets. Hàm này có từ AndroidX Core và Core-ktx 1.16.0-alpha01.

// Dùng id gán cho root view của layout, ví dụ R.id.main
val rootView = findViewById<View>(R.id.main)
// Gọi trước khi tiêu thụ insets
ViewGroupCompat.installCompatInsetsDispatch(rootView)

Cùng cây view nhưng ở SDK 29 sau khi gọi installCompatInsetsDispatch: cả ViewGroup 1 và ViewGroup 2 đều nhận insets, TextView 2 không còn bị đè

Hình 9. Insets được truyền đúng sau khi gọi ViewGroupCompat#installCompatInsetsDispatch.

Có những nội dung xem toàn màn hình sẽ tốt hơn, cho người dùng trải nghiệm “đắm chìm” (immersive). Bạn ẩn các system bar cho immersive mode bằng WindowInsetsController và WindowInsetsControllerCompat:

val windowInsetsController =
WindowCompat.getInsetsController(window, window.decorView)
// Ẩn system bar.
windowInsetsController.hide(Type.systemBars())
// Hiện system bar.
windowInsetsController.show(Type.systemBars())

Chi tiết cách làm tính năng này xem ở Hide system bars for immersive mode.

Gọi enableEdgeToEdge đảm bảo màu icon trên system bar được cập nhật khi theme của thiết bị thay đổi.

Khi chuyển sang edge-to-edge, có thể bạn cần tự cập nhật màu icon trên system bar để chúng tương phản với nền app. Ví dụ, để icon trên status bar có màu sáng:

WindowCompat.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = false

Lưu ý tên thuộc tính dễ gây nhầm: isAppearanceLightStatusBars = true nghĩa là status bar có giao diện sáng (nền sáng, icon tối). Đặt false thì icon sáng màu, hợp với nền tối.

Lớp bảo vệ system bar (system bar protection)

Phần tiêu đề “Lớp bảo vệ system bar (system bar protection)”

Khi app target SDK 35 trở lên, edge-to-edge là bắt buộc. Status bar và navigation bar dạng cử chỉ của hệ thống trong suốt, còn navigation bar 3 nút thì bán trong suốt. Gọi enableEdgeToEdge để có hành vi tương tự trên các phiên bản cũ hơn.

Tuy vậy, mặc định của hệ thống không phải lúc nào cũng hợp với mọi trường hợp. Tham khảo hướng dẫn thiết kế system bar của Android và Edge-to-edge design để quyết định nên dùng system bar trong suốt hay bán trong suốt.

Để có status bar trong suốt: target Android 15 (SDK 35) trở lên, hoặc gọi enableEdgeToEdge() với tham số mặc định cho các phiên bản trước.

Để có navigation bar dạng cử chỉ trong suốt: target Android 15 trở lên, hoặc gọi enableEdgeToEdge() với tham số mặc định cho các phiên bản trước. Với navigation bar 3 nút, đặt Window.setNavigationBarContrastEnforced thành false, nếu không hệ thống sẽ phủ một scrim bán trong suốt.

Để tạo status bar bán trong suốt:

  1. Nâng dependency androidx-core lên 1.16.0-beta01 trở lên.
  2. Bọc XML layout trong androidx.core.view.insets.ProtectionLayout và gán cho nó một ID.
  3. Truy cập ProtectionLayout bằng code để đặt protection, chỉ định cạnh và một GradientProtection cho status bar.
system_bar_protection.xml
<androidx.core.view.insets.ProtectionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_protection"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/item_list"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--items-->
</ScrollView>
</androidx.core.view.insets.ProtectionLayout>
SystemBarProtectionSnippet.kt
findViewById<ProtectionLayout>(R.id.list_protection)
.setProtections(
listOf(
GradientProtection(
WindowInsetsCompat.Side.TOP,
// Lý tưởng nhất là màu nền của pane
paneBackgroundColor
)
)
)

Đảm bảo giá trị ColorInt truyền vào GradientProtection khớp với màu nền của nội dung. Ví dụ, bố cục list-detail trên máy gập có thể dùng các GradientProtection khác màu nhau cho pane danh sách và pane chi tiết.

Hình 10. Gradient protection với màu khác nhau cho từng pane.

Không tạo navigation bar dạng cử chỉ bán trong suốt. Để tạo navigation bar 3 nút bán trong suốt, làm một trong hai cách:

  • Nếu layout đã được bọc trong ProtectionLayout, bạn có thể truyền thêm một ColorProtection hoặc GradientProtection vào hàm setProtections. Trước đó, đảm bảo window.isNavigationBarContrastEnforced = false.
  • Nếu không, đặt window.isNavigationBarContrastEnforced = true.

Một số mẹo thêm khi xử lý insets.

Kiểm tra item cuối cùng trong RecyclerView hoặc NestedScrollView không bị system bar che, bằng cách xử lý insets và đặt clipToPadding thành false.

Video sau là một RecyclerView khi tắt edge-to-edge (bên trái) và khi bật (bên phải):

Code mẫu xem ở phần Create dynamic lists with RecyclerView.

Cho dialog toàn màn hình hiển thị edge-to-edge

Phần tiêu đề “Cho dialog toàn màn hình hiển thị edge-to-edge”

Để dialog toàn màn hình hiển thị edge-to-edge, gọi enableEdgeToEdge trên window của Dialog.

class MyAlertDialogFragment : DialogFragment() {
override fun onStart(){
super.onStart()
dialog?.window?.let { WindowCompat.enableEdgeToEdge(it) }
}
...
}

Blog

Thiết kế

Tài liệu khác

Video