Как не выйти в Window при работе с Window?
Многие разработчики разбиваются о жизненные циклы onResume, onStart, onCreate, которые связаны с отображением UI внутри приложения, будь то Activity или Fragment. Некоторые методы работы со стремительно развивающимся андроидом приходится искать интуитивно, потому что официальная документация не всегда дает полной картины, а иногда даже вводит в заблуждение. Стоит разобраться, где заканчиваются знания и начинается интуиция.
Я Дмитрий Манько, андроид-разработчик в компании Ситимобил, попробую объяснить, что такое onResume () и почему определение от Google не совсем корректное. Разберу иерархию внутри Activity, покажу когда происходит взаимодействие и какие события для этого нужны. А ещё объясню, почему Fragment дешевле и проще Activity.
Сразу начнем с практики. Давайте рассмотрим кейс:
Есть MainActivity, который наследуется от обычного AppCompatActivity. В onCreate мы устанавливаем activity_main, переопределяем и морозим onResume.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onResume() {
super.onResume()
Thread.sleep(TimeUnit.HOURS.toMillis(1))
// Никто никуда не спешит, поспим часик?
}
}
В качестве верстки для activity_main устанавливаем ConstraintLayout с текстовой кнопкой.
Попытаемся выбрать правильный ответ на вопрос: «Что произойдет, если заморозить onResume при первом запуске приложения внутри Activity»?
На кнопку нельзя будет нажать, на экране будет отрисована кнопка.
На кнопку нельзя будет нажать, на экране будет отрисован только черный фон.
Можно будет нажать на кнопку, на экране будет отрисована кнопка.
На кнопку нельзя будет нажать, на экране будет отрисован только белый фон.
Результат ответов с конференции
Этот вопрос демонстрирует, чем руководствуются разработчики, знаниями, опытом или интуицией. Например, участники конференции ответили следующим образом:
31%
25%
12%
32%
Правильный ответ
Четвертый вариант правильный, поэтому не удивительно, что он набрал больше голосов, но остальные ответы близки к нему по популярности. Вариант, что на кнопку нельзя будет нажать, но она будет отрисована, выбрало практически такое же количество опрошенных. А многие из правильно ответивших, не до конца понимали, почему получается именно так. Поэтому, можно сделать вывод, что правильный ответ они просто угадали. Давайте разберемся, почему окно и кнопка так себя ведут.
Большинство разработчиков ожидают получить результат слева, но ни Action Bar, ни кнопка не отрисуются, и ничего, кроме белого фона, не будет.
Официальная документация от Google
В официальной документации от Google написано:
onResume () — This is an indicator that the activity became active and ready to receive input. It is on top of an activity stack and visible to user.
Т.е. это некий указатель на то, что activity становится активной и готова принимать входящие взаимодействия: ввод, вызовы и т.д. Это находится наверху нашего Activity-стека и видно пользователю. Но на самом деле, если onResume () вызвался, а мы морозим поток, то наши View элементы еще не видны пользователю.
Поэтому давайте разбираться. Для этого нам придется спуститься в так называемые «кишки» Андроида и посмотреть, кто вызвал onResume (), кто вызвал тот метод, который вызвал onResume () и отправиться ещё выше.
onResume ()
Кто вызвал onResume ()
public final class ActivityThread extends ClientTransactionHandler {
// Some code here
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest, String reason) {
// And some code here
r.activity.performResume(r.startsNotResumed, reason);
// And some code here
}
Чтобы было проще понять, как все это между собой взаимодействует, я опущу некоторые подробности. В первую очередь нас интересует, как вызывается onResume.
r — это ActivityClientRecord, то есть обертка, которая содержит инстанс нашей Activity и дополнительную информацию о её состоянии. Она вызывает метод performResumeActivity. Он дает ответ, что тот onResume, который мы заморозили, был вызван.
Идем выше, чтобы понять, кто вызывает метод performResumeActivity
Кто вызвал performResumeActivity ()
public final class ActivityThread extends ClientTransactionHandler {
// Some code here
@Override
public void handleTopResumedActivityChanged(IBinder token, boolean onTop, String reason) {
// And some code here
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
// And some code here
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// And some code here
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
С помощью переменной присваивается ActivityClientRecord и в нашем путешествии появляются сущности:
Window
DecorView
WindowManager
Их мы разберем позднее, а сейчас нас интересует код в конце вызова метода activity makeVisible ()
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
Перейдем к погружению в метод makeVisible ():
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
// Some code here
void makeVisible() {
// Implementation here
}
// Some code here
}
Отметим, что имплементация находится в таком классе как Activity.
Код метода makeVisible () выглядит следующим образом:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
Мы получаем WindowManager, добавляем к нему mDecor, кладем атрибуты, и делаем mDecor видимой.
Посмотрим, что происходит внутри метода addView у WindowManager.
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
// Some code here
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
}
У нас существует имплементация WindowManager — WindowManagerImpl. В WindowManagerImpl я не стал описывать происходящее, потому что он делегирует вызов в WindowManagerGlobal. Как видно, это применение того же метода, но уже у самого WindowManagerGlobal.
Взглянем подробнее на WindowManagerGlobal:
WindowManagerGlobal
public final class WindowManagerGlobal {
// Some code here
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
// Some code here
ViewRootImpl root;
// Some code here
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
В нем происходит инициализация ViewRootImpl и добавляются наши атрибуты в массив Views, массив Roots и массив Params. Самое важное — это конечное действие, которое обернуто в try/catch. Здесь у нас устанавливается метод setView нашей ViewRootImpl.
Именно в этот последний промежуток у нас root.setView () устанавливает наши View, начиная от DecorView и это позволяет проинициализировать все необходимое для старта отображения, и запустить тот самый performTraversals.
Возможно, вы знаете про этот метод, но на всякий случай напомню.
PerformTraversals прогоняет жизненный цикл наших Views. Он измеряет и рисует, начиная с корневой и заканчивая дочерними.
Почему так сложно?
Нельзя ли сделать проще?
Чтобы разобраться, давайте попытаемся дать свое определение:
onResume () — метод жизненного цикла Activity, который уведомляет, что необходимый контент был добавлен в DecorView, но будет отображен только тогда, когда ViewRootlmpI установит эту view и выполнит обход вызовов методов View для отрисовки.
Звучит сложно, поэтому и говорят, что сейчас это будет видно пользователю. Но в формулировке от компании Google я бы поправил, что не «видно пользователю», а «вот-вот будет видно», так будет корректней.
Google молодцы
Должен признать, что к моменту выхода этой статьи, Google убрала эти двусмысленные строчки, теперь часть описания метода onResume () выглядит так:
onResume () — This is usually a hint for your activity to start interacting with the user, which is a good indicator that the activity became active and ready to receive input.
Activity
Activity — это контроллер, который обрабатывает входящие события. Но это вовсе не сущность, которая отвечает за View.
Она реагирует на какие-то события, добавляет и отображает View, а также взаимодействует через обратные методы и содержит внутри себя Window.
Так можно понять примерную иерархию: внутри Activity есть Window.
Window
Window — это обертка над DecorView.
Ее задача — передать DecorView ViewRootlmpI для отрисовки и уведомить Activity о том, что произошло событие.
DecorView
DecorView — это корневая View.
Ее можно увидеть в Layout Inspector, но давайте разберемся, что это такое.
Наш контент находится здесь. Также у нас есть navigationBar и statusBar. По сути, DecorView это обычная View, точнее наследник FlameLayout. Она находится внутри Window.
ViewRootlmpI
ViewRootlmpI — это связующий класс между Window Manager Service и DecorView. Все взаимодействия, измерения, расположение и отрисовка View проходят через него
Несмотря на название, он не является частью View-иерархии, не является View в привычном понимании Андроида. Это связующее звено, которое получает события от сервиса и передает обратно View, View Activity и т.д., и мы получаем сообщение о том, что что-то произошло.
WindowManager
WindowManager — это системная служба, управляющая отображением списка Window.
Она отвечает за анимации при закрытии приложения, вращение и другие операции с окнами.
Схема взаимодействий
Найденная на просторах интернета схема мне полностью подходила, и я решил ее не переделывать.
В схеме видно, что есть Activity или Dialog, внутри него PhoneWindow — это единственная имплементация от Window. Внутри него DecorView и View. Есть WindowManager, внутри него ViewRootlmpI и WindowManagerService — это сервис, который управляет окнами.
Почему Activity и Dialog здесь на одном уровне расскажу дальше.
Взаимодействие происходит, когда:
Мы сообщаем из Активити что мы установили в иерархию View что-то
Мы сообщаем это Window
Window передает это DecorView
DecorView передается в ViewRootlmpI
ViewRootlmpI — WindowManager
WindowManager — WindowManager service
Обратно происходят почти тоже самое, но к примеру при физическом касании экрана Manager ответственный за входящие взаимодействия сообщает о событии ViewRootlmpI, ViewRootlmpI сообщает DecorView и т.д.
Есть разные вариации этой схемы, но они отображают одно и то же.
Почему фрагмент
Я много раз слышал, что фрагмент дешевле и проще, чем Activity, но не слышал конкретных причин, кроме идеологических
Причина в том, что для Activity нужно создать DecorView, Window и т.д., то есть пройти первый цикл отрисовки и поставить параметры. Для фрагмента нужно намного меньше, достаточно добавить фрагмент в уже существующую иерархию, которую создала Activity.
Я обещал рассказать, почему на схеме взаимодействия были показаны диалоги.
DialogFragment
На самом деле DialogFragment создает свой DecorView, но у него совершенно другой Window, и все необходимые компоненты, для того чтобы находиться поверх окна, который содержит Activity. Этот DecorView не встроен в иерархию Activity, хотя является фрагментом. Такой способ показа диалога независим от нашей Activity, и от этого возникают эффекты тени и прочее.
Хаки с Window
Все эти знания позволяют нам, например, с помощью WindowManager делать отображение над любыми другими приложениями и меню, так у драйверского приложения есть плавающая иконка (это не иконка запуска)
С помощью сервиса установки разных форматов WindowManager, мы можем сделать плавающее приложение, которое будет открывать, закрывать и выполнять другие действия. Так работает, например, запись экрана.
Примерный код:
WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
wm.addView (myView, myWindowLayoutParams);
Главное учитывать ограничения, которые есть на платформах, и то, что Window необходимо создавать из сервиса, чтобы он не был привязан ни к Activity, ни к фрагменту, тогда окно будет постоянно отображено.