如何全局地正确地截取屏幕截图?

17

背景

自 Android API 21 起,应用程序可以全局截屏并录制屏幕。

问题

我在互联网上找到了一些示例代码,但是它存在一些问题:

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

布局文件: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 网站上(这里),它说输出的位图由于某种原因会更大,但与我注意到的情况(开头和结尾都有黑色边距)相反,它应该在结尾处:

由于无法解释的原因,它会稍微大一点,在每一行的末尾都有多余的未使用像素。

我尝试了那里提供的内容,但它会崩溃并显示“java.lang.RuntimeException:缓冲区对于像素来说不够大”异常。


我没有详细查看,但你可能会错过真实的屏幕尺寸(DisplayMetrics.getRealMetrics),而只抓取窗口大小。 这里有一个可行的示例,可能会有所帮助: https://github.com/mattprecious/telescope/blob/ce5e2710fb16ee214026fc25b091eb946fdbbb3c/telescope/src/main/java/com/mattprecious/telescope/TelescopeLayout.java - Eric Cochran
抱歉,我不明白你的通知关注点。当使用媒体投影API时,状态栏中会显示一个投影图标。还会向用户呈现对话框,通知他们屏幕将被应用程序捕获,该应用程序从媒体投影服务获取了屏幕捕获意图对象。 - Ankit Batra
@AnkitBatra 是的,我希望“投影图标”保持不变,因为我认为这样可以避免重新初始化整个过程。是否有可能在屏幕截图之间避免重新初始化?目前它会显示、隐藏、显示、隐藏... - android developer
1个回答

12

为什么输出的位图(目前我还没有对它进行任何处理,因为它仍然是POC)有黑色边距?在这方面代码有什么问题吗?

您的屏幕截图周围有黑色边框,因为您没有使用窗口的真实大小。要解决此问题:

  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. 这第三步可能不是必需的,但我在我的应用程序的生产代码中看到了另外一种情况(它在各种Android设备上运行)。当您获取ImageReader的图像并创建位图时,请使用以下代码使用窗口大小裁剪该位图。


    // 修复Image带来的额外宽度
    Bitmap croppedBitmap;
    try {
        croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, windowSize.x, windowSize.y);
    } catch (OutOfMemoryError e) {
        Timber.d(e, "在裁剪屏幕尺寸的位图时内存不足");
        croppedBitmap = bitmap;
    }
    if (croppedBitmap != bitmap) {
        bitmap.recycle();
    }


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

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

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

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

你的期望速度有多慢?我使用媒体投影API是为了截屏并将其呈现给用户进行编辑。对我来说,速度足够快。值得一提的是,ImageReader类可以接受处理程序以在setOnImageAvailableListener中的线程上触发onImageAvailable回调,而不是创建ImageReader的那个线程。如果在那里提供处理程序,则回调本身将在后台线程上发生,而无需创建AsyncTask(并启动它)即可在图像可用时。这是我创建ImageReader的方式:

边距问题已经解决,通过使用更好的尺寸,然后进行裁剪。关于速度,使用处理程序并没有帮助。但是,您能否请展示如何让通知在屏幕截图之间保持?例如,我想在特定时间触发屏幕截图,稍后再由其他东西触发它。我认为这至少会提高第一次截图后捕获的速度,因为它不需要每次重新初始化。 - android developer
"newInstance"的解决方案不允许有太多的图像(例如1000),即使只有几张图片(10),通知仍然会被删除。 - android developer
它看起来实际上很快。只是对于用户来说,指示相当慢,并且不会立即消失。如果您知道如何避免这种奇怪的UX(通过让指示留到我完成所有截图),请告诉我。目前,这是正确的答案,我会标记它。 - android developer
如果您想让投影图标保持显示,则必须禁止在代码中调用mediaProjection.stop(),该方法会停止投影并最终隐藏图标。然而,如果您让媒体投影在后台运行,可能会出现一些问题(如缓慢、电池耗尽等)。我认为没有办法控制状态栏中显示的投影图标。但是我还没有在这个方向上进行足够的调查。此外,我认为通过向用户解释/提示有关图标的信息,可以创造更好的用户体验,以便您不会在用户体验方面失去太多。 - Ankit Batra
1
请粘贴您的代码或分享链接,以便社区查看。 - Ankit Batra
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接