如何在全球范围内正确截取屏幕截图?

时间:2017-04-30 11:14:05

标签: android screenshot android-mediaprojection

背景

从Android API 21开始,应用程序可以全局截取屏幕并记录屏幕。

问题

我已经在互联网上发现了一些示例代码,但它有一些问题:

  1. 这很慢。也许有可能避免它,至少对于多个​​屏幕截图,避免通知它被删除直到真的不需要。
  2. 左右边缘有黑色边距,这意味着计算可能出错:
  3. enter image description here

    我尝试了什么

    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>
    

    ScreenshotManager

    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)中有黑色边距?这件事的代码有什么问题?
    4. 注意:我不想仅截取当前应用的屏幕截图。我想知道如何在全球范围内使用它,对于所有应用程序,据我所知,这只能通过使用此API正式使用。

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

        

      出于莫名其妙的原因,它会有点大,过剩未使用   最后每行的像素。

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

1 个答案:

答案 0 :(得分:10)

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

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

  1. 获取窗口的实际大小:
  2. 
    
        final Point windowSize = new Point();
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getRealSize(windowSize);
    
    
    
    1. 使用它来创建图像阅读器:
    2. 
      
          imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES);
      
      
      
      1. 可能不需要这第三步,但我在我的应用程序的生产代码中看到了其他步骤(在各种Android设备上运行)。当您为ImageReader获取图像并从中创建位图时。使用以下代码使用窗口大小裁剪位图。
      2. 
        
            // 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类可以接受setOnImageAvailableListener中线程的Handler。如果在那里提供处理程序,将在处理程序线程而不是创建ImageReader的线程上触发onImageAvailable回调。这将帮助您在图像可用时不创建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());
                }