使用MediaProjection API截屏保存Bitmap:深入解析与实战
Android系统提供了MediaProjection API,用于开发者构建屏幕录制或截图应用。通过MediaProjection API,开发者可以获取设备屏幕的实时画面,并将其转换为视频或图像进行保存。
1. MediaProjection API简介
MediaProjection API主要包含以下几个核心概念:
- MediaProjectionManager: 用于获取MediaProjection对象,它是控制屏幕投影的核心类。
- MediaProjection: 代表着对设备屏幕的投影权限,用于创建VirtualDisplay对象。
- VirtualDisplay: 虚拟显示器,用于接收屏幕画面数据。
- ImageReader: 用于接收VirtualDisplay输出的图像帧,并将其转换为Bitmap或Image对象。
2. MediaProjection API的使用流程
使用MediaProjection API截屏保存Bitmap的基本流程如下:
- 检查权限: 首先需要检查是否有必要的权限,例如
READ_EXTERNAL_STORAGE
和RECORD_AUDIO
。 - 获取MediaProjection对象: 通过MediaProjectionManager获取MediaProjection对象,这需要用户授予相应的权限。
- 创建VirtualDisplay对象: 使用MediaProjection对象创建VirtualDisplay对象,并指定屏幕尺寸和像素格式。
- 创建ImageReader对象: 创建ImageReader对象,并指定ImageReader的尺寸和像素格式,确保与VirtualDisplay一致。
- 监听ImageReader回调: 监听ImageReader的ImageAvailable回调,并在回调中获取Image对象。
- 获取Bitmap: 从Image对象中获取Bitmap对象,并进行保存或处理。
3. MediaProjection API的使用要点和注意事项
- 权限申请: 必须在使用MediaProjection API之前检查并申请必要的权限,否则会导致运行时异常。
- 屏幕尺寸和像素格式: 创建VirtualDisplay和ImageReader时,需要指定正确的屏幕尺寸和像素格式,否则可能导致图像变形或无法显示。
- ImageReader回调: 必须在ImageReader的ImageAvailable回调中处理Image对象,否则会导致内存泄漏。
- 线程安全: MediaProjection API涉及多线程操作,需要确保代码的线程安全。
4. Demo案例:截取屏幕Bitmap
以下是一个简单的Demo案例,演示如何使用MediaProjection API截取屏幕Bitmap:
public class ScreenshotActivity extends Activity {
private MediaProjectionManager mediaProjectionManager;
private MediaProjection mediaProjection;
private VirtualDisplay virtualDisplay;
private ImageReader imageReader;
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screenshot);
// Check permissions
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
return;
}
// Get MediaProjectionManager
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
// Request screenshot permission
startActivityForResult(mediaProjectionManager.createProjectionIntent(), REQUEST_CODE_SCREENSHOT);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_SCREENSHOT && resultCode == Activity.RESULT_OK) {
// Get MediaProjection object
mediaProjection = mediaProjectionManager.getMediaProjectionFromIntent(data);
if (mediaProjection != null) {
// Create VirtualDisplay object
virtualDisplay = createVirtualDisplay();
// Create ImageReader object
imageReader = ImageReader.newInstance(virtualDisplay.getWidth(), virtualDisplay.getHeight(), PixelFormat.RGBA_8888, 1);
// Listen for ImageReader callback
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image != null) {
Image.Plane[] planes = image.getPlanes();
if (planes.length > 0) {
ByteBuffer buffer = planes[0].getBuffer();
int stride = planes[0].getPixelStride();
int width = image.getWidth();
int height = image.getHeight();
// Create Bitmap object
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
buffer.rewind();
bitmap.copyPixelsFromBuffer(buffer, 0, width * stride, 0, 0, width, height);
// Save the bitmap to external storage
saveBitmapToExternalStorage(bitmap);
// Release resources
image.close();
bitmap.recycle();
}
}
}
});
}
}
}
private VirtualDisplay createVirtualDisplay() {
DisplayMetrics metrics = getResources().getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels;
String packageName = getPackageName();
return mediaProjection.createVirtualDisplay(packageName, width, height, PixelFormat.RGBA_8888, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR);
}
private void saveBitmapToExternalStorage(Bitmap bitmap) {
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/screenshot.png";
try {
FileOutputStream outputStream = new FileOutputStream(path);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.close();
Toast.makeText(this, "Screenshot saved to: " + path, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "Failed to save screenshot", Toast.LENGTH_SHORT).show();
}
}
private static final int REQUEST_CODE_SCREENSHOT = 101;
private static final int VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR = 0x1;
}
当然,这里是使用 MediaProjection API 捕获屏幕截图所提供的代码片段的完整解释:
1. onCreate() 方法:
- 权限检查:
- 代码
READ_EXTERNAL_STORAGE
使用来检查是否授予权限checkSelfPermission()
。 - 如果未授予权限,则调用来向用户请求权限。
requestPermissions()
- 该
REQUEST_CODE_SCREENSHOT
常量用于识别权限请求。 - 如果已经授予权限,代码将继续获取
MediaProjectionManager
。
- 代码
- 获取 MediaProjectionManager:
- 该
getSystemService()
方法用于检索MEDIA_PROJECTION_SERVICE
系统服务。 - 该服务提供对
MediaProjectionManager
实例的访问。
- 该
2. onActivityResult() 方法:
-
处理屏幕截图权限结果:
- 该方法检查是否
requestCode
是REQUEST_CODE_SCREENSHOT
以及是否resultCode
是Activity.RESULT_OK
。 - 如果是,则表示用户已经授予了屏幕截图的权限。
- 该方法检查是否
-
获取 MediaProjection 对象:
getMediaProjectionFromIntent()
调用的方法来MediaProjectionManager
获取MediaProjection
对象。- 该对象代表授予的屏幕捕获权限并允许访问屏幕的内容。
-
创建虚拟显示:
- 调用该
createVirtualDisplay()
方法来创建一个VirtualDisplay
对象。 VirtualDisplay
是镜像设备真实屏幕的虚拟屏幕。- 该方法采用以下参数:
packageName
:请求捕获的应用程序的包名称。width
:虚拟屏幕的宽度(与真实屏幕宽度相同)。height
:虚拟屏幕的高度(与真实屏幕高度相同)。pixelFormat
:虚拟屏幕的像素格式(在本例中为)。PixelFormat.RGBA_8888
flags
:是否将真实屏幕镜像到虚拟屏幕的标志(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
使用)。
- 调用该
-
创建 ImageReader:
-
创建该
ImageReader
对象是为了接收来自 的帧VirtualDisplay
。 -
该
newInstance()
方法采用以下参数:width
:宽度ImageReader
(与width相同VirtualDisplay
)。height
:高度ImageReader
(与高度相同VirtualDisplay
)。pixelFormat
:像素格式ImageReader
(与像素格式相同VirtualDisplay
)。maxImages
:可以缓冲的最大图像数量ImageReader
(在本例中设置为 1)。
-
-
设置 ImageReader 回调:
- 调用该
setOnImageAvailableListener()
方法来为设置回调监听器ImageReader
。 onImageAvailable()
每当有新的图像帧可用时,就会调用回调方法( )VirtualDisplay
。
- 调用该
3. onImageAvailable()方法:
-
获取图像:
acquireLatestImage()
调用的方法来ImageReader
检索最新的可用图像帧。- 如果图像可用,则返回该图像;否则,返回。
null
-
处理图像:
- 如果获取了图像:
- 使用 获取图像的平面
getImagePlanes()
。 - 如果有平面(通常一个用于图像数据),则使用从第一个平面获取缓冲区
getBuffer()
。 - 使用 获取平面的步幅(像素宽度)
getPixelStride()
。 getWidth()
使用和获取图像的宽度和高度getHeight()
。
- 使用 获取图像的平面
- 如果获取了图像:
-
创建位图:
- 使用图像宽度、高度和配置创建一个
Bitmap
对象。Bitmap.createBitmap()``Config.ARGB_8888
- 使用图像宽度、高度和配置创建一个
-
将像素复制到位图:
-
使用 倒回缓冲区
buffer.rewind()
。 -
将像素数据从缓冲区复制到
Bitmap
使用bitmap.copyPixelsFromBuffer()
。 -
该
copyPixelsFromBuffer()
方法采用以下参数:buffer
:包含像素数据的源缓冲区。offset
:缓冲区中开始复制的偏移量(以字节为单位)
-