【问题标题】:How to properly take a screenshot, globally?如何在全球范围内正确截取屏幕截图?
【发布时间】:2017-04-30 11:14:04
【问题描述】:

背景

从 Android API 21 开始,应用可以在全局范围内截屏并记录屏幕。

问题

我根据在 Internet 上找到的所有内容制作了一个示例代码,但它存在一些问题:

  1. 速度很慢。也许可以避免它,至少对于多个​​屏幕截图,通过避免通知它被删除直到真的不需要。
  2. 左右两边都有黑色边距,这意味着计算可能有问题:

我尝试过的

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.checkIfPossibleToRecordButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                ScreenshotManager.INSTANCE.requestScreenshotPermission(MainActivity.this, REQUEST_ID);
            }
        });
        findViewById(R.id.takeScreenshotButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                ScreenshotManager.INSTANCE.takeScreenshot(MainActivity.this);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ID)
            ScreenshotManager.INSTANCE.onActivityResult(resultCode, data);
    }
}

layout/activity_main.xml

<LinearLayout
    android:id="@+id/rootView"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">


    <Button
        android:id="@+id/checkIfPossibleToRecordButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="request if possible"/>

    <Button
        android:id="@+id/takeScreenshotButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="take screenshot"/>

</LinearLayout>

屏幕截图管理器

public class ScreenshotManager {
    private static final String SCREENCAP_NAME = "screencap";
    private static final int VIRTUAL_DISPLAY_FLAGS = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
    public static final ScreenshotManager INSTANCE = new ScreenshotManager();
    private Intent mIntent;

    private ScreenshotManager() {
    }

    public void requestScreenshotPermission(@NonNull Activity activity, int requestId) {
        MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        activity.startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), requestId);
    }


    public void onActivityResult(int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK && data != null)
            mIntent = data;
        else mIntent = null;
    }

    @UiThread
    public boolean takeScreenshot(@NonNull Context context) {
        if (mIntent == null)
            return false;
        final MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        final MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, mIntent);
        if (mediaProjection == null)
            return false;
        final int density = context.getResources().getDisplayMetrics().densityDpi;
        final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        final Point size = new Point();
        display.getSize(size);
        final int width = size.x, height = size.y;

        // start capture reader
        final ImageReader imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1);
        final VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay(SCREENCAP_NAME, width, height, density, VIRTUAL_DISPLAY_FLAGS, imageReader.getSurface(), null, null);
        imageReader.setOnImageAvailableListener(new OnImageAvailableListener() {
            @Override
            public void onImageAvailable(final ImageReader reader) {
                Log.d("AppLog", "onImageAvailable");
                mediaProjection.stop();
                new AsyncTask<Void, Void, Bitmap>() {
                    @Override
                    protected Bitmap doInBackground(final Void... params) {
                        Image image = null;
                        Bitmap bitmap = null;
                        try {
                            image = reader.acquireLatestImage();
                            if (image != null) {
                                Plane[] planes = image.getPlanes();
                                ByteBuffer buffer = planes[0].getBuffer();
                                int pixelStride = planes[0].getPixelStride(), rowStride = planes[0].getRowStride(), rowPadding = rowStride - pixelStride * width;
                                bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Config.ARGB_8888);
                                bitmap.copyPixelsFromBuffer(buffer);
                                return bitmap;
                            }
                        } catch (Exception e) {
                            if (bitmap != null)
                                bitmap.recycle();
                            e.printStackTrace();
                        }
                        if (image != null)
                            image.close();
                        reader.close();
                        return null;
                    }

                    @Override
                    protected void onPostExecute(final Bitmap bitmap) {
                        super.onPostExecute(bitmap);
                        Log.d("AppLog", "Got bitmap?" + (bitmap != null));
                    }
                }.execute();
            }
        }, null);
        mediaProjection.registerCallback(new Callback() {
            @Override
            public void onStop() {
                super.onStop();
                if (virtualDisplay != null)
                    virtualDisplay.release();
                imageReader.setOnImageAvailableListener(null, null);
                mediaProjection.unregisterCallback(this);
            }
        }, null);
        return true;
    }
}

问题

这是关于问题的:

  1. 为什么这么慢?有没有办法改进它?
  2. 如何避免在截屏之间删除它们的通知?我什么时候可以删除通知?通知是否意味着它会不断截取屏幕截图?
  3. 为什么输出位图(目前我不使用它,因为它仍然是 POC)有黑色边距?这件事的代码有什么问题?

注意:我不想只截取当前应用的屏幕截图。我想知道如何在全球范围内为所有应用程序使用它,据我所知,只有通过使用这个 API 才能正式实现。


编辑:我注意到在 CommonsWare 网站 (here) 上,据说输出位图由于某种原因更大,但与我注意到的相反(开头和结尾的黑色边距),它说它应该在最后:

由于莫名其妙的原因,它会有点大,多余的未使用 每行末尾的像素。

我已经尝试过那里提供的内容,但它崩溃并出现异常“java.lang.RuntimeException:缓冲区不足以容纳像素”。

【问题讨论】:

  • 我没有仔细看这个,但你可能会错过真正的屏幕尺寸(DisplayMetrics.getRealMetrics),只抓取窗口尺寸。这是一个可能有帮助的工作示例:github.com/mattprecious/telescope/blob/…
  • 对不起,我不明白你的通知问题。使用 Media Projection API 时,状态栏中会显示一个投影图标。还会向用户显示一个对话框,通知他们他们的屏幕将被正在启动从媒体投影服务获取的屏幕捕获意图对象的应用程序捕获。
  • @AnkitBatra 是的,我希望“投影图标”保留,因为我认为它可以避免重新初始化整个过程。是否可以避免屏幕捕获之间的重新初始化?目前它显示,隐藏,显示,隐藏......

标签: android screenshot android-mediaprojection


【解决方案1】:

为什么输出位图(目前我没有用它做任何事情,因为它仍然是 POC)有黑色边距?这件事的代码有什么问题?

您的屏幕截图周围有黑色边距,因为您没有使用您所在窗口的 realSize。要解决这个问题:

  1. 获取窗口的实际大小:
final Point windowSize = new Point(); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); windowManager.getDefaultDisplay().getRealSize(windowSize);
  1. 使用它来创建您的图像阅读器:
imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES);
  1. 这第三步可能不是必需的,但我在我的应用程序的生产代码中看到了其他情况(在各种安卓设备上运行)。当您为 ImageReader 获取图像并从中创建位图时。使用以下代码使用窗口大小裁剪该位图。
// fix the extra width from Image Bitmap croppedBitmap; try { croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, windowSize.x, windowSize.y); } catch (OutOfMemoryError e) { Timber.d(e, "Out of memory when cropping bitmap of screen size"); croppedBitmap = bitmap; } if (croppedBitmap != bitmap) { bitmap.recycle(); }

我不想只截取当前应用的屏幕截图。我想知道如何在全球范围内对所有应用程序使用它,据我所知,只有通过使用这个 API 才能正式实现。

要捕获屏幕/截取屏幕截图,您需要一个 MediaProjection 对象。要创建这样的对象,您需要一对 resultCode (int) 和 Intent。您已经知道如何获取这些对象并将它们缓存在 ScreenshotManager 类中。

回到截取任何应用程序的屏幕截图,您需要遵循相同的过程来获取这些变量 resultCode 和 Intent ,但不要将其缓存在您的类变量中,而是启动后台服务并将这些变量传递给相同的任何其他正常参数。看看 Telecine 是如何做到的here。当这个后台服务启动时,它可以向用户提供一个触发器(一个通知按钮),当用户点击它时,它将执行与您在 ScreenshotManager 类中所做的相同的捕获屏幕/截取屏幕截图的操作。

为什么这么慢?有没有办法改进它?

比您的预期慢多少?我的媒体投影 API 用例是截取屏幕截图并将其呈现给用户进行编辑。对我来说速度已经足够了。我觉得值得一提的是 ImageReader 类可以接受一个 Handler 到 setOnImageAvailableListener 中的一个线程。如果您在那里提供处理程序,onImageAvailable 回调将在处理程序线程上触发,而不是在创建 ImageReader 的线程上触发。这将帮助您在图像可用时不创建 AsyncTask(并启动它),而回调本身将发生在后台线程上。这就是我创建 ImageReader 的方式:

private void createImageReader() { startBackgroundThread(); imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES); ImageHandler imageHandler = new ImageHandler(context, domainModel, windowSize, this, notificationManager, analytics); imageReader.setOnImageAvailableListener(imageHandler, backgroundHandler); } private void startBackgroundThread() { backgroundThread = new HandlerThread(NAME_VIRTUAL_DISPLAY); backgroundThread.start(); backgroundHandler = new Handler(backgroundThread.getLooper()); }

【讨论】:

  • 边距问题解决了,通过使用更好的尺寸,然后裁剪。关于速度,使用处理程序没有帮助。但是,您能否展示如何让通知停留在屏幕截图之间?例如,我想在某个时间触发屏幕截图,然后在其他事情触发它时触发。我认为这至少会提高第一次捕获后的捕获速度,因为它不需要每次都重新初始化。
  • newInstance的解决方案不允许有太多的图片(例如1000),即使只有几张图片(10),通知仍然会被删除。
  • 看起来确实很快。只是对于用户来说,指示很慢并且不会立即消失。如果您知道如何避免这种奇怪的用户体验(通过让指示保持到我完成所有屏幕截图),请告诉我。目前,这是正确答案,我会标记它。
  • 如果您希望投影图标保持不变,那么您必须禁止在代码中调用 mediaProjection.stop() 来停止投影并最终隐藏图标。但是,我想如果您让媒体投影在后台运行,可能会出现一些问题(例如速度慢、电池耗尽等)。我认为没有办法控制状态栏中显示的投影图标。但是我在这个方向上还没有进行足够的调查。另外,我觉得你可以通过向用户解释/提示图标来创造更好的用户体验,这样你就不会在用户体验上失去太多。
  • 请粘贴您的代码或分享链接,以便社区查看。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-10
  • 2011-08-16
  • 1970-01-01
相关资源
最近更新 更多