Это начало серии статей о создании и использовании наложений на Android. Наложение - это представление, расположенное над другими представлениями. Такие взгляды могут быть создано на Android несколькими различными способами и для разных целей.
Существует несколько вариантов наложения на Android. Некоторые из них включают в себя:
- Плавающие кнопки / меню
- Учебные пособия по адаптации
- Реклама
- Дополненная реальность
- Необычные анимации
Жизнь под WindowManager
В этой статье мы увидим, как мы можем создать представление наложения, которое будет лежать поверх других приложений с помощью WindowManager.
Но давайте сначала посмотрим, что такое WindowManager. WindowManager - это система Служба, отвечающая за организацию экрана. Он взаимодействует с приложениями для создания окон и решает, куда они пойдут на экране и в каком порядке. При создании WindowManager запрашивает Surface для этого окна, и на этой поверхности приложение будет рисовать свой пользовательский интерфейс. Каждое окно является контейнером представлений и имеет собственный вид иерархия.
Но давайте будем более конкретными и перечислим, что такое WindowManager и что он делает:
- Системный сервис
- Запрашивает создание и распределение поверхностей для клиентов (приложений).
- Отвечает за то, какие окна видны, их z-порядок и как они расположены на экране
- Обрабатывает и отправляет события ввода и фокусировки клиентам
- Он наблюдает за переходами, ориентацией экрана и анимацией
- Он отправляет все метаданные окна в SurfaceFlinger, чтобы SurfaceFlinger мог использовать эти данные для составных поверхностей на дисплее.
- Каждый экземпляр WindowManager привязан к определенному дисплею
- WindowManager никогда не имеет дело с битами созданных поверхностей, это зависит от клиентов, и они делают это напрямую, не проходя через WindowManager
В процессе создания окон и управления их жизненным циклом мы встретим термин Window Token особого типа объекта Binder. Связыватель является специфичным для Android механизм межпроцессного взаимодействия, а также система удаленного вызова методов. В платформе Android оконные токены и объекты Binder в целом широко используются для обеспечения безопасности. причины. Токен окна уникальным образом идентифицирует окно и используется WindowManager для определения, разрешено ли помещать окно в запрошенную позицию. Приложения может получить свои собственные токены окна, но они не имеют доступа к токенам других приложений. Таким образом, платформа может ограничить приложение своей собственной песочницей и не разрешить приложениям рисовать окна над другими приложениями.
Например, во время запуска приложения через ActivityMangerService создается токен окна (который называется токеном окна приложения), который передается как приложению и WindowManager. Этот токен однозначно определяет вид верхнего окна приложения. Когда приложение хочет добавить или удалить Windows, оно передает этот токен WindowManager, который решает, могут ли они быть созданы в требуемом месте и куда они должны идти. Таким образом, когда ActivityManager хочет закрыть или скрыть все окна, созданные активность, он может легко запросить это, предоставив соответствующий токен WindowManager.
В целом мы можем сказать, что у нас есть два типа токенов окна:
- AppWindowToken , который является дескриптором для действия, которое он использует для отображения окон. Это конкретная версия WindowToken, относящаяся к конкретному приложению или активность, которая отображает окна
- WindowToken , который является контейнером набора связанных окон в WindowManager. Для вложенных окон существует WindowToken, созданный для управления родительским окном. его дети.
Вы можете прочитать больше о жетонах окна и о том, как они обрабатываются, в следующих статьях: “Binders & Window Tokens” and «Окно приложения Android (Activity) просмотр объектов (Просмотр), анализ процесса создания ».
Существует 3 основных класса окон:
- Окна приложений являются обычными окнами верхнего уровня. Для этих типов окон их токен должен быть установлен на токен активности, частью которой он является.
- Подокна - это окна, связанные с другим окном верхнего уровня. У таких окон есть символ окна, к которому они прикреплены.
- Системные окна - это специальные типы окон, которые используются системой для определенных целей, таких как строка состояния, строка поиска, уведомления о тостах и другие. Они не нормально используются приложениями, и для их использования требуется специальное разрешение. Этот вид окон может перекрывать другие приложения. Для создания и использования системных окон, перед Выпуск Android Marshmallow оставалось только включить в Android Manifest разрешение SYSTEM\_ALERT\_WINDOW
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Однако, поскольку Android Marshmallow требует специального запроса с прямым действием пользователя, чтобы приложение могло создавать и добавлять или удалять окна над другими приложениями (Системные окна). Это разрешение не включено в новую структуру разрешений запросов, которая была введена в Marshmallow, и использует совершенно другой подход. Один из его Основные отличия заключаются в том, что он не может быть предоставлен через приложение, как все остальные разрешения.
Поскольку разработчики Android 6.0 могут вызывать Settings.canDrawOverlays () , чтобы проверить, было ли определенному контексту предоставлено разрешение для рисования поверх других приложений. Если разрешение не было предоставлено тем не менее, пользователь может создать и запустить Intent с целевым назначением Settings.ACTION_MANAGE_OVERLAY_PERMISSION , сопровождаемым URI имени пакета приложения для отправки пользователей непосредственно, чтобы дать разрешение на ваше приложение, вы рисуете над другими.
public static int OVERLAY_PERMISSION_CODE = 2525;
public void addOverlay() {
if (!Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
}
}
and then when the user returns to the app a check is made if permission was granted:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OVERLAY_PERMISSION_CODE) {
if (!Settings.canDrawOverlays(this)) {
// SYSTEM_ALERT_WINDOW permission not granted...
}
}
}
Теперь, когда мы перечислили некоторую важную информацию о том, как обрабатываются окна с помощью WindowMananger, и о доступных типах окон, давайте посмотрим, как мы можем создать наложение, которое будет существовать над другими приложениями через WindowMananger.
Обычно, когда приложение находится на переднем плане, пользователи могут взаимодействовать с ним, и они теряют эту способность, когда оно переходит в фоновый режим и приостанавливается. С WindowManager вы можете легко добавить вид наложения в системное окно поверх всего, с чем пользователи могут взаимодействовать, даже если приложение приостановлено и скрыто.
В зависимости от того, как вы хотите разместить этот оверлейный вид и срок жизни, которого вы хотите достичь, вы можете получить ссылку на WindowManager различными способами и добавить вид через активность или услугу.
1. Наложение через контекст действия
WindowManager может быть легко получен:
WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
Затем вам нужно определить параметры макета WindowManager.
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
Point point = OverlayUtil.getScreenDimensions(context);
params.height = point.y* 7/10;
params.width = LayoutParams.MATCH_PARENT;.
params.type = WindowManager.LayoutParams.TYPE_PRIORITY_PHONE;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.format = PixelFormat.TRANSLUCENT;
params.gravity = Gravity.CENTER | Gravity.LEFT;
wm.addView(overlayView, params);
а затем мы пытаемся добавить наш метод представления Overlay
addView(View, WindowManager.LayoutParams)
Этот метод выполняет три основных шага:
- Проверяет, было ли уже добавлено окно
- Определяет тип окна и, если оно является дочерним, и если да, оно присоединяет его к своему родительскому окну
- Он создает новый объект ViewRootImpl и вызывает его метод setView.
Как вы можете видеть, мы указали тип окна TYPE_PRIORITY_PHONE, который является одним из типов системного окна. Этот тип не относится к типу окна приложения и обычно помещается над всеми приложения, но находится за строкой состояния. Другим типом, который можно использовать, был бы TYPE_SYSTEM_ALERT, который является системным окном, таким как оповещение о низком энергопотреблении, и также расположен над другие приложения.
Если мы попытаемся добавить это окно без предоставления разрешения SYSTEM_ALERT_WINDOW, которое позволяет добавлять или удалять системные окна, возникает исключение BadToken, так как WindowManager понимает, что приложение пытается добавить окно безопасности, которое может отображаться поверх других приложений без правильного токена.
Как только вы добавляете представление наложения как системное окно с оконным менеджером через контекст действия, создается новое окно с собственной иерархией представления, которое добавляется над текущее окно действия.
Мы можем использовать Android Device Monitor и Hierarchy Viewer, чтобы увидеть список доступных окон в системе.
Как мы видим, новое окно создается с собственной иерархией представления. Это окно имеет нулевой маркер окна, однако оно связано с MainActivity.
Это тот же список, который мы можем получить из
adb shell dumpsys window windows
WINDOW MANAGER WINDOWS (dumpsys window windows)
.....
Window #3 Window{1fb8d3e0 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}:
mDisplayId=0 mSession=Session{27d0ccae 6335:u0a10090} mClient=android.os.BinderProxy@7c1dde3
mOwnerUid=10090 mShowToOwnerOnly=true
package=com.vourkosa.overlayapp appop=SYSTEM_ALERT_WINDOW
mAttrs=WM.LayoutParams{(0,0)(fillx1243) gr=#13 sim=#20 ty=2007 fl=#1000228 fmt=-3 surfaceInsets=Rect(0, 0 - 0, 0)}
Requested w=1080 h=1243 mLayoutSeq=139
mBaseLayer=81000 mSubLayer=0 mAnimLayer=81000+0=81000 mLastLayer=81000
mToken=WindowToken{1180a699 null}
mRootToken=WindowToken{1a074e0c null}
mViewVisibility=0x0 mHaveFrame=true
mObscured=false
mSeq=0 mSystemUiVisibility=0x0
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mConfiguration={1.0 310mcc260mnc en_US ?layoutDir sw360dp w360dp h567dp 480dpi nrml port finger qwerty/v/v dpad/v s.6}
mHasSurface=true mShownFrame=[0.0,304.0][1080.0,1547.0] isReadyForDisplay()=true
mFrame=[0,304][1080,1547] last=[0,304][1080,1547]
mSystemDecorRect=[0,0][1080,1243] last=[0,0][0,0]
Frames: containing=[0,75][1080,1776] parent=[0,75][1080,1776]
display=[-10000,-10000][10000,10000] overscan=[-10000,-10000][10000,10000]
content=[0,304][1080,1547] visible=[0,304][1080,1547]
decor=[0,0][1080,1920]
Cur insets: overscan=[0,0][0,0] content=[0,0][0,0] visible=[0,0][0,0] stable=[0,0][0,0]
Lst insets: overscan=[0,0][0,0] content=[0,0][0,0] visible=[0,0][0,0] stable=[0,0][0,0]
WindowStateAnimator{5101a36 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}:
mSurface=Surface(name=com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity)
mDrawState=HAS_DRAWN mLastHidden=false
Surface: shown=true layer=81000 alpha=1.0 rect=(0.0,304.0) 1080.0 x 1243.0
Window #2 Window{14eaa6e5 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}:
mDisplayId=0 mSession=Session{27d0ccae 6335:u0a10090} mClient=android.os.BinderProxy@25bcb5dc
mOwnerUid=10090 mShowToOwnerOnly=true
package=com.vourkosa.overlayapp appop=NONE
mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#120 ty=1
fl=#81810100 wanim=0x1030466 vsysui=0x600 surfaceInsets=Rect(0, 0 - 0, 0) needsMenuKey=2}
Requested w=1080 h=1776 mLayoutSeq=139
mBaseLayer=21000 mSubLayer=0 mAnimLayer=21010+0=21010 mLastLayer=21010
mToken=AppWindowToken{2a8c4f1 token=Token{15ff9498 ActivityRecord{398cce7b u0 com.vourkosa.overlayapp/.MainActivity t612}}}
mRootToken=AppWindowToken{2a8c4f1
token=Token{15ff9498 ActivityRecord{398cce7b u0 com.vourkosa.overlayapp/.MainActivity t612}}}
mAppToken=AppWindowToken{2a8c4f1 token=Token{15ff9498 ActivityRecord{398cce7b u0 com.vourkosa.overlayapp/.MainActivity t612}}}
mViewVisibility=0x0 mHaveFrame=true
mObscured=false
mSeq=0 mSystemUiVisibility=0x600
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mConfiguration={1.0 310mcc260mnc en_US ?layoutDir sw360dp w360dp h567dp 480dpi nrml port finger qwerty/v/v dpad/v s.6}
mHasSurface=true mShownFrame=[0.0,0.0][1080.0,1920.0] isReadyForDisplay()=true
mFrame=[0,0][1080,1920] last=[0,0][1080,1920]
mSystemDecorRect=[0,0][1080,1920] last=[0,0][0,0]
Frames: containing=[0,0][1080,1920] parent=[0,0][1080,1920]
display=[0,0][1080,1920] overscan=[0,0][1080,1920]
content=[0,75][1080,1776] visible=[0,75][1080,1776]
decor=[0,0][1080,1920]
Cur insets: overscan=[0,0][0,0] content=[0,75][0,144] visible=[0,75][0,144] stable=[0,75][0,144]
Lst insets: overscan=[0,0][0,0] content=[0,75][0,144] visible=[0,75][0,144] stable=[0,75][0,144]
WindowStateAnimator{b9bcd37 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}:
mSurface=Surface(name=com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity)
mDrawState=HAS_DRAWN mLastHidden=false
Surface: shown=true layer=21010 alpha=1.0 rect=(0.0,0.0) 1080.0 x 1920.0
....
и мы также можем потерять жетоны окна с
adb shell dumpsys window tokens
WINDOW MANAGER TOKENS (dumpsys window tokens)
All tokens:
WindowToken{1180a699 null}:
windows=[Window{1fb8d3e0 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=-1 hidden=false hasVisible=true
AppWindowToken{2a8c4f1 token=Token{15ff9498 ActivityRecord{398cce7b u0 com.vourkosa.overlayapp/.MainActivity t612}}}:
windows=[Window{14eaa6e5 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=2 hidden=false hasVisible=true
app=true voiceInteraction=false
allAppWindows=[Window{14eaa6e5 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
groupId=612 appFullscreen=true requestedOrientation=-1
hiddenRequested=false clientHidden=false willBeHidden=false reportedDrawn=true reportedVisible=true
numInterestingWindows=1 numDrawnWindows=1 inPendingTransaction=false allDrawn=true (animator=true)
startingData=null removed=false firstWindowDrawn=true mDeferRemoval=false
...
Из списка доступных токенов мы можем видеть, что у нас есть верхний уровень AppWindowToken для MainActivity и WindowToken для нашего оверлейного окна, который также указан, но имеет нулевое значение значение.
Если мы начнем второе действие, не разрушая первое действие, мы увидим, что для этого действия было создано новое окно и новый AppWindowToken во время наложения вид по-прежнему находится на вершине всего.
WINDOW MANAGER TOKENS (dumpsys window tokens)
All tokens:
WindowToken{38eaa3b8 null}:
windows=[Window{37fadf5d u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=-1 hidden=false hasVisible=true
AppWindowToken{572ee3b token=Token{2622ccca ActivityRecord{429e035 u0 com.vourkosa.overlayapp/.MainActivity t613}}}:
windows=[Window{f186e0f u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=2 hidden=true hasVisible=true
app=true voiceInteraction=false
allAppWindows=[Window{f186e0f u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
groupId=613 appFullscreen=true requestedOrientation=-1
hiddenRequested=true clientHidden=true willBeHidden=false reportedDrawn=false reportedVisible=false
numInterestingWindows=2 numDrawnWindows=2 inPendingTransaction=false allDrawn=true (animator=true)
startingData=null removed=false firstWindowDrawn=true mDeferRemoval=false
....
AppWindowToken{7d560a0 token=Token{6f9a5a3 ActivityRecord{24cda0d2 u0 com.vourkosa.overlayapp/.SecondActivity t613}}}:
windows=[Window{bc0f81e u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.SecondActivity}]
windowType=2 hidden=false hasVisible=true
app=true voiceInteraction=false
allAppWindows=[Window{bc0f81e u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.SecondActivity}]
groupId=613 appFullscreen=true requestedOrientation=-1
hiddenRequested=false clientHidden=false willBeHidden=false reportedDrawn=true reportedVisible=true
numInterestingWindows=1 numDrawnWindows=1 inPendingTransaction=false allDrawn=true (animator=true)
startingData=null removed=false firstWindowDrawn=true mDeferRemoval=false
....
Поскольку деятельность не уничтожена, проблем нет. Однако после того, как это действие уничтожено, происходит утечка окна, если оверлейный вид не удаляется заранее.
Если мы углубимся в это, мы увидим, что происходящее относится к жетонам окон. Если мы посмотрим на код Android, в WindowManagerGlobal , когда выполняется действие
уничтожен, он уведомляет WindowManagerGlobal о попытке закрыть все окна, связанные с этим действием. Оверлейное окно имеет нулевой маркер окна, но на него все еще ссылаются из
контекст действия.
Это та точка, где произойдет утечка окна.
WindowManagerGlobal.java
public void closeAll(IBinder token, String who, String what) {
synchronized (this) {
if (mViews == null)
return;
int count = mViews.length;
//Log.i("foo", "Closing all windows of " + token);
for (int i=0; i<count; i++) {
//Log.i("foo", "@ " + i + " token " + mParams[i].token
// + " view " + mRoots[i].getView());
if (token == null || mParams[i].token == token) {
ViewRootImpl root = mRoots[i];
root.mAddNesting = 1;
//Log.i("foo", "Force closing " + root);
if (who != null) {
WindowLeaked leak = new WindowLeaked(
what + " " + who + " has leaked window "
+ root.getView() + " that was originally added here");
leak.setStackTrace(root.getLocation().getStackTrace());
Log.e("WindowManager", leak.getMessage(), leak);
}
removeViewLocked(i);
i--;
count--;
}
}
}
}
Итак, что мы можем сделать в качестве обходного пути, чтобы избежать утечки окна и сохранить наше окно наложения над системой, даже если созданная деятельность уничтожена?
Есть два разных способа добиться этого:
a) Назначив токен новому окну безопасности перед вызовом WindowManager, чтобы добавить его в свой WindowManager LayoutParams
params . токен = new Binder ();
Из списка токенов видно, что в окне Overlay теперь есть токен.
WINDOW MANAGER TOKENS (dumpsys window tokens)
All tokens:
WindowToken{2bdf2e70 android.os.BinderProxy@39a72622}:
windows=[Window{1a15c8b3 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=-1 hidden=false hasVisible=true
...
AppWindowToken{2d2fc41a token=Token{c1de2c5 ActivityRecord{39c9f53c u0 com.vourkosa.overlayapp/.MainActivity t616}}}:
windows=[Window{219167ca u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=2 hidden=false hasVisible=true
app=true voiceInteraction=false
allAppWindows=[Window{219167ca u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
groupId=616 appFullscreen=true requestedOrientation=-1
hiddenRequested=false clientHidden=false willBeHidden=false reportedDrawn=true reportedVisible=true
numInterestingWindows=1 numDrawnWindows=1 inPendingTransaction=false allDrawn=true (animator=true)
startingData=null removed=false firstWindowDrawn=true mDeferRemoval=false
...
Таким образом, мы оставляем представление Overlay живым и независимым от жизненного цикла действия. Однако, как только приложение будет уничтожено, окно Overlay также будет удалено.
б) Получив ссылку на WindowManager через контекст приложения
WindowManager wm = (WindowManager) getApplicationContext().getSystemService(WINDOW_SERVICE);
Если мы получим ссылку на WindowManager через контекст приложения вместо контекста действия, то теперь мы можем видеть, что окно наложения не имеет отношения к действию больше, кроме приложения.
WINDOW MANAGER TOKENS (dumpsys window tokens)
All tokens:
WindowToken{23292f77 null}:
windows=[Window{1cef1811 u0 com.vourkosa.overlayapp}]
windowType=-1 hidden=false hasVisible=true
...
AppWindowToken{21f5a48f token=Token{3dc7d7ee ActivityRecord{1e855169 u0 com.vourkosa.overlayapp/.MainActivity t618}}}:
windows=[Window{39091423 u0 com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
windowType=2 hidden=false hasVisible=true
app=true voiceInteraction=false
allAppWindows=[Window{39091423 u0
com.vourkosa.overlayapp/com.vourkosa.overlayapp.MainActivity}]
groupId=618 appFullscreen=true requestedOrientation=-1
hiddenRequested=false clientHidden=false willBeHidden=false reportedDrawn=true reportedVisible=true
numInterestingWindows=1 numDrawnWindows=1 inPendingTransaction=false allDrawn=true (animator=true)
startingData=null removed=false firstWindowDrawn=true mDeferRemoval=false
...
Этот трюк позволяет представлению «Наложение» жить так долго, как живет приложение. После остановки приложения вид Overlay также удаляется.
2. Наложение через службу
Другой способ создать наложение как системное окно, которое будет добавлено поверх других приложений, - добавить это наложение через WindowManager таким же образом, как описано в предыдущий раздел, но через службу, которая может работать в фоновом режиме.
Обычное использование этого подхода можно найти в чатах Facebook и других популярных приложениях. Вам просто нужно объявить службу в файле приложения AndroidManifest.
<service android:name=".OverlayService"></service>
and start or stop the service at any time:
Intent intent = new Intent(activity, OverlayService.class);
activity.startService(intent);
Intent intent = new Intent(activity, OverlayService.class);
activity.stopService(intent);
public class OverlayService extends Service {
private WindowManager wm;
private ImageView androidHead;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
androidHead = new ImageView(this);
androidHead.setImageResource(R.drawable.ic_launcher);
wm = (WindowManager) getSystemService(WINDOW_SERVICE);
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
params.type = WindowManager.LayoutParams.TYPE_PRIORITY_PHONE;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.format = PixelFormat.TRANSLUCENT;
params.gravity = Gravity.TOP | Gravity.LEFT;
wm.addView(androidHead, params);
}
@Override
public void onDestroy() {
super.onDestroy();
removeView();
}
private void removeView() {
if (androidHead != null) {
wm.removeView(androidHead);
}
}
}
Как мы видели, существуют разные способы размещения оверлеев над приложением или другими приложениями с помощью WindowManager. Однако необходимость запроса специального разрешения, особенно с Android Marshmallow или необходимость объявления и использования службы для сохранения наложения независимо от жизненного цикла приложения или действия делает этот подход менее удобным.
section>