如何在全球范围内正确截取屏幕截图?
How to properly take a screenshot, globally?
背景
从AndroidAPI21开始,应用程序可以全局截屏并记录屏幕。
问题
我根据在 Internet 上找到的所有内容制作了一个示例代码,但它存在一些问题:
- 速度很慢。也许有可能避免它,至少对于多个屏幕截图,通过避免通知它被删除直到真的不需要。
- 它左右两边都有黑边,这意味着计算可能有问题:
我试过的
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;
}
}
问题
嗯,关于问题:
- 为什么这么慢?有没有办法改善它?
- 如何避免在截屏之间删除它们的通知?我什么时候可以删除通知?通知是否意味着它会不断截屏?
- 为什么输出位图(目前我没有对它做任何事情,因为它仍然是 POC)中有黑边?这件事的代码有什么问题?
注意:我不想只截取当前应用程序的屏幕截图。我想知道如何在全球范围内为所有应用程序使用它,据我所知,这只有通过使用这个 API 才能正式实现。
编辑:我注意到在 CommonsWare 网站 (here) 上,据说输出位图由于某种原因更大,但与我注意到的相反(开头的黑边距AND 结束),它说它应该在最后:
For inexplicable reasons, it will be a bit larger, with excess unused
pixels on each row on the end.
我已经尝试了那里提供的内容,但它崩溃了 "java.lang.RuntimeException: Buffer not large enough for pixels"。
为什么输出位图(目前我没有对它做任何事情,因为它仍然是 POC)中有黑边?这件事的代码有什么问题吗?
你的屏幕截图周围有黑边,因为你没有使用你所在 window 的 realSize。要解决这个问题:
- 获取 window 的实际大小:
final Point windowSize = new Point();
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getRealSize(windowSize);
- 用它来创建你的图像 reader:
imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES);
- 这第三步可能不是必需的,但我在我的应用程序的生产代码中看到了其他情况(它在各种 android 设备上运行)。当您为 ImageReader 获取图像并从中创建位图时。使用以下代码使用 window 大小裁剪该位图。
// 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 才能正式实现。
要捕获 screen/take 屏幕截图,您需要一个 MediaProjection 对象。要创建这样的对象,您需要一对 resultCode (int) 和 Intent。您已经知道如何获取这些对象并将它们缓存在您的 ScreenshotManager class 中。
回到截取任何应用程序的屏幕截图,您需要遵循获取这些变量 resultCode 和 Intent 的相同过程,但不要将其缓存在本地 class 变量中,而是启动后台服务并传递这些变量与任何其他正常参数相同。看看 Telecine 是如何做到的 here。当这个后台服务启动时,它可以向用户提供一个触发器(一个通知按钮),当点击它时,将执行与您在 ScreenshotManager class 中所做的相同的捕获 screen/taking 屏幕截图的操作。 =15=]
为什么这么慢?有什么办法可以改善吗?
它比您的预期慢了多少?我的媒体投影 API 用例是截取屏幕截图并将其呈现给用户进行编辑。对我来说,速度足够好。我觉得值得一提的是 ImageReader class 可以接受一个 Handler 到 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());
}
背景
从AndroidAPI21开始,应用程序可以全局截屏并记录屏幕。
问题
我根据在 Internet 上找到的所有内容制作了一个示例代码,但它存在一些问题:
- 速度很慢。也许有可能避免它,至少对于多个屏幕截图,通过避免通知它被删除直到真的不需要。
- 它左右两边都有黑边,这意味着计算可能有问题:
我试过的
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;
}
}
问题
嗯,关于问题:
- 为什么这么慢?有没有办法改善它?
- 如何避免在截屏之间删除它们的通知?我什么时候可以删除通知?通知是否意味着它会不断截屏?
- 为什么输出位图(目前我没有对它做任何事情,因为它仍然是 POC)中有黑边?这件事的代码有什么问题?
注意:我不想只截取当前应用程序的屏幕截图。我想知道如何在全球范围内为所有应用程序使用它,据我所知,这只有通过使用这个 API 才能正式实现。
编辑:我注意到在 CommonsWare 网站 (here) 上,据说输出位图由于某种原因更大,但与我注意到的相反(开头的黑边距AND 结束),它说它应该在最后:
For inexplicable reasons, it will be a bit larger, with excess unused pixels on each row on the end.
我已经尝试了那里提供的内容,但它崩溃了 "java.lang.RuntimeException: Buffer not large enough for pixels"。
为什么输出位图(目前我没有对它做任何事情,因为它仍然是 POC)中有黑边?这件事的代码有什么问题吗?
你的屏幕截图周围有黑边,因为你没有使用你所在 window 的 realSize。要解决这个问题:
- 获取 window 的实际大小:
final Point windowSize = new Point();
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getRealSize(windowSize);
- 用它来创建你的图像 reader:
imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES);
- 这第三步可能不是必需的,但我在我的应用程序的生产代码中看到了其他情况(它在各种 android 设备上运行)。当您为 ImageReader 获取图像并从中创建位图时。使用以下代码使用 window 大小裁剪该位图。
// 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 才能正式实现。
要捕获 screen/take 屏幕截图,您需要一个 MediaProjection 对象。要创建这样的对象,您需要一对 resultCode (int) 和 Intent。您已经知道如何获取这些对象并将它们缓存在您的 ScreenshotManager class 中。
回到截取任何应用程序的屏幕截图,您需要遵循获取这些变量 resultCode 和 Intent 的相同过程,但不要将其缓存在本地 class 变量中,而是启动后台服务并传递这些变量与任何其他正常参数相同。看看 Telecine 是如何做到的 here。当这个后台服务启动时,它可以向用户提供一个触发器(一个通知按钮),当点击它时,将执行与您在 ScreenshotManager class 中所做的相同的捕获 screen/taking 屏幕截图的操作。 =15=]
为什么这么慢?有什么办法可以改善吗?
它比您的预期慢了多少?我的媒体投影 API 用例是截取屏幕截图并将其呈现给用户进行编辑。对我来说,速度足够好。我觉得值得一提的是 ImageReader class 可以接受一个 Handler 到 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());
}