Разработка
Один «подводный камень» в Jetpack Compose и как его можно исправить
В этой небольшой статье я расскажу об одном подводном камне, с которым вы вполне можете столкнуться при использовании Jetpack Compose.
В этой небольшой статье я расскажу об одном подводном камне, с которым вы вполне можете столкнуться при использовании Jetpack Compose. Я также объясню, как его обойти. Это связано с распространением касаний, макетами и Поверхностью в composable.
Основа
Стандартные встроенные Макеты
Jetpack Compose предлагает три основных встроенных “материальных” компонента компоновки — Row, Column и Box.
- Row — располагает дочерние элементы в ряд.
- Column — Располагает дочерние элементы в колонке.
- Box — располагает дочерние элементы друг над другом.
Существуют и другие лэйауты, такие как LazyRow и LazyColumn. Ловушка, которую мы рассмотрим, применима ко всем встроенным макетам и даже к пользовательским composable. Поскольку стандартные встроенные компоновки обычно используются как строительные блоки для других компоновок, я взял их в качестве примера.
Компонент Surface
Композабл Surface используется для отображения «материальной поверхности» на экране. Он заботится об обрезании, возвышении, границах, установке правильного цвета фона и содержимого, а также блокирует распространение касаний за поверхностью. Внимание, спойлер! Последнее как раз относится к подводным камням, так что имейте это в виду 😉. Вот документация по Surface composable.
Подводный камень
Предположим, что вы создаете клон приложения Spotify в качестве побочного проекта и хотите добавить функциональность, при которой нажатие на мини-плеер будет разворачивать его на весь экран, чтобы он отображался поверх содержимого всего приложения. Реализация этого может показаться очень простой, не так ли? Мы можем просто использовать Box, поместить содержимое приложения перед мини-плеером и вуаля! Код для этого может выглядеть примерно так.
@Composable fun App() { Box(modifier = Modifier.fillMaxSize()) { AppContent(...) . . ExpandableMiniPlayer( modifier = Modifier.align(Alignment.BottomCenter) . . ) } }
В приведенном выше рисунке, кажется, что все нормально и, безусловно, работает. Но вы заметите очень странное поведение, если начнете касаться фона полноэкранного музыкального проигрывателя, когда он отображается над списком элементов, которые можно касаться. Элементы за плеером получают события касания, даже если плеер нарисован над списком элементов! Если это список треков, то выбранный трек начинает играть, даже если плеер нарисован перед ним!
На рисунке выше обратите внимание, как я могу прокручивать список треков, отображаемый позади, и менять текущий воспроизводимый трек, касаясь фона полноэкранного проигрывателя, несмотря на то, что полноэкранный проигрыватель наложен поверх списка. Странно, не правда ли?! Позвольте мне объяснить, что происходит.
Объяснение
Это происходит потому, что ни один из компонуемых макетов не использует Surface composable под капотом. Они напрямую используют Макеты. Это означает, что они по сути «прозрачны», даже если применяется цвет фона. Это означает, что все события касания проходят «сквозь» них, и именно поэтому элементы за музыкальным проигрывателем реагируют на события касания, даже если они перекрыты экраном музыкального проигрывателя. Чтобы еще больше убедиться в этом, давайте рассмотрим пример. В приведенном ниже фрагменте кода я разместил Box composable над кнопкой. Я также добавил фоновый градиент к Box composable, расположенному поверх кнопки.
@Composable fun Example() { val brush = remember { Brush.verticalGradient( listOf( Color.Red.copy(alpha = 0.5f), Color.Transparent ) ) } Box(modifier = Modifier.fillMaxSize()) { Button( modifier = Modifier.align(Alignment.Center), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Black), onClick = { Log.d("MainActivity", "Clicked!") }, content = { Text("Button") } ) // box that will appear on top of the button Box( modifier = Modifier .fillMaxSize() .background(brush) ) } }
Если вы заметили на рисунке выше, кнопка является кликабельной, несмотря на то, что поверх нее размещен целый композит с градиентным цветом. Это ясно показывает нам, что composable, которые напрямую используют Макеты (Layout) под капотом, без Surface, по сути «прозрачны» и позволяют всем событиям касания проходить через них. Давайте вернемся к нашему клону Spotify и исправим проблему.
Исправление
Проблема кроется в composable ExpandableMiniPlayer. В частности, она заключается в реализации composable FullScreenPlayer, который ExpandableMiniPlayer использует под капотом. FullScreenPlayer напрямую использует composable Column. Поскольку Column основан на Layout, он просто выстраивает свои дочерние элементы в столбец без «сплошного» фона, даже если применяется цвет фона.
@Composable fun ExpandableMiniPlayer(...) { AnimatedContent(...) { isFullScreenPlayerVisible -> if (isFullScreenPlayerVisible) { FullScreenPlayer(...) } else { Miniplayer(...) } } } // issue lies in this composable private fun FullScreenPlayer() { Column( modifier = Modifier .dynamicBackgroundColor(...) // custom modifier .fillMaxSize() .systemBarsPadding() .padding(start = 16.dp, end = 16.dp) ) {...} }
Это означает, что когда композит накладывается на другой композит, он просто распространяет все события прикосновения на композит за ним, поскольку у него нет барьера, который не позволяет событию прикосновения распространяться дальше. Теперь, когда мы поняли, почему это происходит, давайте исправим это. Исправление довольно простое. Просто поместите содержимое FullScreenPlayer в Surface. Вот и все!
private fun FullScreenPlayer() { // surround the content with the Surface composable Surface{ Column( modifier = Modifier .dynamicBackgroundColor(...) // custom modifier .fillMaxSize() .systemBarsPadding() .padding(start = 16.dp, end = 16.dp) ) { ... } } }
Поскольку Surface действует как «сплошной» фон для макетов, событие касания не передается на композит под FullScreenPlayer. Это также имеет метафорический смысл. Компонуемый Макет отвечает только за компоновку своих дочерних элементов. Композитная поверхность обеспечивает «поверхность» или фон для компоновки. Это также согласуется с документацией по композиту Surface, где упоминается, что он отвечает за блокирование событий касания (см. скриншот документации выше).
Заключение
Надеюсь, эта статья была вам полезна. Как всегда, я хотел бы поблагодарить вас за то, что нашли время прочитать эту статью😊. Желаю вам удачи! Счастливого кодинга. Создавайте потрясающие приложения для Android. Ура!