Adaptive Layout — подход за изграждане на интерфейси, при който приложението използва различни макети за различни типове устройства и ориентации на екрана. За разлика от responsive design, adaptive layout превключва между предварително подготвени layout-и в точките на breakpoint. Статията разглежда Size Classes и UITraitCollection в iOS, квалификаторите sw600dp/layout-w600dp в Android и дава практически примери.
Основни точки
Adaptive Layout — стратегия за изграждане на интерфейси, при която разработчикът създава отделни макети за всеки тип устройство и превключва между тях на базата на характеристиките на екрана. За разлика от responsive design, където елементите текат плавно, adaptive layout използва дискретни превключвания: телефон — един layout, таблет — друг, десктоп — трети.
Apple въведе Size Classes в iOS 8 (2014) заедно с универсални приложения (universal binaries, работещи и на iPhone, и на iPad). Google представи квалификатора smallestWidth (sw
Съвременните препоръки на Apple HIG и Google Material Design се съгласяват, че adaptive layout е задължителен за приложения, поддържащи повече от един размер екран. Google Play маркира липсата на адаптация за таблети като недостатък. App Store няма формално изискване, но потребителите на iPad очакват native изживяване, а не разтегнато iPhone приложение.
Size Classes — характеристика на iOS, определяща наличното пространство като Compact или Regular по всяка ос. UITraitCollection — система на iOS за предаване на характеристики на средата през йерархията на view: размер, мащаб на екрана, force touch capability, цветови гама (light/dark), accessibility settings.
| Устройство / Ориентация | Horizontal | Vertical |
|---|---|---|
| iPhone портрет (всички модели) | Compact | Regular |
| iPhone Plus/Pro Max пейзаж | Regular | Compact |
| iPhone SE / mini пейзаж | Compact | Compact |
| iPad портрет | Regular | Regular |
| iPad пейзаж | Regular | Regular |
| iPad Split View (1/3 екран) | Compact | Regular |
UITraitCollection се формира от системата и се предава от UIApplication чрез UIWindow до всеки UIView. При промяна на ориентацията или размера на прозореца (Split View), системата генерира нов UITraitCollection и извиква traitCollectionDidChange. В SwiftUI промените се проследяват чрез Environment Values и автоматично прерисуват view.
Адаптацията чрез UITraitCollection в UIKit се реализира чрез абониране за промени на trait collection и преизграждане на layout. Примерът по-долу показва контролер, който превключва между вертикален стек за iPhone и split макет за iPad.
// Адаптивен контролер с 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: странична лента + съдържание
} else {
showCompactLayout() // iPhone: лента с раздели + 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
}
}Методът traitCollectionDidChange се извиква при всяка промяна на traits, но ние проверяваме само horizontalSizeClass — най-значимият индикатор за смяна на макета. В iOS 17+ се препоръчва използването на UIViewController.horizontalSizeClass като computed property за реактивно обновяване без абониране за промени.
Android предоставя два основни квалификатора за ширина: sw
| Квалификатор | Условие | Пример за устройство |
|---|---|---|
| layout-sw600dp | Минимална ширина ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Текуща ширина ≥ 600 dp | iPad в Split View (600 dp), телефон в пейзаж |
| layout-land | Пейзажна ориентация | Всяко устройство, завъртяно хоризонтално |
| layout-sw720dp | Минимална ширина ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Йерархия на ресурсите в Android: res/layout/activity_main.xml (телефон), res/layout-sw600dp/activity_main.xml (таблет 7"), res/layout-w600dp-land/activity_main.xml (телефон в пейзаж). Android избира най-специфичния квалификатор, подходящ за текущата конфигурация на устройството. При липса на подходящ файл се използва основният ресурс от res/layout/.
Съвременната разработка за Android използва Jetpack WindowManager за определяне размера на прозореца вместо директно четене на ресурси. Това е особено важно за сгъваеми устройства, където размерът на екрана може да се променя динамично (Galaxy Fold се разгъва от 6,2" на 7,6").
// Адаптация чрез 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("Телефон", 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()
}
}Компонентът currentWindowAdaptiveInfo() от библиотеката androidx.window адаптира интерфейса към всяко устройство: телефон, таблет, сгъваемо, ChromeOS. Класове ширина: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Класове височина: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI предоставя вградени инструменти за адаптивен layout без директно използване на Size Classes. AnyLayout, ViewThatFits и GeometryReader позволяват изграждане на интерфейси, които автоматично се адаптират към наличното пространство.
// SwiftUI адаптация чрез AnyLayout и 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("Любими", systemImage: "star")
Label("Скорошни", systemImage: "clock")
Label("Настройки", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — автоматичен избор на layout
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Приоритет 1: хоризонтален
CardView()
CardView()
}
VStack { // Ако не се побира — вертикален
CardView()
CardView()
}
}
}
}ViewThatFits — мощен инструмент на SwiftUI, който автоматично избира първия дъщерен view, който се побира в наличното пространство без изрязване. Това елиминира необходимостта от изрична проверка на Size Classes за прости адаптивни превключвания. AnyLayout позволява промяна на типа layout (HStack/VStack) без if-else в тялото на view.
Jetpack Compose използва WindowSizeClass (от библиотеката Material 3 adaptive) и BoxWithConstraints за адаптивен layout. За разлика от XML квалификаторите, Compose определя размера на прозореца по време на изпълнение и реактивно прекомпозира UI при промяна на конфигурацията.
// Jetpack Compose адаптация чрез BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Таблет: две колони (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 {
// Телефон: една колона с навигация
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 предоставя готови компоненти: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Тези компоненти автоматично се адаптират към WindowWidthSizeClass — превключват между stacked (COMPACT) и side-by-side (MEDIUM/EXPANDED). Разработчикът трябва само да избере модела и да предаде панелите със съдържание.
Често задавани въпроси
sw600dp (smallestWidth) — минимална ширина на екрана независимо от ориентацията. Винаги работи на таблети 7"+. w600dp (available width) — налична ширина, отчитаща текущата ориентация. Работи на телефони в пейзаж, където ширината може да надвиши 600 dp.
UITraitCollection — iOS обект, съдържащ характеристики на средата: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Предава се от UIApplication през йерархията на view и се променя при смяна на ориентацията или размера на прозореца.
Използвайте Jetpack WindowManager (Android) и UIScreen nativeBounds + traitCollection (iOS). Сгъваемите устройства променят Size Class при разгъване. Препоръчва се архитектура, базирана на канонични layout-и (list-detail, supporting pane) с реактивно абониране за промени в размера на прозореца без рестартиране на Activity.
Не. Използвайте един storyboard с Size Classes вариации на constraint или програмен layout чрез UIKit. SwiftUI изобщо не изисква storyboards. Отделни storyboards за iPad създават дублиране и усложняват поддръжката — по-добре е да адаптирате един макет чрез Size Classes.
Canonical layouts — готови модели за адаптивен layout от Google: list-detail (списък + детайли), supporting pane (съдържание + панел за действия), feed (карти). Всеки модел поддържа три състояния (compact/medium/expanded) и е имплементиран в библиотеката Material 3 Adaptive чрез ListDetailScaffold и SupportingPaneScaffold.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също