- PVSM.RU - https://www.pvsm.ru -

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

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

Для начала нам необходимо разобраться, нужна ли вообще оптимизация приложению?
Если у вас 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 на устройство, достаточно перейти сюда [1] и нажать «Получить бета-версию» для вашего устройства. Получать её по воздуху или накатывать самому — выбор за вами. Инструкция на сайте прилагается.
Но если вы не можете или не хотите устанавливать Android P на устройство, то никто не отменял эмулятор. Иструкция по настройке тут [2].

2. Включить саму чёлку программно (если нет аппаратной)

Тут всё просто: идём в System -> Developer options -> Simulate a display with a cutout.
Здесь на выбор предоставляются 3 варианта:

  • Corner
  • Double
  • Tall

«Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 2
Выглядят они следующим образом:

Corner Double Tall
«Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 3 «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 4 «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 5

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

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

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

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

Не повышая compileSdkVersion

Начиная с 20 API, появился класс WindowInsets [3], который представляет собой объекты Rect [4], описывающие доступные и недоступные части экрана. Вместе с ними во View появились такие методы, с помощью которых мы можем обрабатывать координаты недоступных частей экрана:

WindowInsets dispatchApplyWindowInsets(WindowInsets);
WindowInsets onApplyWindowInsets(WindowInsets);
void requestApplyInsets();
void setOnApplyWindowInsetsListener(OnApplyWindowInsetsListener);

Подробно о том, как ими пользоваться, тут [5].

Использовать эти методы можно двумя способами:
а) поставить тег android:fitsSystemWindows="true" в вёрстке на ваш layout или view;
б) сделать это из кода:

layout.setFitsSystemWindows(true);
layout.requestApplyInsets();

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

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

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

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

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

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT [7] — с этим флагом чёлка будет поверх экрана приложения только в режиме portrait, в landscape же будет просто чёрная полоса;
    «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 10
    «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 11
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER [8] — с этим флагом модной чёлки не будет вообще, она сольётся с чёрной полосой;
    «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 12
    «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 13
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES [9] — при использовании этого флага чёлка есть всегда и в любой ориентации.
    «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 14
    «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 15

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

window.attributes.layoutInDisplayCutoutMode =
    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

Б. Если же вариант А вам не подходит и нужно учитывать именно расположение злополучного выреза (например, у вас что-то отображается прямо в статус-баре, как сообщения о соединении в Telegram), то в данном случае поможет новый класс DisplayCutout [10].
Рассмотрим его методы:

С ними вы сможете уже сделать всё, на что хватит фантазии. Хотите — двигайте 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
«Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 16 «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 17 «Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 18

Landscape

Corner
«Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 19
Double
«Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 20
Tall
«Поясняем за чёлку» в Android P. Что делать с Android Cutout? - 21

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

Автор: Дмитрий Васильев

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/288095

Ссылки в тексте:

[1] сюда: https://developer.android.com/preview/devices/

[2] тут: https://developer.android.com/preview/get

[3] WindowInsets: https://developer.android.com/reference/android/view/WindowInsets

[4] Rect: https://developer.android.com/reference/android/graphics/Rect

[5] тут: https://medium.com/@azizbekian/windowinsets-24e241d4afb9

[6] тут: https://developer.android.com/preview/features/legacy-testing-libs

[7] LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT: https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT

[8] LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER: https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER

[9] LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES: https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

[10] DisplayCutout: https://developer.android.com/reference/android/view/DisplayCutout

[11] getBoundingRects(): https://developer.android.com/reference/android/view/DisplayCutout.html#getBoundingRects()

[12] getSafeInsetLeft(): https://developer.android.com/reference/android/view/DisplayCutout.html#getSafeInsetLeft()

[13] getSafeInsetRight(): https://developer.android.com/reference/android/view/DisplayCutout.html#getSafeInsetRight()

[14] getSafeInsetTop(): https://developer.android.com/reference/android/view/DisplayCutout.html#getSafeInsetTop()

[15] getSafeInsetBottom(): https://developer.android.com/reference/android/view/DisplayCutout.html#getSafeInsetBottom()

[16] Источник: https://habr.com/post/419109/?utm_source=habrahabr&utm_medium=rss&utm_campaign=419109