您的当前位置:首页正文

通过MediaProjectionManager实现截图、投屏功能(已适配至Android14)

2024-10-31 来源:个人技术集锦

首先,在Manifest文件中添加自定义的前台服务:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

    <application>
        <service android:name=".service.MediaProjectionService"
            android:foregroundServiceType="mediaProjection" />
    </application>

</manifest>
启动投屏服务时,先获取MediaProjectionManager:
MediaProjectionManager MEDIA_PROJECTION_MANAGER = (MediaProjectionManager) App.getApp().getSystemService(Context.MEDIA_PROJECTION_SERVICE)

启动Activity来请求录制屏幕:

// RequestCode自己定义
startActivityForResult(MEDIA_PROJECTION_MANAGER.createScreenCaptureIntent(), RequestCode.START_MEDIA_PROJECTION);

 此时系统会显示请求弹窗:

处理返回结果,判断弹窗成功授权后启动服务:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode != RequestCode.START_MEDIA_PROJECTION) {
            return;
        }
        if (resultCode == Activity.RESULT_OK) {
            // 自定义变量保存resultCode、resultData字段,服务创建时会用到
            MediaProjectionService.resultCode = resultCode;
            MediaProjectionService.resultData = resultData;
            Intent SERVICE_INTENT = new Intent(this, MediaProjectionService.class);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(SERVICE_INTENT);
            } else {
                startService(SERVICE_INTENT);
            }
        } else {
            // 录制服务启动失败
        }
    }

增加服务类型判断:

public interface ServiceType {
    // 截图
    int SCREENSHOT = 0;
    // 投屏
    int PROJECTION = 1;
}

服务创建时(注意,一定要先发送前台服务通知再获取MediaProjection):

    public static int serviceType = ServiceType.SCREENSHOT;
    public static int resultCode;
    public static Intent resultData;

    @Override
    public void onCreate() {
        super.onCreate();
        startMediaProjectionForeground();
        mMediaProjection = MEDIA_PROJECTION_MANAGER.getMediaProjection(resultCode, resultData);
        if (serviceType == ServiceType.SCREENSHOT) {
            createImageReaderVirtualDisplay();
        } else if (serviceType == ServiceType.PROJECTION) {
            createProjectionVirtualDisplay();
        }
    }

发送通知:

    private void startMediaProjectionForeground() {
        NotificationManager NOTIFICATION_MANAGER = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        Notification.Builder notificationBuilder = new Notification.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("服务已启动");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String channelId = "CHANNEL_ID_MEDIA_PROJECTION";
            NotificationChannel channel = new NotificationChannel(channelId, "屏幕录制", NotificationManager.IMPORTANCE_HIGH);
            NOTIFICATION_MANAGER.createNotificationChannel(channel);

            notificationBuilder.setChannelId(channelId);
        }
        Notification notification = notificationBuilder.build();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
        } else {
            startForeground(1, notification);
        }
    }

使用ImageReader记录画面:

private static final MediaProjection.Callback MEDIA_PROJECTION_CALLBACK = new MediaProjection.Callback() {
    };

    private void createImageReaderVirtualDisplay() {
        if (mMediaProjection != null) {
            WindowManager WINDOW_MANAGER = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics dm = new DisplayMetrics();
            WINDOW_MANAGER.getDefaultDisplay().getRealMetrics(dm);

            mImageReader = ImageReader.newInstance(dm.widthPixels, dm.heightPixels, PixelFormat.RGBA_8888, 1);
            mImageReader.setOnImageAvailableListener(reader -> {
                mImageAvailable = true;
            }, null);
            mMediaProjection.registerCallback(MEDIA_PROJECTION_CALLBACK, null);
            mVirtualDisplayImageReader = mMediaProjection.createVirtualDisplay("ImageReader", dm.widthPixels, dm.heightPixels, dm.densityDpi, Display.FLAG_ROUND, mImageReader.getSurface(), null, null);
        }
    }
    public static void screenshot() {
        if (!mImageAvailable) {
            Log.e(TAG, "screenshot: mImageAvailable is false");
            ToastUtils.shortCall("截屏失败");
            return;
        }
        if (mImageReader == null) {
            Log.e(TAG, "screenshot: mImageReader is null");
            ToastUtils.shortCall("截屏失败");
            return;
        }
        try {
            Image image = mImageReader.acquireLatestImage();

            // 获取数据
            int width = image.getWidth();
            int height = image.getHeight();
            final Image.Plane plane = image.getPlanes()[0];
            final ByteBuffer buffer = plane.getBuffer();

            // 重新计算Bitmap宽度,防止Bitmap显示错位
            int pixelStride = plane.getPixelStride();
            int rowStride = plane.getRowStride();
            int rowPadding = rowStride - pixelStride * width;
            int bitmapWidth = width + rowPadding / pixelStride;

            // 创建Bitmap
            Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888);
            bitmap.copyPixelsFromBuffer(buffer);

            // 释放资源
            image.close();

            // 裁剪Bitmap,因为重新计算宽度原因,会导致Bitmap宽度偏大
            Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, width, height);
            bitmap.recycle();

            String fileName = createScreenshotFileName();
            File file = new File(App.getApp().getExternalFilesDir(null).getParent(), fileName);
            if (!file.exists()) {
                file.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(file);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            result.compress(Bitmap.CompressFormat.PNG, 100, bos);
            bos.close();
            result.recycle();
            ToastUtils.longCall("截图成功!"+fileName);
        } catch (IOException e) {
            e.printStackTrace();
            ToastUtils.longCall("截图失败!");
        }
    }

    private static String createScreenshotFileName() {
        Calendar calendar = Calendar.getInstance(Locale.CHINA);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH) + 1;
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);
        return String.format("Screenshot-%d%02d%02d%02d%02d%02d.png", year, month, day, hour, minute, second);
    }

即可实现截图功能。

投屏功能和截图功能类似,只不过需要将SurfaceView的Surface传入其中。方法如下:

    public void createProjectionVirtualDisplay() {
        if (mMediaProjection != null && ProjectionView.isSurfaceCreated()) {
            WindowManager WINDOW_MANAGER = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics dm = new DisplayMetrics();
            WINDOW_MANAGER.getDefaultDisplay().getRealMetrics(dm);
            mMediaProjection.registerCallback(MEDIA_PROJECTION_CALLBACK, null);
            mVirtualDisplayProjection = mMediaProjection.createVirtualDisplay("Projection", dm.widthPixels, dm.heightPixels, dm.densityDpi, Display.FLAG_ROUND, /*Surface*/, null, null);
        }
    }

这里写了一个小Demo,投屏效果演示(小的窗口是悬浮窗,里面是SurfaceView,显示的是当前屏幕的内容):

Demo的源代码:

https://github.com/MagicianGuo/Android-MediaProjectionDemo

Top