Горел сентябрь 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!

Комментарии (18)


  1. nafgne
    03.08.2018 10:34
    +1

    Я пытался придумать более дурацкую причину фрагментации, но не смог.
    Неужели нельзя было по умолчанию показывать старые приложения без использования ограниченной части экрана?


    1. smartdev Автор
      03.08.2018 11:27

      Да, глупее пока сложно что-то придумать
      По сути, если приложение не лезет под статус бар, то оно по умолчанию будет без ограниченной части верстаться
      В основном зависит от windowActionBarOverlay флага, и других, типа windowActionBar, windowNoTitle, windowDrawsSystemBarBackgrounds, windowTranslucentStatus, windowIsTranslucent и их комбинаций
      т.е дефолтное приложение без заморочек сверстается адекватно


    1. Tihon_V
      03.08.2018 11:53

      На Huawei/Honor при открытии приложения — открывается запрос можно ли использовать весь экран, однако в некоторых приложениях (Microsoft RDP Client) без разрешения — скругляются углы.


    1. vlad_august
      03.08.2018 15:05

      на oneplus 6 так и есть. По умолчанию чёлка скрыта у всех приложений.


  1. saege5b
    03.08.2018 11:16

    Я прошу прощения, а зачем нужна это 'чёлка' и есть ли в ней жизненная необходимость?


    1. smartdev Автор
      03.08.2018 11:33

      В данном случае я привожу примеры программной чёлки, она нужна только для того, чтобы понять, где могут быть проблемы на реальных девайсах с чёлкой.
      На реальных девайсах же она будет аппаратная и в ней будет скрываться камера и разные датчики, т.е. там уже будет действительно недоступная зона экрана.
      Есть ли в ней жизненная необходимость производители смартфонов уже решили и практически каждый новый анонсированный девайс ей оснащён. Пользоваться такими устройствами или нет — Ваш выбор. А вот поддерживать её в своём приложении или нет — думаю нет выбора, т.к. если где-то из-за неё едет вёрстка, то однозначно придётся.


    1. hdfan2
      03.08.2018 11:37

      Карго-культ, как он есть. Если вы сделаете на своём смартфоне чёлку, то ваша компания сразу подорожает до триллиона долларов.


    1. DareDen
      03.08.2018 11:38

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


    1. Javian
      03.08.2018 14:13

      «Дизайнеры победили инженеров».
      P.S. Надеюсь эта идея финансово провалится и больше не будет таких вырезаний какой-то части экрана.


  1. Revertis
    03.08.2018 13:37

    «Поясняем за чёлку»

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


    1. uploadfor
      03.08.2018 13:55
      -2

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




      Вероятно, одна из причин того, что «карма потихоньку течёт» — это когда человек с явно нерусским ником типа Revertis даже находясь в Словакии не перестаёт поучать остальных русскому языку ;)


      1. nafgne
        03.08.2018 15:06

        Да нет, не совсем так. Карма "течёт" потому что это единственный способ заставить людей писать статьи, и администрации выгодно поддерживать атмосферу всеобщей грызни.
        Не удивлюсь, если половина минусов вообще фиктивная.


    1. ClearAirTurbulence
      03.08.2018 14:07
      +1

      Это шутка такая, про гопников и неформалов, так что эта конструкция вполне оправданна.


      1. Revertis
        03.08.2018 15:26

        Просто совсем не хочется, чтобы ИТ-сообщество превращалось в гопников и неформалов.


        1. dunsky
          03.08.2018 20:56
          +1

          Такова жизнь, братан…


  1. Sinii
    03.08.2018 17:42
    +1

    Вопрос не совсем про челку, но сам столкнулся с проблемой что при выставленном флаге setFitsSystemWindows = false ломается работа adjustResize
    Гугл о баге знает с 2009 года — issuetracker.google.com/issues/36911528
    Вы как-нибудь решали эту проблему?


    1. smartdev Автор
      03.08.2018 17:46

      Да, встречались с такой проблемой. Написали свой workaround, который слушал onGlobalLayout через ViewTreeObserver.OnGlobalLayoutListener и высчитывали высоту, которую можно использовать для верстки через getWindowVisibleDisplayFrame. И после расчёта прокидывали в качестве callback'а новую высоту и разницу в высотах всем слушателям. А они уже в свою очередь перерисовывались. Надеюсь будет полезно :)


      1. Sinii
        03.08.2018 17:53

        Спасибо за ответ! Некоторое время такой фикс и у меня работал. Но, к сожалению, в таком способе стал сталкиваться с проблемой что на некоторых устройствах Samsung (S5, если это имеет значение) неверно определяется высота статусбара и фикс стал вычислять высоту некорректно.

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