company_banner

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

  • Tutorial
Горел сентябрь 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.
Рассмотрим его методы:

С ними вы сможете уже сделать всё, на что хватит фантазии. Хотите — двигайте 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!
FunCorp
209.41
Разработка развлекательных сервисов
Share post

Comments 18

  • UFO just landed and posted this here
      +1
      Да, глупее пока сложно что-то придумать
      По сути, если приложение не лезет под статус бар, то оно по умолчанию будет без ограниченной части верстаться
      В основном зависит от windowActionBarOverlay флага, и других, типа windowActionBar, windowNoTitle, windowDrawsSystemBarBackgrounds, windowTranslucentStatus, windowIsTranslucent и их комбинаций
      т.е дефолтное приложение без заморочек сверстается адекватно
        0
        На Huawei/Honor при открытии приложения — открывается запрос можно ли использовать весь экран, однако в некоторых приложениях (Microsoft RDP Client) без разрешения — скругляются углы.
          0
          на oneplus 6 так и есть. По умолчанию чёлка скрыта у всех приложений.
          +2
          Я прошу прощения, а зачем нужна это 'чёлка' и есть ли в ней жизненная необходимость?
            0
            В данном случае я привожу примеры программной чёлки, она нужна только для того, чтобы понять, где могут быть проблемы на реальных девайсах с чёлкой.
            На реальных девайсах же она будет аппаратная и в ней будет скрываться камера и разные датчики, т.е. там уже будет действительно недоступная зона экрана.
            Есть ли в ней жизненная необходимость производители смартфонов уже решили и практически каждый новый анонсированный девайс ей оснащён. Пользоваться такими устройствами или нет — Ваш выбор. А вот поддерживать её в своём приложении или нет — думаю нет выбора, т.к. если где-то из-за неё едет вёрстка, то однозначно придётся.
              +7
              Карго-культ, как он есть. Если вы сделаете на своём смартфоне чёлку, то ваша компания сразу подорожает до триллиона долларов.
                +4
                Нет в ней необходимости. Просто мода. Повлиять можем лишь не покупая телефоны с козырьком. Эплофилы будут брать челочный iPhone в любом случае, китайцы копировать его тоже не перестанут, так что повлиять по факту не можем никак :(.
                  0
                  «Дизайнеры победили инженеров».
                  P.S. Надеюсь эта идея финансово провалится и больше не будет таких вырезаний какой-то части экрана.
                  0
                  «Поясняем за чёлку»

                  Перестаньте уже писать это «за» везде. Скучать за, пояснять за… Это не по-русски.
                    +2
                    Перестаньте уже писать это «за» везде. Скучать за, пояснять за… Это не по-русски.




                    Вероятно, одна из причин того, что «карма потихоньку течёт» — это когда человек с явно нерусским ником типа Revertis даже находясь в Словакии не перестаёт поучать остальных русскому языку ;)
                    • UFO just landed and posted this here
                      +4
                      Это шутка такая, про гопников и неформалов, так что эта конструкция вполне оправданна.
                        –1
                        Просто совсем не хочется, чтобы ИТ-сообщество превращалось в гопников и неформалов.
                          +1
                          Такова жизнь, братан…
                      +1
                      Вопрос не совсем про челку, но сам столкнулся с проблемой что при выставленном флаге setFitsSystemWindows = false ломается работа adjustResize
                      Гугл о баге знает с 2009 года — issuetracker.google.com/issues/36911528
                      Вы как-нибудь решали эту проблему?
                        0
                        Да, встречались с такой проблемой. Написали свой workaround, который слушал onGlobalLayout через ViewTreeObserver.OnGlobalLayoutListener и высчитывали высоту, которую можно использовать для верстки через getWindowVisibleDisplayFrame. И после расчёта прокидывали в качестве callback'а новую высоту и разницу в высотах всем слушателям. А они уже в свою очередь перерисовывались. Надеюсь будет полезно :)
                          0
                          Спасибо за ответ! Некоторое время такой фикс и у меня работал. Но, к сожалению, в таком способе стал сталкиваться с проблемой что на некоторых устройствах Samsung (S5, если это имеет значение) неверно определяется высота статусбара и фикс стал вычислять высоту некорректно.

                          В итоге получается что такой способ уж очень привязан к устройству и нет гарантии что на каком-нибудь китайце все опять не поедет)

                      Only users with full accounts can post comments. Log in, please.