«Поясняем за чёлку» в Android P. Что делать с Android Cutout?

Горел сентябрь 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 тоже будет с этой хренью с изящным вырезом. Что ж, нам, как разработчикам, остаётся только оптимизировать свои приложения под этот вырез, чтобы пользователи смогли продолжать комфортно ими пользоваться. За подробностями прошу под кат.

vczaephxhgg4entua42pcfj4rxa.png
Для начала нам необходимо разобраться, нужна ли вообще оптимизация приложению?
Если у вас 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


czoopw-4xddmw5l5f9k9-obslpa.png
Выглядят они следующим образом:

Corner Double Tall
hsx24b9mdq9bf3i1wl50vycxpru.png pqtahjumpjjnulq24ir5deudl0m.png i0hef98an6wlx95vlg71evri_9s.png


3. Пройтись по основным экранам


Само собой, этот кейс у всех будет разный. У кого-то простая логика, у кого-то не очень. Приведу пару примеров экранов с поехавшей вёрсткой, которые я нашёл в нашем приложении.

Explore Profile
br8wnbapotdaxbd9gwevvq7gqgq.png lqthbm55j1rexitpvdjhbwmtvla.png


Теперь давайте посмотрим, какие есть способы устранения недостатков вёрстки.

Не повышая 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();


Было Стало
fw8ene_z6sxphxdncb9yzcxjdu4.png hvmljmblq51npqv32f7o_lcjnj8.png


Повысить compileSdkVersion до версии 28

В ближайшем будущем придётся переходить на эту версию, так почему бы не подготовиться к этому сейчас? Но будьте внимательны, если у вас в проекте есть юнит-тесты (а я надеюсь, они у вас есть), пакет JUnit переехал. Как его подключать, описано тут.

Итак, какие варианты теперь предоставляет нам Android P?

А. У WindowManager.LayoutParams появилось 3 новых флага:


Как применять?

window.attributes.layoutInDisplayCutoutMode =
    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES


Б. Если же вариант А вам не подходит и нужно учитывать именно расположение злополучного выреза (например, у вас что-то отображается прямо в статус-баре, как сообщения о соединении в Telegram), то в данном случае поможет новый класс DisplayCutout.
Рассмотрим его методы:
С ними вы сможете уже сделать всё, на что хватит фантазии. Хотите — двигайте 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
l2knkrhjztwt4-gol5kv3lhl9fc.png okwd9zs9neutlmyxurbsg2dggzc.png zyd8z-0amk-izvnypdvyyhqbpkc.png

Landscape


Corner
bwexii25tezwsfe6zv98rai8xrw.png
Double
a9dpqomfrbf7tp8tuv4oqaii8ac.png
Tall
tccpqj2m8eh2kshlsu_o-q2-kda.png


Итак, как мы видим, чёлка принесёт нам некоторые неудобства и заставит совершить лишние телодвижения/дополнительные манипуляции. В принципе, всё решаемо. Главное, приступить к устранению недостатков вёрстки как можно раньше, чтобы иметь в запасе достаточно времени на подготовку. Удачно вам справиться с правками. Да не сломает Google свой Play!

© Habrahabr.ru