Как сделать фон наложения системы на НЕСКОЛЬКО (режим смешивания)?

Сначала немного контекста для моей проблемы: я пытаюсь создать представление типа системного наложения (то есть рисовать поверх других приложений), которое (пока) является просто полноэкранным сплошным цветом. Я включаю / выключаю, запуская / останавливая службу.

Вот как выглядит мой код (внутри сервиса):

public void onCreate() {
    super.onCreate();
    oView = new LinearLayout(this);
    oView.setBackgroundColor(0x66ffffff);

    PorterDuffColorFilter fil = new PorterDuffColorFilter(0xfff5961b, PorterDuff.Mode.MULTIPLY);
    oView.getBackground().setColorFilter(fil);

    WindowManager.LayoutParams params = new WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
            0 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT);
    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    wm.addView(oView, params);
}

Таким образом, я могу получить сплошной цвет по сравнению с остальной частью моего регулярного использования Android, когда я запускаю этот сервис. Теперь моя конкретная проблема:есть ли способ, которым я могу получить этот цвет в МНОГОКРАТНО (думаю, режим смешивания фотошопа) по сравнению с регулярным использованием Android за этим?

Как вы можете заметить из моего кода, я пытался использовать режимы фильтра PorterDuff (пара различных их комбинаций), чтобы добиться этого тоже напрасно.

Вот пара скриншотов, чтобы лучше объяснить это:

оригинальный вид <- Оригинальный экран без моего обслуживания.

текущий результат <- Тот же экран с включенным сервисом текущего кода.

предполагаемый результат <- ожидаемый результат на том же экране. Обратите внимание, как темные цвета умножаются на нижнюю.

Как вы видите, мой текущий код использует только слой сплошного цвета. Я ценю все предложения. Заранее спасибо!

1 ответ

Для тех, кто все еще задается вопросом, возможно ли такое, я решил поделиться своим опытом решения этой проблемы.

Короче

Android спроектирован таким образом, что он не позволяет применять смешение цветов в разных контекстах (на самом деле я не могу доказать это утверждение, и это только из моего личного опыта), поэтому для достижения того, что вы хотите, вам сначала нужно будет как-то отрендерить целевое изображение на поверхности, принадлежащее одному из контекстов вашего приложения.


0. Обзор

Невозможно просто транслировать домашний экран Android в приложении, по крайней мере, из соображений безопасности (в противном случае можно было бы украсть личные данные и пароли, заполнив ввод данных пользователем, заставив приложение выглядеть и вести себя точно так же, как домашний экран системы), Однако в Android API 21 (Lollipop) было введено так называемое MediaProjection класс, который вводит возможность записи экрана. Я не думаю, что это возможно для более низкого API без рутирования устройства (если это нормально для вас, вы можете использовать бесплатно adb shell screencap команда из приложения с помощью exec командование Runtime учебный класс). Если у вас есть скриншот домашнего экрана, вы можете создать начальный экран из своего приложения, а затем применить смешение цветов. К сожалению, эта функция записи экрана также будет записывать наложение, поэтому вам придется записывать экран только тогда, когда наложение скрыто.

1. Сделай скриншот

Делая скриншот с MediaProjection это не так сложно, но требует некоторых усилий для выполнения. Также требуется, чтобы у вас была активность, чтобы запросить у пользователя разрешения на запись экрана. В первую очередь вам нужно будет запросить разрешение на использование Media Project Service:

private final static int SCREEN_RECORDING_REQUEST_CODE = 0x0002;
...
private void requestScreenCapture() {
    MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    Intent intent = mediaProjectionManager.createScreenCaptureIntent();
    startActivityForResult(intent, SCREEN_RECORDING_REQUEST_CODE);
}

И после этого обработать запрос в onActivityResult метод. Имейте в виду, что вам также нужно сохранить Intent вернулся из этого запроса для последующего использования, чтобы получить VirtualDisplay (это фактически делает всю работу для захвата экрана)

@Override
protected void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case SCREEN_RECORDING_REQUEST_CODE:
            if (resultCode == RESULT_OK) {
                launchOverlay(data);
            } else {
                finishWithMessage(R.string.error_permission_screen_capture);
            }
            break;
    }
}

В конце концов вы можете получить MediaProjection экземпляр (используя данные намерения, ранее возвращенные) из MediaProjectionManager системный сервис:

MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) appContext.getSystemService(MEDIA_PROJECTION_SERVICE);
// screenCastData is the Intent returned in the onActivityForResult method
mMediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, screenCastData);

Для того, чтобы сделать VirtualDisplay сделать начальный экран, мы должны предоставить ему Surface, Затем, если нам нужно изображение с этой поверхности, нам нужно будет кэшировать рисунок и захватить его в растровое изображение, или попросить поверхность нарисовать прямо на нашем холсте. Однако в данном конкретном случае нет необходимости изобретать колесо, для таких целей уже есть нечто, называемое ImageReader, Поскольку нам нужно имитировать домашний экран, ImageReader должен быть создан с использованием реального размера устройства (включая строку состояния и панель кнопок навигации, если она представлена ​​как часть экрана Android):

mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Display defaultDisplay = mWindowManager.getDefaultDisplay();
Point displaySize = new Point();
defaultDisplay.getRealSize(displaySize);
final ImageReader imageReader = ImageReader.newInstance(displaySize.x, displaySize.y, PixelFormat.RGBA_8888, 1);

Нам также нужно установить прослушиватель буфера входящего изображения, поэтому при создании снимка экрана он переходит к этому прослушивателю:

imageReader.setOnImageAvailableListener(this, null);

Мы скоро вернемся к реализации этого слушателя. Теперь давайте создадим VirtualDisplay так что, наконец, мы можем сделать снимок экрана:

final Display defaultDisplay = mWindowManager.getDefaultDisplay();
final DisplayMetrics displayMetrics = new DisplayMetrics();
defaultDisplay.getMetrics(displayMetrics);
mScreenDpi = displayMetrics.densityDpi;
mVirtualDisplay = mMediaProjection.createVirtualDisplay("virtual_display",
                                                        imageReader.getWidth(),
                                                        imageReader.getHeight(),
                                                        mScreenDpi,
                                                        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                                                        imageReader.getSurface(),
                                                        null, null);

Сейчас ImageRender отправит слушателю новое изображение всякий раз, когда VirtualDisplay испускает это. Слушатель состоит только из одного метода, он выглядит следующим образом:

@Override
public void onImageAvailable(@NonNull ImageReader reader) {
    final Image image = reader.acquireLatestImage();
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer buffer = planes[0].getBuffer();
    int pixelStride = planes[0].getPixelStride();
    int rowStride = planes[0].getRowStride();
    int rowPadding = rowStride - pixelStride * image.getWidth();
    final Bitmap bitmap = Bitmap.createBitmap(image.getWidth() + rowPadding / pixelStride,
            image.getHeight(), Bitmap.Config.ARGB_8888);
    bitmap.copyPixelsFromBuffer(buffer);
    image.close();
    reader.close();
    onScreenshotTaken(bitmap);
}

После завершения обработки изображения вы должны позвонить close() метод ImageReader, Это освобождает все ресурсы, выделенные им, и в то же время делает ImageReder непригодным для использования. Таким образом, когда вам нужно сделать следующий снимок экрана, вам нужно будет создать другой экземпляр ImageReader, (Поскольку наш экземпляр был создан с использованием буфера 1 изображения, он все равно не может быть использован)

2. Поместите скриншот поверх Homescreen

Теперь, когда мы получим точный скриншот главного экрана, он должен быть размещен так, чтобы конечный пользователь не заметил разницу. Вместо LinearLayout что вы используете в своем вопросе, я выбрал ImageView с аналогичными параметрами макета. Поскольку он уже охватил весь экран, остается только отрегулировать положение скриншота на нем, чтобы он не содержал строки состояния и кнопок панели навигации (это происходит потому, что запись экрана фактически захватывает весь экран устройства, тогда как наш вид наложения может быть размещен только в пределах области рисования). Для этого мы можем использовать простой матричный перевод:

private void showDesktopScreenshot(Bitmap screenshot, ImageView imageView) {
    // The goal is to position the bitmap such it is attached to top of the screen display, by
    // moving it under status bar and/or navigation buttons bar
    Rect displayFrame = new Rect();
    imageView.getWindowVisibleDisplayFrame(displayFrame);
    final int statusBarHeight = displayFrame.top;
    imageView.setScaleType(ImageView.ScaleType.MATRIX);
    Matrix imageMatrix = new Matrix();
    imageMatrix.setTranslate(-displayFrame.left, -statusBarHeight);
    imageView.setImageMatrix(imageMatrix);
    imageView.setImageBitmap(screenshot);
}

3. Применить смешивание цветов

Поскольку у нас уже есть вид, содержащий домашний экран, я счел удобным расширить его, чтобы у него была возможность рисовать наложение с примененным режимом наложения. Давайте расширим класс ImageView и представим пару пустых методов:

class OverlayImageView extends ImageView {
    @NonNull
    private final Paint mOverlayPaint;

    public OverlayImageView(Context context) {
        super(context);
    }

    void setOverlayColor(int color) {
    }

    void setOverlayPorterDuffMode(PorterDuff.Mode mode) {
    }
}

Как видите, я добавил поле Paint тип. Это поможет нам нарисовать наш оверлей и отразить изменения. На самом деле я не считаю себя экспертом в компьютерной графике, но, чтобы исключить какие-либо недоразумения, я хотел бы подчеркнуть, что подход, который вы использовали в этом вопросе, несколько ошибочен - вы берете фон, который можно нарисовать для вашего взгляда (и это не на самом деле не зависит от того, что скрывается за вами), и примените режим PorterDuff только к нему. Таким образом, фон служит цветом назначения и цветом, который вы указали в PorterDuffColorFilter Конструктор служит исходным цветом. Только эти две вещи смешаны, ничто другое не затронуто. Однако, когда вы применяете режим PorterDuff на Paint и нарисуйте его на холсте, все виды, которые находятся за этим холстом (и принадлежат одному Context) смешаны. Давайте переопределим onDraw метод, и просто нарисуйте краску в нашем ImageView:

@Override
protected void onDraw(@NonNull Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPaint(mOverlayPaint);
}

Теперь нам нужно только добавить соответствующие изменения для наших сеттеров и попросить представление перерисовать себя, вызвав invalidate() метод:

void setOverlayColor(@SuppressWarnings("SameParameterValue") int color) {
    mOverlayPaint.setColor(color);
    invalidate();
}

void setOverlayPorterDuffMode(@SuppressWarnings("SameParameterValue") PorterDuff.Mode mode) {
    mOverlayPaint.setXfermode(new PorterDuffXfermode(mode));
    invalidate();
}

Это оно! Вот пример того же эффекта без смешивания в режиме умножения и с его применением:


Вместо заключения

Конечно, это решение далеко от совершенства - оверлей полностью перекрывает домашний экран, и чтобы сделать его жизнеспособным, вам нужно найти множество обходных путей для решения таких сложных ситуаций, как обычное взаимодействие, клавиатура, прокрутка, вызовы, системные диалоги и т. Д. многие другие. Я попробовал это сделать и быстро создал приложение, которое скрывает оверлей, когда пользователь касается экрана. У него все еще много проблем, но он должен стать для вас хорошей отправной точкой. Основная проблема заключается в том, чтобы приложение осознало, что что-то происходит вокруг. Я не проверял Accessibility Service, но он должен подходить очень хорошо, так как в нем гораздо больше информации о действиях пользователя, чем в обычном сервисе. Вы можете проверить мое решение здесь - https://github.com/AlexandrSMed/stack-35590953

Другие вопросы по тегам