Adaptive Layout — abordare de realizare a interfețelor în care aplicația folosește diferite machete pentru diferite tipuri de dispozitive și orientări ale ecranului. Spre deosebire de responsive design, adaptive layout comută între machete pregătite în prealabil în punctele de breakpoint. Articolul analizează Size Classes și UITraitCollection în iOS, calificatorii sw600dp/layout-w600dp în Android și oferă exemple practice.
Principalele puncte
Adaptive Layout — strategie de construire a interfeței în care dezvoltatorul creează machete separate pentru fiecare tip de dispozitiv și comută între ele pe baza caracteristicilor ecranului. Spre deosebire de responsive design, unde elementele curg lin, adaptive layout folosește comutări discrete: telefon — un layout, tabletă — altul, desktop — al treilea.
Apple a introdus Size Classes în iOS 8 (2014) împreună cu aplicațiile universale (universal binaries, care funcționează atât pe iPhone, cât și pe iPad). Google a introdus calificatorul smallestWidth (sw
Recomandările moderne Apple HIG și Google Material Design sunt de acord că adaptive layout este obligatoriu pentru aplicațiile care acceptă mai multe dimensiuni de ecran. Google Play marchează lipsa adaptării pentru tablete ca un defect. App Store nu are o cerință formală, dar utilizatorii de iPad se așteaptă la o experiență nativă, nu la o aplicație iPhone întinsă.
Size Classes — caracteristica iOS care definește spațiul disponibil ca Compact sau Regular pe fiecare axă. UITraitCollection — sistemul iOS pentru transmiterea caracteristicilor de mediu prin ierarhia view: dimensiune, scară ecran, force touch capability, gamă de culori (light/dark), accessibility settings.
| Dispozitiv / Orientare | Horizontal | Vertical |
|---|---|---|
| iPhone portret (toate modelele) | Compact | Regular |
| iPhone Plus/Pro Max peisaj | Regular | Compact |
| iPhone SE / mini peisaj | Compact | Compact |
| iPad portret | Regular | Regular |
| iPad peisaj | Regular | Regular |
| iPad Split View (1/3 ecran) | Compact | Regular |
UITraitCollection este format de sistem și transmis de la UIApplication prin UIWindow la fiecare UIView. La schimbarea orientării sau a dimensiunii ferestrei (Split View), sistemul generează un nou UITraitCollection și apelează traitCollectionDidChange. În SwiftUI, modificările sunt urmărite prin Environment Values și redesenează automat view-ul.
Adaptarea prin UITraitCollection în UIKit se implementează prin abonarea la modificările trait collection și reconstruirea layout-ului. Exemplul de mai jos arată un controler care comută între un stack vertical pentru iPhone și un layout split pentru iPad.
// Controler adaptiv cu 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: bară laterală + conținut
} else {
showCompactLayout() // iPhone: bară de file + push
}
}
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
}
}Metoda traitCollectionDidChange este apelată la orice modificare a traits, dar verificăm doar horizontalSizeClass — cel mai semnificativ indicator pentru schimbarea machetei. În iOS 17+ se recomandă utilizarea UIViewController.horizontalSizeClass ca proprietate calculată pentru actualizare reactivă fără abonare la modificări.
Android oferă doi calificatori principali de lățime: sw
| Calificator | Condiție | Exemplu dispozitiv |
|---|---|---|
| layout-sw600dp | Lățime minimă ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Lățime curentă ≥ 600 dp | iPad în Split View (600 dp), telefon în peisaj |
| layout-land | Orientare peisaj | Orice dispozitiv rotit orizontal |
| layout-sw720dp | Lățime minimă ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Ierarhia resurselor în Android: res/layout/activity_main.xml (telefon), res/layout-sw600dp/activity_main.xml (tabletă 7"), res/layout-w600dp-land/activity_main.xml (telefon în peisaj). Android alege cel mai specific calificator care se potrivește configurației curente a dispozitivului. În lipsa unui fișier potrivit, se folosește resursa de bază din res/layout/.
Dezvoltarea modernă Android folosește Jetpack WindowManager pentru a determina dimensiunea ferestrei în loc să citească direct resursele. Acest lucru este deosebit de important pentru dispozitivele pliabile, unde dimensiunea ecranului se poate schimba dinamic (Galaxy Fold se desface de la 6,2" la 7,6").
// Adaptare prin 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("Telefon", 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()
}
}Componenta currentWindowAdaptiveInfo() din biblioteca androidx.window adaptează interfața la orice dispozitiv: telefon, tabletă, pliabil, ChromeOS. Clasele de lățime: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Clasele de înălțime: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI oferă instrumente încorporate pentru layout adaptiv fără utilizarea directă a Size Classes. AnyLayout, ViewThatFits și GeometryReader permit construirea de interfețe care se adaptează automat la spațiul disponibil.
// Adaptare SwiftUI prin AnyLayout și 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("Favorite", systemImage: "star")
Label("Recent", systemImage: "clock")
Label("Setări", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — selecție automată a layout-ului
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Prioritate 1: orizontal
CardView()
CardView()
}
VStack { // Dacă nu încape — vertical
CardView()
CardView()
}
}
}
}ViewThatFits — un instrument puternic SwiftUI care selectează automat primul view copil care încape în spațiul disponibil fără tăiere. Acest lucru elimină necesitatea de a verifica explicit Size Classes pentru comutări adaptive simple. AnyLayout permite schimbarea tipului de layout (HStack/VStack) fără if-else în corpul view-ului.
Jetpack Compose folosește WindowSizeClass (din biblioteca Material 3 adaptive) și BoxWithConstraints pentru layout adaptiv. Spre deosebire de calificatorii XML, Compose determină dimensiunea ferestrei în runtime și recompune UI reactiv la modificarea configurației.
// Adaptare Jetpack Compose prin BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tabletă: două coloane (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 {
// Telefon: o coloană cu navigare
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 oferă componente gata făcute: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Aceste componente se adaptează automat la WindowWidthSizeClass — comută între stacked (COMPACT) și side-by-side (MEDIUM/EXPANDED). Dezvoltatorul trebuie doar să selecteze modelul și să transmită panourile de conținut.
Întrebări frecvente
sw600dp (smallestWidth) — lățimea minimă a ecranului independent de orientare. Se activează întotdeauna pe tabletele 7"+. w600dp (available width) — lățimea disponibilă ținând cont de orientarea curentă. Se activează pe telefoanele în peisaj, unde lățimea poate depăși 600 dp.
UITraitCollection — obiect iOS care conține caracteristicile mediului: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Este transmis de la UIApplication prin ierarhia view și se modifică la schimbarea orientării sau a dimensiunii ferestrei.
Folosește Jetpack WindowManager (Android) și UIScreen nativeBounds + traitCollection (iOS). Dispozitivele pliabile schimbă Size Class la desfacere. Se recomandă o arhitectură bazată pe layout-uri canonice (list-detail, supporting pane) cu abonare reactivă la modificările dimensiunii ferestrei fără repornirea Activity.
Nu. Folosește un singur storyboard cu variații Size Classes de constrângeri sau layout programatic prin UIKit. SwiftUI nu necesită deloc storyboards. Storyboards separate pentru iPad creează duplicare și complică întreținerea — este mai bine să adaptezi o singură machetă prin Size Classes.
Canonical layouts — modele gata făcute de layout adaptiv de la Google: list-detail (listă + detalii), supporting pane (conținut + panou de acțiuni), feed (carduri). Fiecare model suportă trei stări (compact/medium/expanded) și este implementat în biblioteca Material 3 Adaptive prin ListDetailScaffold și SupportingPaneScaffold.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și