Adaptive Layout là một cách tiếp cận bố cục giao diện trong đó ứng dụng sử dụng các bố cục khác nhau cho các loại thiết bị và hướng màn hình khác nhau. Không giống như responsive design, adaptive layout chuyển đổi giữa các bố cục được chuẩn bị sẵn tại các điểm ngắt. Bài viết này phân tích Size Classes và UITraitCollection trong iOS, bộ định tính sw600dp/layout-w600dp trong Android và đưa ra các ví dụ thực tế.
Những điểm chính
Adaptive Layout là một chiến lược UI nơi nhà phát triển tạo các bố cục riêng biệt cho từng loại thiết bị và chuyển đổi giữa chúng dựa trên đặc điểm màn hình. Không giống như responsive design nơi các phần tử trôi chảy mượt mà, adaptive layout sử dụng chuyển đổi rời rạc: điện thoại — một bố cục, máy tính bảng — bố cục khác, máy tính để bàn — bố cục thứ ba.
Apple giới thiệu Size Classes trong iOS 8 (2014) cùng với các ứng dụng phổ quát (nhị phân chạy trên cả iPhone và iPad). Google giới thiệu bộ định tính smallestWidth (sw
Các hướng dẫn hiện đại của Apple HIG và Google Material Design đều đồng ý rằng adaptive layout là cần thiết cho các ứng dụng hỗ trợ nhiều hơn một kích thước màn hình. Google Play đánh dấu việc thiếu thích ứng với máy tính bảng là một nhược điểm. App Store không có yêu cầu chính thức, nhưng người dùng iPad mong đợi trải nghiệm gốc, không phải ứng dụng iPhone bị kéo giãn.
Size Classes là một đặc tính iOS xác định không gian khả dụng là Compact (gọn) hoặc Regular (thông thường) trên mỗi trục. UITraitCollection là một hệ thống iOS để truyền các đặc tính môi trường qua hệ thống phân cấp view: kích thước, tỷ lệ màn hình, khả năng force touch, dải màu (sáng/tối), cài đặt trợ năng.
| Thiết bị / Hướng | Ngang | Dọc |
|---|---|---|
| iPhone dọc (tất cả các model) | Compact | Regular |
| iPhone Plus/Pro Max ngang | Regular | Compact |
| iPhone SE / mini ngang | Compact | Compact |
| iPad dọc | Regular | Regular |
| iPad ngang | Regular | Regular |
| iPad Split View (1/3 màn hình) | Compact | Regular |
UITraitCollection được hệ thống tạo ra và truyền từ UIApplication qua UIWindow đến từng UIView. Khi hướng hoặc kích thước cửa sổ (Split View) thay đổi, hệ thống tạo UITraitCollection mới và gọi traitCollectionDidChange. Trong SwiftUI, các thay đổi được theo dõi qua Environment Values và view được vẽ lại tự động.
Việc thích ứng qua UITraitCollection trong UIKit được thực hiện bằng cách đăng ký thay đổi trait collection và xây dựng lại bố cục. Ví dụ dưới đây cho thấy một bộ điều khiển chuyển đổi giữa ngăn xếp dọc cho iPhone và bố cục chia cho iPad.
// Bộ điều khiển thích ứng với Size Classes
import UIKit
final class AdaptiveViewController: UIViewController {
private let compactStack = UIStackView()
private let regularStack = UIStackView()
private let sidebar = UIView()
private let content = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateLayout(for: traitCollection)
}
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.horizontalSizeClass
!= previousTraitCollection?.horizontalSizeClass {
updateLayout(for: traitCollection)
}
}
private func updateLayout(for traits: UITraitCollection) {
if traits.horizontalSizeClass == .regular {
showRegularLayout() // iPad: thanh bên + nội dung
} else {
showCompactLayout() // iPhone: thanh tab + đẩy
}
}
private func showRegularLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
regularStack.addArrangedSubview(sidebar)
regularStack.addArrangedSubview(content)
regularStack.frame = view.bounds
regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(regularStack)
}
private func showCompactLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
compactStack.addArrangedSubview(content)
compactStack.frame = view.bounds
compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(compactStack)
}
private func setupViews() {
sidebar.backgroundColor = .systemGray6
content.backgroundColor = .systemBackground
compactStack.axis = .vertical
regularStack.axis = .horizontal
}
}Phương thức traitCollectionDidChange được gọi khi có bất kỳ thay đổi trait nào, nhưng chúng ta chỉ kiểm tra horizontalSizeClass — chỉ báo quan trọng nhất để chuyển đổi bố cục. Trong iOS 17+ nên sử dụng UIViewController.horizontalSizeClass như một thuộc tính tính toán để cập nhật phản ứng mà không cần đăng ký thay đổi.
Android cung cấp hai bộ định tính chiều rộng chính: sw
| Bộ định tính | Điều kiện | Ví dụ thiết bị |
|---|---|---|
| layout-sw600dp | Chiều rộng tối thiểu ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Chiều rộng hiện tại ≥ 600 dp | iPad ở Split View (600 dp), điện thoại ở chế độ ngang |
| layout-land | Hướng ngang | Bất kỳ thiết bị nào xoay ngang |
| layout-sw720dp | Chiều rộng tối thiểu ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Hệ thống phân cấp tài nguyên trong Android: res/layout/activity_main.xml (điện thoại), res/layout-sw600dp/activity_main.xml (máy tính bảng 7\"), res/layout-w600dp-land/activity_main.xml (điện thoại ở chế độ ngang). Android chọn bộ định tính cụ thể nhất phù hợp với cấu hình thiết bị hiện tại. Khi không có tệp phù hợp, tài nguyên cơ sở từ res/layout/ được sử dụng.
Phát triển Android hiện đại sử dụng Jetpack WindowManager để xác định kích thước cửa sổ thay vì đọc trực tiếp tài nguyên. Điều này đặc biệt quan trọng đối với các thiết bị có thể gập lại nơi kích thước màn hình thay đổi động (Galaxy Fold mở ra từ 6,2\" lên 7,6\").
// Thích ứng qua Jetpack WindowManager
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
FlowRow(
verticalAlignment = Arrangement.Top,
horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
else -> Arrangement.Start
}
) {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
CompactContent()
}
WindowWidthSizeClass.MEDIUM -> {
MediumContent()
}
WindowWidthSizeClass.EXPANDED -> {
ExpandedContent()
}
}
}
}
@Composable
private fun CompactContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text("Điện thoại", style = MaterialTheme.typography.headlineSmall)
ListContent()
}
}
@Composable
private fun MediumContent() {
Row(modifier = Modifier.padding(24.dp)) {
NavigationRail { ... }
Column { ListContent() }
}
}
@Composable
private fun ExpandedContent() {
Row(modifier = Modifier.padding(32.dp)) {
PermanentNavigationDrawer { ... }
Column {
ListContent()
}
DetailPane()
}
}Thành phần currentWindowAdaptiveInfo() từ thư viện androidx.window thích ứng giao diện với mọi thiết bị: điện thoại, máy tính bảng, thiết bị gập, ChromeOS. Các lớp chiều rộng: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Các lớp chiều cao: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI cung cấp các công cụ tích hợp cho bố cục thích ứng mà không cần sử dụng trực tiếp Size Classes. AnyLayout, ViewThatFits và GeometryReader cho phép xây dựng các giao diện tự động điều chỉnh theo không gian khả dụng.
// Thích ứng SwiftUI qua AnyLayout và ViewThatFits
import SwiftUI
struct AdaptiveGrid: View {
@Environment(\.horizontalSizeClass) private var hSizeClass
var body: some View {
let layout = hSizeClass == .regular
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Label("Yêu thích", systemImage: "star")
Label("Gần đây", systemImage: "clock")
Label("Cài đặt", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — chọn bố cục tự động
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Ưu tiên 1: ngang
CardView()
CardView()
}
VStack { // Nếu không vừa — dọc
CardView()
CardView()
}
}
}
}ViewThatFits là một công cụ mạnh mẽ của SwiftUI tự động chọn view con đầu tiên vừa với không gian khả dụng mà không bị cắt. Điều này loại bỏ nhu cầu kiểm tra tường minh Size Classes cho các chuyển đổi thích ứng đơn giản. AnyLayout cho phép chuyển đổi loại bố cục (HStack/VStack) mà không cần if-else trong thân view.
Jetpack Compose sử dụng WindowSizeClass (từ thư viện Material 3 adaptive) và BoxWithConstraints cho bố cục thích ứng. Không giống như các bộ định tính XML, Compose xác định kích thước cửa sổ tại thời điểm chạy và tổng hợp lại UI một cách phản ứng khi cấu hình thay đổi.
// Jetpack Compose thích ứng qua BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Máy tính bảng: hai cột (list-detail)
Row(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(items) { item ->
ListItem(text = item)
}
}
var selected by remember { mutableStateOf(items.first()) }
DetailPanel(item = selected)
.weight(2f)
}
} else {
// Điện thoại: một cột với điều hướng
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
ListItem(text = item, onClick = { navigateToDetail(item) })
}
}
}
}
}
@Composable
fun AdaptiveListMaterial3() {
val windowClass = currentWindowAdaptiveInfo().windowSizeClass
AdaptiveLayout(
layout = windowClass.windowWidthSizeClass
) {
when (windowClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
else -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
}
}
}Material 3 Adaptive Layouts cung cấp các thành phần có sẵn: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Các thành phần này tự động thích ứng với WindowWidthSizeClass — chuyển đổi giữa xếp chồng (COMPACT) và cạnh nhau (MEDIUM/EXPANDED). Nhà phát triển chỉ cần chọn một mẫu và truyền các bảng nội dung.
Câu hỏi thường gặp
sw600dp (smallestWidth) — chiều rộng màn hình tối thiểu bất kể hướng. Luôn kích hoạt trên máy tính bảng 7\"+. w600dp (available width) — chiều rộng khả dụng có tính đến hướng hiện tại. Kích hoạt trên điện thoại ở chế độ ngang nơi chiều rộng có thể vượt quá 600 dp.
UITraitCollection là một đối tượng iOS chứa các đặc tính môi trường: size classes (ngang/dọc), tỷ lệ màn hình, khả năng force touch, ngữ cảnh giao diện người dùng (iPhone/iPad), dải màu, cài đặt trợ năng. Nó được truyền từ UIApplication qua hệ thống phân cấp view và thay đổi khi hướng hoặc kích thước cửa sổ thay đổi.
Sử dụng Jetpack WindowManager (Android) và UIScreen nativeBounds + traitCollection (iOS). Thiết bị gập thay đổi Size Class khi mở ra. Kiến trúc dựa trên bố cục chuẩn (list-detail, supporting pane) với đăng ký phản ứng thay đổi kích thước cửa sổ mà không khởi động lại Activity được khuyến nghị.
Không. Sử dụng một storyboard duy nhất với các biến thể ràng buộc Size Class hoặc bố cục lập trình qua UIKit. SwiftUI hoàn toàn không yêu cầu storyboard. Storyboard riêng cho iPad tạo ra sự trùng lặp và làm phức tạp việc bảo trì — tốt hơn nên thích ứng một bố cục duy nhất qua Size Classes.
Canonical layouts là các mẫu bố cục thích ứng có sẵn từ Google: list-detail (danh sách + chi tiết), supporting pane (nội dung + bảng hành động), feed (thẻ). Mỗi mẫu hỗ trợ ba trạng thái (compact/medium/expanded) và được triển khai trong thư viện Material 3 Adaptive qua ListDetailScaffold và SupportingPaneScaffold.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm