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

mzmfm0qo  于 2022-11-27  发布在  Android
关注(0)|答案(1)|浏览(180)

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

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使用模式下倍增(想想Photoshop混合模式)?
从我的代码中可以看出,我也尝试过使用PorterDuff过滤器模式(它们的几种不同组合)来实现它,但都是徒劳的。
这里有几个截图来更好地解释这一点:
x1c 0d1x〈--没有我的服务的原始屏幕。


〈--打开当前代码服务时的相同屏幕。


〈--同一屏幕上的预期效果。注意较深的颜色是如何叠加到下面的。
正如你所看到的,我目前的代码只抛出了一层纯色。我感谢所有的建议。提前感谢!

rsaldnfx

rsaldnfx1#

对于那些仍然想知道这样的事情是否可能的人,我决定分享我解决这个问题的经验。
长话短说
Android的设计,所以它不允许应用颜色混合在不同上下文的视图(我实际上无法证明这一说法,这纯粹是从我的个人经验),所以要实现你想要的,你将需要首先以某种方式渲染一个目标图像的表面,这属于你的应用程序上下文之一.

0.概述

至少出于安全考虑,没有办法在应用程序中播放android主屏幕(否则,将能够通过使应用程序的外观和行为完全像系统主屏幕来填充用户输入,从而窃取个人数据和密码)。(Lollipop)引入了所谓MediaProjection类,它引入了录制屏幕的可能性。我不认为在没有root设备的情况下,较低的API是可能的。(如果您可以,您可以使用Runtime类的exec命令在应用程序中自由使用adb shell screencap命令)。如果您有主屏幕的截图,你可以在你的应用程序中创造一种主屏幕的感觉,然后应用你的颜色混合。不幸的是,这个屏幕录制功能将记录覆盖本身,以及,所以你将不得不记录屏幕只有当覆盖是隐藏的。

1.截图

使用MediaProjection进行屏幕截图并不困难,但需要花费一些精力来执行。它还要求您有一个Activity来向用户请求屏幕录制权限。首先,您需要请求使用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;
    }
}

最后,您可以从MediaProjectionManager系统服务获取一个MediaProjection示例(使用先前返回的intent数据):

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应使用真实的设备大小进行示例化(包括状态栏和导航按钮栏,如果它作为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);

现在,每当VirtualDisplay发出新的图像时,ImageRender就会将其发送到侦听器。侦听器只包含一个方法,如下所示:

@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);
}

在图像处理完成后,你应该调用ImageReaderclose()方法。它释放了它分配的所有资源,同时使ImageReder不可用。因此,当你需要拍摄下一张截图时,你必须示例化另一个ImageReader。(由于我们的示例是使用1张图像的缓冲区创建的,因此它无论如何都是不可用的)

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

现在,当我们得到主屏幕的精确截图时,应该将其放置在最终用户不会注意到差异的位置。我选择了具有类似布局参数的ImageView,而不是您在问题中使用的LinearLayout。由于它已经覆盖了整个屏幕,唯一剩下的事情就是调整截图在其中的位置。因此它不包含状态栏和导航栏按钮(这是因为屏幕录制实际上是捕获整个设备屏幕,而我们的覆盖视图只能放置在“可绘制”区域内)。为了实现这一点,我们可以使用简单的矩阵转换:

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类型的字段。它将帮助我们绘制覆盖图并反映更改。我实际上并不认为自己是计算机图形方面的Maven,但为了排除任何混淆,我想强调的是,你在问题中使用的方法有些错误--你把你的观点的背景画出来了(它实际上并不取决于视图后面的内容),并只对它应用PorterDuff模式。因此,背景作为目标颜色,而在PorterDuffColorFilter构造函数中指定的颜色作为源颜色。只有这两种颜色被混合,其他颜色不受影响。但是,当您在Paint上应用PorterDuff模式并在画布上绘制它时,该画布后面的所有视图(并且属于同一个Context)都将被混合。让我们首先覆盖onDraw方法,然后在ImageView中绘制绘画:

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

现在我们只需要为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();
}

就是这样!下面是不使用混合乘法模式和应用混合乘法模式的相同效果的示例:
x1c 0d1x

代替结论
当然,这个解决方案还远远不够完美--覆盖层完全覆盖了主屏幕,为了使它可行,你应该想出很多解决方案来解决一些角落的问题,比如常见的交互、键盘、滚动、调用、系统对话框和许多其他问题。我试了一下,做了一个快速的应用程序,当用户触摸屏幕时,它会隐藏覆盖层。它仍然有很多问题,但对您来说应该是一个很好的起点。主要的问题是让应用程序意识到周围正在发生的事情。我没有检查Accessibility Service,但它应该非常适合,因为与普通服务相比,它有更多关于用户操作的信息。
如果需要,请随时参考我的答案here中描述的完整解决方案。

相关问题