Разработка приложения под все размеры экрана

Из этой статьи вы узнаете, как оптимизировать приложение, чтобы оно одинаково хорошо выглядело на экранах любого размера. Это касается и телефона, и планшета, и даже настольного компьютера (Chrome OS). Но для начала выясним, почему стоит подумать об оптимизации приложения для больших экранов. 

Зачем поддерживать большие размеры экрана?

Android работает не только на смартфонах, но и на планшетах, настольных компьютерах (Chrome OS), складных и автомобильных устройствах, а также на телевизорах Smart TV, насчитывающих более трех миллиардов активных девайсов. Мы, разработчики, должны обеспечивать бесперебойную работу приложений на каждой платформе. Взять хотя бы такую актуальную тему, как складные устройства. Каждый бренд смартфонов сегодня выпускает свою версию складных устройств, которые динамически меняют размер экрана, превращаясь из телефона в дисплей размером с планшет.

Как поддерживать большие размеры экрана?

Чтобы поддерживать все размеры экрана, необходимо создать адаптивный макет, изменяющий размер в зависимости от размера экрана. В Android это можно сделать с помощью классов размера окна (window-size-classes). Они обеспечивают высокоуровневую абстракцию размера экрана, предоставляя три области отображения — compact (компактную), medium (среднюю) и expanded (расширенную). Это упрощает принятие решений по дизайну UI в зависимости от размера экрана. Например, в классе compact, который используется для отображения Navigation Bar (нижней панели навигации) или Modal Navigation Drawer (бокового меню), есть 2 типа размера:

  1. высота (height);
  1. ширина (width).

Обычно ширина важнее для UI, чем высота, потому что большая часть контента в приложении прокручивается вертикально.

Классы window size

Чтобы получить текущий размер окна, добавьте одну зависимость в libs.versions.toml:

[versions]
material3AdaptiveNavigationSuite = "1.3.0-beta04"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavigationSuite" }

И определите ее в build.gradle.kts(module:app):

dependencies {

// NavigationSuiteScaffold
implementation(libs.androidx.material3.adaptive.navigation.suite)
}

Примечание: в настоящее время библиотека Material Adaptive находится в стадии бета-версии. Ознакомьтесь с последними версиями здесь.

Добавление этой зависимости дает два преимущества:

  1. Доступ как к высоте, так и к ширине класса с помощью функции currentWindowAdaptiveInfo().
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass

Это позволяет принимать решения по UI-дизайну.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun App(
widthSizeClass: WindowWidthSizeClass
) {
// Выполните логические операции над классом размера, чтобы решить, показывать ли узкое боковое меню или нет.
val isExpanded = windowSizeClass == WindowWidthSizeClass.EXPANDED

Row {
if (isExpanded) {
NavigationRail {
NavigationRailItem(
/* ... */
)
}
}
DefaultNavGraph(/* ... */)
}
}

2. Доступ к NavigationSuiteScaffold, который упрощает логику принятия решения о навигации по UI в зависимости от размера экрана.

Navigation Bar (нижняя панель навигации) показывается, если размер окна находится в компактной или настольной (горизонтальной) позиции. В остальных случаях показывается Navigation Rail (узкое боковое меню).

Как разработать оптимальный макет навигации?

В соответствии с рекомендациями Material Design, при разработке макета следует ориентироваться на типичные сценарии использования классов window-size:

  • Compact (ширина < 600 dp): Navigation Bar, Modal Navigation Drawer;
  • Medium (600 dp ≤ ширина < 840 dp): Navigation Rail, Modal Navigation Drawer;
  • Expanded (840 ≤ ширина < 1200*): Navigation Rail, Modal или Standard Navigation Drawer (для реализации шаблона «list-detail» рекомендуется разработать макет с 2 окнами с помощью ListDetailPaneScaffold).

Примечание: ListDetailPaneScaffold — это библиотека Material Adaptive, которая в настоящее время находится в альфа-версии и также пока не поддерживает Compose Navigation. Для создания приложений, основанных на представлении, используйте SlidingPaneLayout.

Реальная демонстрация

Как уже упоминалось, использование NavigationSuiteScaffold упрощает жизнь разработчику. Ему не нужно самому писать логику, основанную на классах window-size. Это выполняется «из коробки». Перейдем к демонстрации и посмотрим, как вручную отобразить макет навигации на основе классов window-size.

Что будем создавать?

Демонстрация приложения NavigationSuiteScaffold 

Не сосредотачиваясь на контенте окна, просто покажем текст, который отображает текущее положение. Наша цель — отобразить навигационный макет в зависимости от размера окна.

Перейдем к реализации.

Поскольку необходимая зависимость — navigation-suite — уже добавлена из библиотеки Material 3 Adaptive, сосредоточимся на коде.

Для высокоуровневых позиций добавим класс enum, как показано в приведенном ниже коде:

enum class AppDestinations(
@StringRes val label: Int,
val icon: ImageVector,
@StringRes val contentDescription: Int
) {
HOME(R.string.home, Icons.Default.Home, R.string.home),
DRAWING(R.string.draw, Icons.Default.Draw, R.string.draw),
EDIT(R.string.edit, Icons.Default.Edit, R.string.edit),
SETTINGS(R.string.profile, Icons.Default.Person, R.string.settings),
}

Вызовем NavigationSuiteScaffold и передадим необходимые аргументы.

@Composable
fun App(modifier: Modifier = Modifier) {

var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) }
val adaptiveInfo = currentWindowAdaptiveInfo()

/* Опционально */
val customNavSuiteType = with(adaptiveInfo) {
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
NavigationSuiteType.NavigationRail
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
}
}

NavigationSuiteScaffold(
layoutType = customNavSuiteType, // only for further customization ourselves
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationBarContainerColor = Color.Transparent,
),
navigationSuiteItems = {
AppDestinations.entries.forEach {
item(
icon = {
Icon(
it.icon,
contentDescription = stringResource(it.contentDescription)
)
},
label = { Text(stringResource(it.label)) },
selected = it == currentDestination,
onClick = { currentDestination = it }
)
}
}
) {
// Destination content.
when (currentDestination) {
AppDestinations.HOME -> /* Home screen content goes here */
AppDestinations.DRAWING -> /* Drawing screen content goes here */
AppDestinations.EDIT -> /* Edit screen content goes here */
AppDestinations.SETTINGS -> /* Settings screen content goes here */
}
}
}

Как видно из кода, при использовании NavigationSuiteScaffold реализовать навигационный макет на основе класса window-size очень просто: достаточно одного условного оператора для класса window-size.

При желании можно дополнительно настроить его. Если хотите отображать другие макеты навигации на основе размера окна, можете сделать это, передав еще один параметр в NavigationSuiteScaffold с именем layoutType.

val adaptiveInfo = currentWindowAdaptiveInfo()

/* Опционально */
val customNavSuiteType = with(adaptiveInfo) {
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
NavigationSuiteType.NavigationRail
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
}
}

Теперь настраиваем навигационный макет на основе window-size. Если мы в данный момент находимся на экране EXPANDED, то, вместо стандартного компонента Navigation Bar, отображаем Navigation Rail. Макет экрана считается EXPANDED, если соответствует нижеперечисленным критериям:

  • телефон в альбомном положении;
  • планшет в альбомном положении;
  • складной смартфон в альбомном положении (в развернутом состоянии);
  • десктопный компьютер (Chrome OS).

Итак, вы узнали, как работать с NavigationSuiteScaffold. Теперь перейдем к более тонко настраиваемым вариантам.

Принятие решений о навигационном макете с помощью классов window-size

Теперь посмотрим, как самостоятельно работать с макетом навигации, основанном на классе window-size.

@Composable
fun OsbApp(
widthSizeClass: WindowWidthSizeClass
) {

val navController = rememberNavController()
val coroutineScope = rememberCoroutineScope()

val navActions = remember {
OsbNavActions(navController)
}

val isExpanded = widthSizeClass == WindowWidthSizeClass.EXPANDED
val drawerState = rememberDrawerState(DrawerValue.Closed)

val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = backStackEntry?.destination?.route ?: HOME_ROUTE

ModalNavigationDrawer(
drawerContent = {
/* Контент меню */
}, gesturesEnabled = !isExpanded, // Не позволяйте слайду открывать боковое меню на экранах EXPANDED
drawerState = drawerState
) { // Content
Row {
if (isExpanded) {
AppNavRail(
currentScreen = currentScreen,
navigateToHome = {
navController.navigate(OsbDestination.HOME_DESTINATION) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}, navigateToSettings = {
navController.navigate(OsbDestination.SETTINGS_DESTINATION) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
})
}
OsbNavGraph(navController = navController, onDrawerClicked = {
coroutineScope.launch {
drawerState.apply {
if (isExpanded) {
close()
} else open() // разрешать пользователям боковое меню, только
// если они находятся не в режиме EXPANDED, в противном случае закрывать, так как открыто узкое боковое меню
}
}
})
}
}
}

Как можно видеть в коде, мы условно показываем Navigation Rail в зависимости от размера окна, используя:

val isExpanded = widthSizeClass == WindowWidthSizeClass.EXPANDED

Вызовем эту функцию в файле MainActivity.kt:

YourAppTheme{
val windowWidthSizeClass = currentWindowAdaptiveInfo().windowSizeClass
OsbApp(windowWidthSizeClass.windowWidthSizeClass) // здесь используем класс Width Size
}

Запущенное приложение показывает соответствующий навигационный макет, основанный на классе window-size.

Условный показ навигационного макета, основанного на классе window-size

Исходный код демо-версии NavigationSuiteScaffold доступен в этом репозитории: GitHub — MubarakNative/Compose-Code-Examples at navigation-suite-scaffold.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Mubarak Native: Get your android app ready for larger screen sizes using window-size classes on android

Предыдущая статьяКак работает React Fiber Reconciler?
Следующая статья10 наиболее эффективных CLI-инструментов