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