Adaptive Layout: ключови понятия, класове размери и квалификатори

Автор: IT Sectr Публикувано: 2026-02-26 Време за четене: 10 мин

Adaptive Layout — подход за изграждане на интерфейси, при който приложението използва различни макети за различни типове устройства и ориентации на екрана. За разлика от responsive design, adaptive layout превключва между предварително подготвени layout-и в точките на breakpoint. Статията разглежда Size Classes и UITraitCollection в iOS, квалификаторите sw600dp/layout-w600dp в Android и дава практически примери.

Основни точки

  • Adaptive Layout — дискретно превключване на макети в зависимост от размера на екрана, ориентацията и типа устройство
  • Size Classes в iOS класифицират пространството като Compact или Regular хоризонтално и вертикално
  • UITraitCollection — контейнер за характеристики на средата на iOS (размер, мащаб, force touch, цветови гама)
  • sw600dp — Android квалификатор за минимална ширина за таблети (7" и повече)
  • layout-w600dp — квалификатор за налична ширина, който се активира при достатъчно пространство в текущата ориентация

Какво е Adaptive Layout?

Adaptive Layout — стратегия за изграждане на интерфейси, при която разработчикът създава отделни макети за всеки тип устройство и превключва между тях на базата на характеристиките на екрана. За разлика от responsive design, където елементите текат плавно, adaptive layout използва дискретни превключвания: телефон — един layout, таблет — друг, десктоп — трети.

Apple въведе Size Classes в iOS 8 (2014) заедно с универсални приложения (universal binaries, работещи и на iPhone, и на iPad). Google представи квалификатора smallestWidth (swdp) в Android 3.2 (2011). И двата механизма решават един и същи проблем: да дадат на разработчика инструмент за създаване на различни UI за различни екрани без дублиране на код.

Съвременните препоръки на Apple HIG и Google Material Design се съгласяват, че adaptive layout е задължителен за приложения, поддържащи повече от един размер екран. Google Play маркира липсата на адаптация за таблети като недостатък. App Store няма формално изискване, но потребителите на iPad очакват native изживяване, а не разтегнато iPhone приложение.

Size Classes и UITraitCollection в iOS

Size Classes — характеристика на iOS, определяща наличното пространство като Compact или Regular по всяка ос. UITraitCollection — система на iOS за предаване на характеристики на средата през йерархията на view: размер, мащаб на екрана, force touch capability, цветови гама (light/dark), accessibility settings.

Устройство / ОриентацияHorizontalVertical
iPhone портрет (всички модели)CompactRegular
iPhone Plus/Pro Max пейзажRegularCompact
iPhone SE / mini пейзажCompactCompact
iPad портретRegularRegular
iPad пейзажRegularRegular
iPad Split View (1/3 екран)CompactRegular

UITraitCollection се формира от системата и се предава от UIApplication чрез UIWindow до всеки UIView. При промяна на ориентацията или размера на прозореца (Split View), системата генерира нов UITraitCollection и извиква traitCollectionDidChange. В SwiftUI промените се проследяват чрез Environment Values и автоматично прерисуват view.

Пример за адаптивен layout в Swift

Адаптацията чрез UITraitCollection в UIKit се реализира чрез абониране за промени на trait collection и преизграждане на layout. Примерът по-долу показва контролер, който превключва между вертикален стек за iPhone и split макет за iPad.

swift
// Адаптивен контролер с 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 квалификатори: sw600dp и w600dp

Android предоставя два основни квалификатора за ширина: swdp (smallestWidth) и wdp (available width). Разликата е критична: sw — минимална ширина на екрана независимо от ориентацията, w — налична ширина, отчитаща текущата ориентация. sw гарантира, че layout ще се зареди за устройство, което физически има посочената минимална ширина. w реагира на завъртане — в пейзажен режим може да активира w600dp, дори ако в портрет ширината е 360 dp.

КвалификаторУсловиеПример за устройство
layout-sw600dpМинимална ширина ≥ 600 dpiPad (768 dp), Pixel C (900 dp)
layout-w600dpТекуща ширина ≥ 600 dpiPad в Split View (600 dp), телефон в пейзаж
layout-landПейзажна ориентацияВсяко устройство, завъртяно хоризонтално
layout-sw720dpМинимална ширина ≥ 720 dpiPad 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/.

Пример за адаптивен layout в Kotlin

Съвременната разработка за Android използва Jetpack WindowManager за определяне размера на прозореца вместо директно четене на ресурси. Това е особено важно за сгъваеми устройства, където размерът на екрана може да се променя динамично (Galaxy Fold се разгъва от 6,2" на 7,6").

kotlin
// Адаптация чрез 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).

Adaptive Layout в SwiftUI

SwiftUI предоставя вградени инструменти за адаптивен layout без директно използване на Size Classes. AnyLayout, ViewThatFits и GeometryReader позволяват изграждане на интерфейси, които автоматично се адаптират към наличното пространство.

swift
// 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.

Adaptive Layout в Jetpack Compose

Jetpack Compose използва WindowSizeClass (от библиотеката Material 3 adaptive) и BoxWithConstraints за адаптивен layout. За разлика от XML квалификаторите, Compose определя размера на прозореца по време на изпълнение и реактивно прекомпозира UI при промяна на конфигурацията.

kotlin
// 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 и w600dp?

sw600dp (smallestWidth) — минимална ширина на екрана независимо от ориентацията. Винаги работи на таблети 7"+. w600dp (available width) — налична ширина, отчитаща текущата ориентация. Работи на телефони в пейзаж, където ширината може да надвиши 600 dp.

Какво е UITraitCollection в iOS?

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.

Трябва ли да създавам отделни storyboards за iPad?

Не. Използвайте един storyboard с Size Classes вариации на constraint или програмен layout чрез UIKit. SwiftUI изобщо не изисква storyboards. Отделни storyboards за iPad създават дублиране и усложняват поддръжката — по-добре е да адаптирате един макет чрез Size Classes.

Какво са canonical layouts в Material 3?

Canonical layouts — готови модели за адаптивен layout от Google: list-detail (списък + детайли), supporting pane (съдържание + панел за действия), feed (карти). Всеки модел поддържа три състояния (compact/medium/expanded) и е имплементиран в библиотеката Material 3 Adaptive чрез ListDetailScaffold и SupportingPaneScaffold.

Резюме

  • Adaptive Layout — дискретно превключване на макети в точки на breakpoint за различни устройства
  • Size Classes iOS (Compact/Regular) определят layout за iPhone, iPad и Split View
  • UITraitCollection — механизъм за предаване на характеристики на средата през йерархията на view
  • sw600dp — Android квалификатор за минимална ширина за таблети 7"+
  • w600dp — квалификатор за налична ширина, активиращ се при завъртане в пейзаж
  • Jetpack WindowManager предоставя WindowSizeClass (WindowWidthSizeClass: COMPACT/MEDIUM/EXPANDED)
  • Material 3 Adaptive — готови компоненти с автоматична адаптация (ListDetailScaffold)

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също