Горел сентябрь 2007 года. Шёл сентябрь 2017 года, Apple вернули моду на чёлку, представив iPhone X. Неудивительно, что наши друзья из Китая, недолго думая, скопировали этот дизайн у Apple (хотя самая первая мини-чёлка была ещё в Essential Phone, который не взлетел). Но что мы видим сейчас? Huawei P20, Asus Zenfone 5, OnePlus 6, Motorola One Power, Xiaomi Redmi 6 и другие более-менее известные производители уже выпускают или анонсировали телефоны с чёлкой. Samsung и Google остались последними оплотами в этой гонке за хайпом борьбе за безрамочность. Или нет? По слухам, Google Pixel 3 XL тоже будет с этой хренью с изящным вырезом. Что ж, нам, как разработчикам, остаётся только оптимизировать свои приложения под этот вырез, чтобы пользователи смогли продолжать комфортно ими пользоваться. За подробностями прошу под кат.
Для начала нам необходимо разобраться, нужна ли вообще оптимизация приложению?
Если у вас fullscreen-приложение или в теме присутствуют windowActionBarOverlay = true
, то с большой вероятностью нужна.
Практически все приложения состоят далеко не из одного экрана, и можно не заметить, как на одном из них поедет вёрстка. Особенно если в приложении объёмный legacy code. Поэтому стоит всё-таки пройтись по всем основным экранам и перепроверить. Давайте разберёмся, что для этого нужно сделать.
1. Подготовить тестовый девайс/эмулятор
Для того чтобы протестировать ваше приложение с чёлкой, нужна (спасибо, кэп!) Android P. В данный момент доступна версия Android P Preview 5 для следующих устройств (спасибо Project Treble):
Essential Phone;
Google Pixel 2;
Google Pixel 2 XL;
Google Pixel;
Google Pixel XL;
Nokia 7 plus;
OnePlus 6;
Oppo R15 Pro;
Sony Xperia XZ2;
Vivo X21UD;
Vivo X21;
Xiaomi Mi Mix 2S.
Чтобы установить Android P на устройство, достаточно перейти сюда и нажать «Получить бета-версию» для вашего устройства. Получать её по воздуху или накатывать самому — выбор за вами. Инструкция на сайте прилагается.
Но если вы не можете или не хотите устанавливать Android P на устройство, то никто не отменял эмулятор. Иструкция по настройке тут.
2. Включить саму чёлку программно (если нет аппаратной)
Тут всё просто: идём в System -> Developer options -> Simulate a display with a cutout.
Здесь на выбор предоставляются 3 варианта:
- Corner
- Double
- Tall
Выглядят они следующим образом:
Corner | Double | Tall |
---|---|---|
3. Пройтись по основным экранам
Само собой, этот кейс у всех будет разный. У кого-то простая логика, у кого-то не очень. Приведу пару примеров экранов с поехавшей вёрсткой, которые я нашёл в нашем приложении.
Explore | Profile |
---|---|
Теперь давайте посмотрим, какие есть способы устранения недостатков вёрстки.
Не повышая compileSdkVersion
Начиная с 20 API, появился класс WindowInsets, который представляет собой объекты Rect, описывающие доступные и недоступные части экрана. Вместе с ними во View появились такие методы, с помощью которых мы можем обрабатывать координаты недоступных частей экрана:
WindowInsets dispatchApplyWindowInsets(WindowInsets);
WindowInsets onApplyWindowInsets(WindowInsets);
void requestApplyInsets();
void setOnApplyWindowInsetsListener(OnApplyWindowInsetsListener);
Подробно о том, как ими пользоваться, тут.
Использовать эти методы можно двумя способами:
а) поставить тег android:fitsSystemWindows="true"
в вёрстке на ваш layout или view;
б) сделать это из кода:
layout.setFitsSystemWindows(true);
layout.requestApplyInsets();
Было | Стало |
---|---|
Повысить compileSdkVersion до версии 28
В ближайшем будущем придётся переходить на эту версию, так почему бы не подготовиться к этому сейчас? Но будьте внимательны, если у вас в проекте есть юнит-тесты (а я надеюсь, они у вас есть), пакет JUnit переехал. Как его подключать, описано тут.
Итак, какие варианты теперь предоставляет нам Android P?
А. У WindowManager.LayoutParams появилось 3 новых флага:
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT — с этим флагом чёлка будет поверх экрана приложения только в режиме portrait, в landscape же будет просто чёрная полоса;
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER — с этим флагом модной чёлки не будет вообще, она сольётся с чёрной полосой;
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES — при использовании этого флага чёлка есть всегда и в любой ориентации.
Как применять?
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
Б. Если же вариант А вам не подходит и нужно учитывать именно расположение злополучного выреза (например, у вас что-то отображается прямо в статус-баре, как сообщения о соединении в Telegram), то в данном случае поможет новый класс DisplayCutout.
Рассмотрим его методы:
- getBoundingRects() возвращает List объектов Rect, каждый из которых обозначает недоступную область экрана;
- getSafeInsetLeft(), getSafeInsetRight(), getSafeInsetTop(), getSafeInsetBottom() возвращают левый, правый, верхний и нижний отступ без выреза в пикселях соответственно.
С ними вы сможете уже сделать всё, на что хватит фантазии. Хотите — двигайте margin
в коде по ним. Хотите — обрабатывайте в OnApplyWindowInsetsListener
и делайте consumeDisplayCutout()
. Возможно, вам нужны более сложные манипуляции. Я приведу простой пример, как обозначить чёлку.
class SampleFragment() : Fragment() {
private lateinit var root: ViewGroup
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.sample_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
root = view.findViewById(R.id.root)
addArrowsToCutout()
}
private fun addArrowsToCutout() {
//Нужно учитывать, что фрагмент должен успеть сделать attach к window, иначе тут будут null'ы
val cutoutList = root.rootWindowInsets?.displayCutout?.boundingRects
cutoutList?.forEach {
addArrow(context!!.getDrawable(R.drawable.left), it.left.toFloat(), it.top + (it.bottom - it.top).toFloat() / 2,
::calculateLeftArrow)
addArrow(context!!.getDrawable(R.drawable.right), it.right.toFloat(), it.top + (it.bottom - it.top).toFloat() / 2,
::calculateRightArrow)
addArrow(context!!.getDrawable(R.drawable.top), it.left + (it.right - it.left).toFloat() / 2, it.top.toFloat(),
::calculateTopArrow)
addArrow(context!!.getDrawable(R.drawable.bottom), it.left + (it.right - it.left).toFloat() / 2, it.bottom.toFloat(),
::calculateBottomArrow)
}
}
private fun addArrow(arrowIcon: Drawable, x: Float, y: Float, calculation: (View, Float, Float) -> Unit) {
val arrowView = ImageView(context)
arrowView.setImageDrawable(arrowIcon)
arrowView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
root.addView(arrowView)
arrowView.post {
calculation(arrowView, x, y)
}
}
private fun calculateLeftArrow(arrowView: View, x: Float, y: Float) {
arrowView.x = x - arrowView.width
arrowView.y = y - arrowView.height / 2
}
private fun calculateRightArrow(arrowView: View, x: Float, y: Float) {
arrowView.x = x
arrowView.y = y - arrowView.height / 2
}
private fun calculateTopArrow(arrowView: View, x: Float, y: Float) {
arrowView.x = x - arrowView.width / 2
arrowView.y = y - arrowView.height
}
private fun calculateBottomArrow(arrowView: View, x: Float, y: Float) {
arrowView.x = x - arrowView.width / 2
arrowView.y = y
}
}
Portrait
Corner | Double | Tall |
---|---|---|
Landscape
Corner |
---|
Double |
Tall |
Итак, как мы видим, чёлка принесёт нам некоторые неудобства и заставит совершить лишние телодвижения/дополнительные манипуляции. В принципе, всё решаемо. Главное, приступить к устранению недостатков вёрстки как можно раньше, чтобы иметь в запасе достаточно времени на подготовку. Удачно вам справиться с правками. Да не сломает Google свой Play!
Автор: Дмитрий Васильев