Для начала нам необходимо разобраться, нужна ли вообще оптимизация приложению?
Если у вас 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!
Комментарии (18)
saege5b
03.08.2018 11:16Я прошу прощения, а зачем нужна это 'чёлка' и есть ли в ней жизненная необходимость?
smartdev Автор
03.08.2018 11:33В данном случае я привожу примеры программной чёлки, она нужна только для того, чтобы понять, где могут быть проблемы на реальных девайсах с чёлкой.
На реальных девайсах же она будет аппаратная и в ней будет скрываться камера и разные датчики, т.е. там уже будет действительно недоступная зона экрана.
Есть ли в ней жизненная необходимость производители смартфонов уже решили и практически каждый новый анонсированный девайс ей оснащён. Пользоваться такими устройствами или нет — Ваш выбор. А вот поддерживать её в своём приложении или нет — думаю нет выбора, т.к. если где-то из-за неё едет вёрстка, то однозначно придётся.
hdfan2
03.08.2018 11:37Карго-культ, как он есть. Если вы сделаете на своём смартфоне чёлку, то ваша компания сразу подорожает до триллиона долларов.
DareDen
03.08.2018 11:38Нет в ней необходимости. Просто мода. Повлиять можем лишь не покупая телефоны с козырьком. Эплофилы будут брать челочный iPhone в любом случае, китайцы копировать его тоже не перестанут, так что повлиять по факту не можем никак :(.
Javian
03.08.2018 14:13«Дизайнеры победили инженеров».
P.S. Надеюсь эта идея финансово провалится и больше не будет таких вырезаний какой-то части экрана.
Revertis
03.08.2018 13:37«Поясняем за чёлку»
Перестаньте уже писать это «за» везде. Скучать за, пояснять за… Это не по-русски.uploadfor
03.08.2018 13:55-2Перестаньте уже писать это «за» везде. Скучать за, пояснять за… Это не по-русски.
Вероятно, одна из причин того, что «карма потихоньку течёт» — это когда человек с явно нерусским ником типа Revertis даже находясь в Словакии не перестаёт поучать остальных русскому языку ;)nafgne
03.08.2018 15:06Да нет, не совсем так. Карма "течёт" потому что это единственный способ заставить людей писать статьи, и администрации выгодно поддерживать атмосферу всеобщей грызни.
Не удивлюсь, если половина минусов вообще фиктивная.
ClearAirTurbulence
03.08.2018 14:07+1Это шутка такая, про гопников и неформалов, так что эта конструкция вполне оправданна.
Sinii
03.08.2018 17:42+1Вопрос не совсем про челку, но сам столкнулся с проблемой что при выставленном флаге setFitsSystemWindows = false ломается работа adjustResize
Гугл о баге знает с 2009 года — issuetracker.google.com/issues/36911528
Вы как-нибудь решали эту проблему?smartdev Автор
03.08.2018 17:46Да, встречались с такой проблемой. Написали свой workaround, который слушал onGlobalLayout через ViewTreeObserver.OnGlobalLayoutListener и высчитывали высоту, которую можно использовать для верстки через getWindowVisibleDisplayFrame. И после расчёта прокидывали в качестве callback'а новую высоту и разницу в высотах всем слушателям. А они уже в свою очередь перерисовывались. Надеюсь будет полезно :)
Sinii
03.08.2018 17:53Спасибо за ответ! Некоторое время такой фикс и у меня работал. Но, к сожалению, в таком способе стал сталкиваться с проблемой что на некоторых устройствах Samsung (S5, если это имеет значение) неверно определяется высота статусбара и фикс стал вычислять высоту некорректно.
В итоге получается что такой способ уж очень привязан к устройству и нет гарантии что на каком-нибудь китайце все опять не поедет)
nafgne
Я пытался придумать более дурацкую причину фрагментации, но не смог.
Неужели нельзя было по умолчанию показывать старые приложения без использования ограниченной части экрана?
smartdev Автор
Да, глупее пока сложно что-то придумать
По сути, если приложение не лезет под статус бар, то оно по умолчанию будет без ограниченной части верстаться
В основном зависит от windowActionBarOverlay флага, и других, типа windowActionBar, windowNoTitle, windowDrawsSystemBarBackgrounds, windowTranslucentStatus, windowIsTranslucent и их комбинаций
т.е дефолтное приложение без заморочек сверстается адекватно
Tihon_V
На Huawei/Honor при открытии приложения — открывается запрос можно ли использовать весь экран, однако в некоторых приложениях (Microsoft RDP Client) без разрешения — скругляются углы.
vlad_august
на oneplus 6 так и есть. По умолчанию чёлка скрыта у всех приложений.