如何制作系统叠加视图的背景 MULTIPLY(混合模式)?

and*_*art 6 android blending system-alert-window

首先是我的问题的一些背景:我正在尝试创建一个系统覆盖类型视图(即在其他应用程序上绘制),它(目前)只是全屏纯色。我通过启动/停止服务来打开/关闭它。

这是我的代码到目前为止的样子(在服务内部):

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);
}
Run Code Online (Sandbox Code Playgroud)

这样,当我启动此服务时,我可以在其余的常规 android 使用中获得纯色。现在,我的具体问题有什么方法可以使这种颜色在其背后的常规 android 使用中进行 MULTIPLY(想想 photoshop 混合模式)?

从我的代码中可以看出,我也尝试使用 PorterDuff 过滤器模式(它们的几种不同组合)来实现它,但徒劳无功。

这里有几个截图,希望能更好地解释这一点:

原图 <-- 没有我的服务的原始屏幕。

当前结果 <-- 打开当前代码服务的同一屏幕。

预期结果<-- 在同一屏幕上的预期结果。注意较深的颜色是如何叠加到下面的。

如您所见,我当前的代码仅抛出了一层纯色。我感谢所有的建议。提前致谢!

The*_*ind 3

对于那些仍然想知道这样的事情是否可能的人,我决定分享我解决这个问题的经验。

长话短说

Android 的设计不允许在不同上下文的视图之间应用颜色混合(我实际上无法证明这个说法,这纯粹是我个人的经验),所以要实现你想要的,你需要首先以某种方式渲染一个表面上的目标图像,属于您的应用程序上下文之一。


0. 概述

没有办法只在应用程序中广播 Android 主屏幕,至少是出于安全原因(否则人们可以通过使应用程序的外观和行为与系统主屏幕完全相同来填充用户输入来窃取个人数据和密码) 。然而,在 Android API 21 (Lollipop) 中引入了所谓的MediaProjection类,它引入了录制屏幕的可能性。我不认为在不root设备的情况下使用较低的API是不可能的(如果你可以的话,你可以使用类的adb shell screencap命令在应用程序中自由使用命令)。如果您有主屏幕的屏幕截图,您可以在应用程序中创建主屏幕的感觉,然后应用颜色混合。不幸的是,此屏幕录制功能也会记录覆盖本身,因此您必须仅在隐藏覆盖时才录制屏幕。execRuntime

1. 截图

使用 截屏MediaProjection并不困难,但需要一些努力才能执行。它还要求您有一个活动来询问用户屏幕录制权限。首先,您需要获得使用媒体项目服务的许可:

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);
}
Run Code Online (Sandbox Code Playgroud)

然后在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

最终你可以从系统服务获取一个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);
Run Code Online (Sandbox Code Playgroud)

为了制作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);
Run Code Online (Sandbox Code Playgroud)

我们还需要设置传入图像缓冲区的侦听器,因此当截取屏幕截图时,它会转到此侦听器:

imageReader.setOnImageAvailableListener(this, null);
Run Code Online (Sandbox Code Playgroud)

我们很快就会回到这个监听器的实现。现在让我们创建一个,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);
Run Code Online (Sandbox Code Playgroud)

现在,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);
}
Run Code Online (Sandbox Code Playgroud)

图像处理完成后,您应该close()调用ImageReader. 它释放了它分配的所有资源,同时使这些资源变得ImageReder不可用。因此,当您需要截取下一个屏幕截图时,您将必须实例化另一个ImageReader. (由于我们的实例是使用 1 个图像的缓冲区制作的,因此无论如何它都不可用)

2. 将屏幕截图放在主屏幕上

现在,当我们获得主屏幕的精确屏幕截图时,应该将其放置在最终用户不会注意到差异的位置。我没有选择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);
}
Run Code Online (Sandbox Code Playgroud)

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) {
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我添加了一个Paint类型字段。它将帮助我们绘制叠加层并反映变化。实际上,我并不认为自己是计算机图形学方面的专家,但为了排除任何混乱,我想强调一下,您在问题中使用的方法有些错误 - 您采用了视图的背景可绘制(并且它不实际上并不取决于您的视图背后的内容)并仅对其应用 PorterDuff 模式。因此,背景充当目标颜色,而您在PorterDuffColorFilter构造函数中指定的颜色充当源颜色。只有这两件事融合在一起,其他的都不受影响。但是,当您在 a 上应用 PorterDuff 模式Paint并将其绘制在画布上时,该画布后面(并且属于同一Context)的所有视图都会混合。让我们onDraw首先重写方法,然后在我们的ImageView

@Override
protected void onDraw(@NonNull Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPaint(mOverlayPaint);
}
Run Code Online (Sandbox Code Playgroud)

现在我们只需要为我们的setter添加相应的更改,并通过调用方法让视图重新绘制自身invalidate()

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

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

就是这样!以下是不使用混合乘法模式并应用它的相同效果的示例:


代替结论

当然,这个解决方案远非完美 - 覆盖层完全覆盖主屏幕,为了使其可行,您应该想出很多解决方法来解决极端情况,例如常见交互、键盘、滚动、调用、系统对话框和好多其它的。我尝试了一下,制作了一个快速应用程序,每当用户触摸屏幕时就会隐藏覆盖层。它仍然存在很多问题,但对您来说应该是一个很好的起点。主要问题是让应用程序意识到周围正在发生的事情。我没有检查Accessibility Service,但它应该非常适合,因为与通用服务相比,它具有更多有关用户操作的信息。

请根据需要随意参考我的答案中描述的完整解决方案