对于那些仍然想知道这样的事情是否可能的人,我决定分享我解决这个问题的经验。
长话短说
Android 的设计不允许在不同上下文的视图中应用颜色混合(我实际上无法证明这一说法,这纯粹是我个人的经验),所以要实现你想要的,你首先需要以某种方式渲染表面上的目标图像,属于您的应用程序上下文之一。
0. 概述
至少出于安全原因,无法在应用程序中仅广播 android 主屏幕(否则,通过使应用程序看起来和行为与系统主屏幕完全相同,可以通过填充用户输入来窃取个人数据和密码) . 然而,在 Android API 21 (Lollipop) 中引入了所谓的MediaProjection
类,它引入了记录屏幕的可能性。我不认为在不生根设备的情况下使用较低的 API 是不可能的(如果对您来说没问题,您可以使用adb shell screencap
应用程序中exec
的命令自由使用命令Runtime
班级)。如果您有主屏幕的屏幕截图,您可以在应用程序中创建主屏幕的感觉,然后应用您的颜色混合。不幸的是,此屏幕录制功能也会记录叠加层本身,因此您必须仅在隐藏叠加层时记录屏幕。
1. 截图
用 截图MediaProjection
并不难,但需要一些努力来执行。它还要求您有一个 Activity 来向用户询问屏幕录制权限。首先,您需要获得使用媒体项目服务的许可:
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.将屏幕截图放在主屏幕上
现在,当我们获得主屏幕的精确屏幕截图时,应该将其放置以便最终用户不会注意到差异。我选择了具有类似布局参数的,而不是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
构造函数中指定的颜色用作源颜色。只有这两种东西混合在一起,没有其他东西受到影响。但是,当您在 a 上应用 PorterDuff 模式Paint
并将其绘制在画布上时,该画布后面(并且属于同一个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();
}
而已!这是没有混合乘法模式并应用它的相同效果的示例:

代替结论
当然,这个解决方案远非完美——覆盖层与主屏幕完全重叠,为了使其可行,你应该想出很多解决方法来解决常见的交互、键盘、滚动、呼叫、系统对话框和好多其它的。我试了一下,做了一个快速的应用程序,只要用户触摸屏幕,它就会隐藏覆盖。它仍然有很多问题,但对你来说应该是一个很好的起点。主要问题是让应用程序意识到周围正在发生的事情。我没有检查Accessibility Service,但它应该非常适合,因为与普通服务相比,它具有更多关于用户操作的信息。您可以在这里查看我的解决方案 - https://github.com/AlexandrSMed/stack-35590953