需求总是推动着我进步/(ㄒoㄒ)/~~,最近有了个新的需求:实现一个对桌面局部区域截屏的效果。还记得以前在4.4的版本上实现截屏分享,使用的是View的cacheDrawable,缺点是截不到状态栏、Activity的上层Dialog等,也无法在后台服务中截屏。一直听说5.0之后Google开放了截屏录屏的API,刚好手上有5.1版本的源码,走一波~
相关类
MediaProjection:可以用来捕获屏幕内容或系统声音,可以通过MediaProjectionManager的createScreenCaptureIntent方法创建的Intent来启动。
MediaProjectionManager:MediaProjection的管理类。通过Context.getSystemService()方法获取实例,参数传Context.MEDIA_PROJECTION_SERVICE。
VirtualDisplay:VirtualDisplay将屏幕内容渲染在一个Surface上,当进程终止时会被自动的释放。当不再使用他时,你也应该调用release() 方法来释放资源。通过调用DisplayManager 类的 createVirtualDisplay()方法来创建。
基本流程
- 请求用户授予捕获屏幕内容的权限
- 获取MediaProjection实例
- 获取VirtualDisplay实例
- 通过ImageReader获取截图
- 对图片进行区域裁剪
请求权限
manager = (MediaProjectionManager) application.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(manager.createScreenCaptureIntent(), 1);
首先获取MediaProjectionManager对象,然后通过createScreenCaptureIntent方法获取请求权限的Intent,并通过startActivityForResult启动。
我们简单看一下createScreenCaptureIntent方法:
/**
* Returns an Intent that <b>must</b> passed to startActivityForResult()
* in order to start screen capture. The activity will prompt
* the user whether to allow screen capture. The result of this
* activity should be passed to getMediaProjection.
*/
public Intent createScreenCaptureIntent() {
Intent i = new Intent();
i.setClassName("com.android.systemui",
"com.android.systemui.media.MediaProjectionPermissionActivity");
return i;
}
其实就是启动MediaProjectionPermissionActivity,该Activity会询问用户是否要开启屏幕捕获,成功开启后可以通过getMediaProjection获取MediaProjection实例。
获取MediaProjection实例
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
Log.d(TAG, "onActivityResult: permission request success..");
mediaProjection = manager.getMediaProjection(resultCode, data);
getVirtualDisplay(activity);
} else {
Log.d(TAG, "onActivityResult: permission request defined..");
}
}
请求授权成功后我们可以通过MediaProjectionManager的getMediaProjection方法来获取MediaProjection实例。
再看一下getMediaProjection方法:
/**
* Retrieve the MediaProjection obtained from a succesful screen
* capture request. Will be null if the result from the
* startActivityForResult() is anything other than RESULT_OK.
*
* @param resultCode The result code from {@link android.app.Activity#onActivityResult(int,
* int, android.content.Intent)}
* @param resultData The resulting data from {@link android.app.Activity#onActivityResult(int,
* int, android.content.Intent)}
*/
public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {
if (resultCode != Activity.RESULT_OK || resultData == null) {
return null;
}
IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION);
if (projection == null) {
return null;
}
return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));
}
两个参数分别对应onActivityResult返回的参数,resultCode如果不是-1(RESULT_OK),该方法将返回null。
获取VirtualDisplay实例
private static void getVirtualDisplay(Activity activity) {
int width;
int height;
if (ScreenUtil.isLandspace(activity)) {
width = Capture.screenW + Capture.nativeH;
height = Capture.screenH;
} else {
width = Capture.screenW;
height = Capture.screenH + Capture.nativeH;
}
virtualDisplay = mediaProjection.createVirtualDisplay("demo", width,
height, Capture.screenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.getSurface(),
null, null);
}
通过MediaProjection的createVirtualDisplay方法来获取VirtualDisplay实例,该方法内部调用的是DisplayManager 类的 createVirtualDisplay()方法,有以下几个参数(按顺序):
- name:virtual display 的名字,不能为null。
- width:virtual display的宽(像素),必须大于0。
- height:virtual display的高(像素),必须大于0。
- dpi:virtual display的密度,必须大于0。
- flags:DisplayManager类中的几个常量,标识virtual display类型。
- surface:virtual display内容需要被提供到的那个surface,截图的话我们这里传ImageReader的surface。
- callback:virtual display状态发生改变时的回调。有暂停、恢复、停止三个回调。
- handler:回调时所使用的handler,如果是在主线程回调,可以传null。
需要注意的一点是,如果手机有虚拟键,创建virtualDiaplay实例的时候宽高尺寸需要考虑虚拟导航栏的高度。否则得到的图片会变形。
读取图片
imageReader = ImageReader.newInstance(screenW, screenH, PixelFormat.RGBA_8888, 1);
public static Bitmap screenShot() {
Image image = imageReader.acquireNextImage();
if (image != null) {
int width = image.getWidth();
int height = image.getHeight();
final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
image.close();
return bitmap;
}
return null;
}
注意有个问题就是在获取VirtualDisplay实例后直接获取图片会报错,需要等半秒左右(初始化?),或者为ImageReader设置一个setOnImageAvailableListener监听。
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Log.d(TAG, "onImageAvailable");
Bitmap bitmap = screenShot();
stopVirtual();
}
}, null);
区域裁剪
Bitmap cutBitmap = Bitmap.createBitmap(bitmap, mRect.left, mRect.top, mRect.width(), mRect.height());