web-dev-qa-db-ja.com

グローバルにスクリーンショットを正しく撮る方法は?

バックグラウンド

Android API 21)以降、アプリはスクリーンショットをグローバルに取得して画面を記録することができます。

問題

インターネットで見つけたすべてのサンプルコードを作成しましたが、いくつかの問題があります。

  1. かなり遅いです。おそらく、少なくとも複数のスクリーンショットでは、本当に必要なくなるまで削除されるという通知を回避することで、それを回避することが可能です。
  2. 左右に黒い余白があり、計算に問題がある可能性があります。

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

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>

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であるため、現在は何もしていません)に黒い余白があるのはなぜですか?この問題のコードの何が問題になっていますか?

注:現在のアプリのスクリーンショットだけを撮りたくありません。私の知る限り、このAPIを使用することによってのみ公式に可能になる、すべてのアプリでグローバルに使用する方法を知りたいです。


編集:CommonsWareのWebサイト( here )で、出力ビットマップが一部の人にとって大きいと言われていることに気づきました理由が、私が気づいたこと(最初と最後の黒いマージン)とは対照的に、それは最後にあるはずだと言っています:

説明できない理由で、それは少し大きくなり、最後の各行に余分な未使用のピクセルがあります。

そこで提供されているものを試しましたが、「Java.lang.RuntimeException:バッファがピクセルに対して十分な大きさではありません」という例外でクラッシュします。

16

出力ビットマップ(まだ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. この3番目の手順は必須ではないかもしれませんが、アプリの本番コード(さまざまなAndroidデバイスで実行されます)で別の方法で確認しました)。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がどのようにそれを行うかを見てください ここ 。このバックグラウンドサービスが開始されると、ユーザーにトリガー(通知ボタン)を提供できます。このトリガーをクリックすると、ScreenshotManagerクラスで実行するのと同じ操作で画面をキャプチャ/スクリーンショットを取得します。

なぜそんなに遅いのですか?それを改善する方法はありますか?

あなたの期待にどれくらい遅いですか? Media Projection APIの私の使用例は、スクリーンショットを撮り、編集のためにユーザーに提示することです。私にとって、速度は十分にまともです。言及する価値があると思うことの1つは、ImageReaderクラスがsetOnImageAvailableListenerのスレッドへのハンドラーを受け入れることができるということです。そこでハンドラーを指定すると、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());
        }

11
Ankit Batra