Android源码系列(21) -- GlobalScreenshot

March 9, 2019

这篇文章介绍系统如何实现屏幕截取操作,并为拦截截屏事件提供思路。下篇文章 Android源码系列(22) – TakeScreenshotService 将介绍截图如何写入系统磁盘。源码版本 Android 28

一、TakeScreenshotService

TakeScreenshotServiceService 的子类,通过IPC的方式接受截屏请求,并通过 GlobalScreenshot 实现屏幕截取和图片保存逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class TakeScreenshotService extends Service {
    private static final String TAG = "TakeScreenshotService";

    private static GlobalScreenshot mScreenshot;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            final Messenger callback = msg.replyTo;

            // 构建finisher响应Messenger
            Runnable finisher = new Runnable() {
                @Override
                public void run() {
                    Message reply = Message.obtain(null, 1);
                    try {
                        callback.send(reply);
                    } catch (RemoteException e) {
                    }
                }
            };

            // 如果此用户的存储被锁定无法保存屏幕截图,跳过执行而不是显示误导性的动画和错误通知
            if (!getSystemService(UserManager.class).isUserUnlocked()) {
                Log.w(TAG, "Skipping screenshot because storage is locked!");
                // 截图没有保存,发送finisher
                post(finisher);
                return;
            }

            // 初始化GlobalScreenshot
            if (mScreenshot == null) {
                mScreenshot = new GlobalScreenshot(TakeScreenshotService.this);
            }

            // 获取消息类型,根据类型执行操作
            switch (msg.what) {
                // 全屏截取
                case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
                    mScreenshot.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0);
                    break;

                // 局部屏幕截取
                case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
                    mScreenshot.takeScreenshotPartial(finisher, msg.arg1 > 0, msg.arg2 > 0);
                    break;
                    
                // 不支持类型,输出日志后不执行操作
                default:
                    Log.d(TAG, "Invalid screenshot option: " + msg.what);
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        // 返回IBinder支持IPC
        return new Messenger(mHandler).getBinder();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        if (mScreenshot != null) mScreenshot.stopScreenshot();
        return true;
    }
}

二、GlobalScreenshot

此类负责获取截图,下面出现的定义类都是 GlobalScreenshot 的内部类。

1
class GlobalScreenshot

2.1 常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
static final String SHARING_INTENT = "android:screenshot_sharing_intent";

// 动画展示时间的配置
private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130;
private static final int SCREENSHOT_DROP_IN_DURATION = 430;
private static final int SCREENSHOT_DROP_OUT_DELAY = 500;
private static final int SCREENSHOT_DROP_OUT_DURATION = 430;
private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370;
private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320;
private static final float BACKGROUND_ALPHA = 0.5f;
private static final float SCREENSHOT_SCALE = 1f;
private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f;
private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f;
private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f;
private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f;

2.2 数据成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 预览图宽高
private final int mPreviewWidth;
private final int mPreviewHeight;

private Context mContext;
private WindowManager mWindowManager;
private WindowManager.LayoutParams mWindowLayoutParams;
private NotificationManager mNotificationManager;

// 用于测量屏幕宽高
private Display mDisplay;
private DisplayMetrics mDisplayMetrics;
private Matrix mDisplayMatrix;

// 截图的Bitmap
private Bitmap mScreenBitmap;
private View mScreenshotLayout;

// 截图选择器
private ScreenshotSelectorView mScreenshotSelectorView;
private ImageView mBackgroundView;
private ImageView mScreenshotView;
private ImageView mScreenshotFlash;

// 截屏的屏幕动画
private AnimatorSet mScreenshotAnimation;

private int mNotificationIconSize;
private float mBgPadding;
private float mBgPaddingScale;

// 异步保存截图的AsyncTask
private AsyncTask<Void, Void, Void> mSaveInBgTask;

// 截屏时发出模拟快门的声音
private MediaActionSound mCameraSound;

2.3 构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public GlobalScreenshot(Context context) {
    Resources r = context.getResources();
    mContext = context;
    LayoutInflater layoutInflater = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    mDisplayMatrix = new Matrix();
    
    // 填充截屏布局
    mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
    // 绑定View
    mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
    mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
    mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
    mScreenshotSelectorView = (ScreenshotSelectorView) mScreenshotLayout.findViewById(
            R.id.global_screenshot_selector);
    mScreenshotLayout.setFocusable(true); // 令此布局获取焦点
    mScreenshotSelectorView.setFocusable(true);
    mScreenshotSelectorView.setFocusableInTouchMode(true);
    mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // 拦截并抛弃所有触摸事件
            return true;
        }
    });

    // 设置将要使用的window
    mWindowLayoutParams = new WindowManager.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
            WindowManager.LayoutParams.TYPE_SCREENSHOT,
            WindowManager.LayoutParams.FLAG_FULLSCREEN
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
            PixelFormat.TRANSLUCENT);
    mWindowLayoutParams.setTitle("ScreenshotAnimation");
    mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    mNotificationManager =
        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    // 从WindowManager获取Display
    mDisplay = mWindowManager.getDefaultDisplay();
    mDisplayMetrics = new DisplayMetrics();
    // 测量Display参数
    mDisplay.getRealMetrics(mDisplayMetrics);

    // 获取通知图标的尺寸
    mNotificationIconSize =
        r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);

    // 背景的边距
    mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding);
    mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels;

    // 确定最优化的预览尺寸
    int panelWidth = 0;
    try {
        panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width);
    } catch (Resources.NotFoundException e) {
    }

    // panelWidth在上述异常出现时为0
    if (panelWidth <= 0) {
        // includes notification_panel_width==match_parent (-1)
        panelWidth = mDisplayMetrics.widthPixels;
    }
    mPreviewWidth = panelWidth;
    mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height);

    // 加载快门声音
    mCameraSound = new MediaActionSound();
    mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
}

2.4 saveScreenshotInWorkerThread

调用方法时截图已经保存在内存中。此方法会创建新工作任务,并在 AsyncTask 子线程把截图保存到媒体存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void saveScreenshotInWorkerThread(Runnable finisher) {
    // 创建空任务,把参数填到对象中
    SaveImageInBackgroundData data = new SaveImageInBackgroundData();
    data.context = mContext;
    data.image = mScreenBitmap; // 截图
    data.iconSize = mNotificationIconSize;
    data.finisher = finisher;
    data.previewWidth = mPreviewWidth;
    data.previewheight = mPreviewHeight;
    if (mSaveInBgTask != null) {
        mSaveInBgTask.cancel(false);
    }
    // 由execute()可知任务在AsyncTask中串行执行
    mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager)
            .execute();
}

系统很多任务通过 AsyncTask 而不是子线程的方式执行后台任务,类似上面的截图写入到存储的场景。所以一定不能把长耗时任务放入 AsyncTask 导致任务阻塞,或依赖 AsyncTask 完成实时性要求高的工作。

2.5 getDegreesForRotation

获取屏幕旋转角度,矫正图片

1
2
3
4
5
6
7
8
9
10
11
private float getDegreesForRotation(int value) {
    switch (value) {
    case Surface.ROTATION_90:
        return 360f - 90f;
    case Surface.ROTATION_180:
        return 360f - 180f;
    case Surface.ROTATION_270:
        return 360f - 270f;
    }
    return 0f;
}

2.6 takeScreenshot

GlobalScreenshot 初始化完成后,即可截取当前屏幕并展示动画。方法使用 private 修饰,由同类的其他方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
        Rect crop) {
    // 获取屏幕的旋转角度、宽、高
    int rot = mDisplay.getRotation();
    int width = crop.width();
    int height = crop.height();

    // 从SurfaceControl获得截取的Bitmap
    mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot);

    // 检查获取的屏幕截图是否为空
    if (mScreenBitmap == null) {
        notifyScreenshotError(mContext, mNotificationManager,
                R.string.screenshot_failed_to_capture_text);
        finisher.run();
        return;
    }

    // 截图设置为非透明图片,能提升绘制速度
    mScreenBitmap.setHasAlpha(false);
    mScreenBitmap.prepareToDraw();

    // 开始截屏后的动画
    startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
            statusBarVisible, navBarVisible);
}

以下方法截取全屏图片,调用了上面的方法。根据全屏宽高创建 Rect

1
2
3
4
5
6
7
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
    // 计算全屏的DisplayMetrics
    mDisplay.getRealMetrics(mDisplayMetrics);
    // 并把全屏宽高的参数传到方法内
    takeScreenshot(finisher, statusBarVisible, navBarVisible,
            new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
}

2.7 takeScreenshotPartial

takeScreenshot() 截取全屏,此方法能截取屏幕的部分区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible,
        final boolean navBarVisible) {
    // 向WindowManager添加截屏布局
    mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);

    // 准备选择器的点击事件
    mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ScreenshotSelectorView view = (ScreenshotSelectorView) v;
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: // 获取ACTION_DOWN操作,开始截屏
                    view.startSelection((int) event.getX(), (int) event.getY());
                    return true;

                case MotionEvent.ACTION_MOVE: // 选择截屏范围
                    view.updateSelection((int) event.getX(), (int) event.getY());
                    return true;

                case MotionEvent.ACTION_UP: // 手指离开屏幕,结束选择
                    view.setVisibility(View.GONE);
                    mWindowManager.removeView(mScreenshotLayout);
                    // 获取选择的矩形区域
                    final Rect rect = view.getSelectionRect();
                    if (rect != null) {
                        if (rect.width() != 0 && rect.height() != 0) {
                            // 在view消失之后需要mScreenshotLayout处理截图保存的任务
                            mScreenshotLayout.post(new Runnable() {
                                public void run() {
                                    // 把选中的矩形区域作为依据从全屏截取部分图像
                                    takeScreenshot(finisher, statusBarVisible, navBarVisible,
                                            rect);
                                }
                            });
                        }
                    }

                    // 结束选择操作
                    view.stopSelection();
                    return true;
            }

            return false;
        }
    });

    // 显示mScreenshotLayout并发出requestFocus(),开始截屏操作
    mScreenshotLayout.post(new Runnable() {
        @Override
        public void run() {
            mScreenshotSelectorView.setVisibility(View.VISIBLE);
            mScreenshotSelectorView.requestFocus();
        }
    });
}

2.8 stopScreenshot

停止截屏

1
2
3
4
5
6
7
void stopScreenshot() {
    // 如果选择器图层依然呈现在屏幕上,则将其移除并重置其状态
    if (mScreenshotSelectorView.getSelectionRect() != null) {
        mWindowManager.removeView(mScreenshotLayout);
        mScreenshotSelectorView.stopSelection();
    }
}

2.9 startAnimation

截图动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
        boolean navBarVisible) {
    // 手机处于省电模式,显示一个toast提示用于已截屏
    PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
    if (powerManager.isPowerSaveMode()) {
        Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show();
    }

    // 添加动画视图
    mScreenshotView.setImageBitmap(mScreenBitmap);
    mScreenshotLayout.requestFocus();

    // 使用刚拍摄的屏幕截图设置动画,如动画已启动则需要结束
    if (mScreenshotAnimation != null) {
        if (mScreenshotAnimation.isStarted()) {
            mScreenshotAnimation.end();
        }
        mScreenshotAnimation.removeAllListeners();
    }

    mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
    // 通过代码构建动画集合并组装
    ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
    ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
            statusBarVisible, navBarVisible);
    mScreenshotAnimation = new AnimatorSet();
    mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
    mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            // 动画播放结束时启动保存截图的任务
            saveScreenshotInWorkerThread(finisher);
            // 截屏的布局也可以从屏幕移除了
            mWindowManager.removeView(mScreenshotLayout);

            // 清除位图的引用,避免内存泄漏
            mScreenBitmap = null;
            mScreenshotView.setImageBitmap(null);
        }
    });
    mScreenshotLayout.post(new Runnable() {
        @Override
        public void run() {
            // 播放快门声通知用户已截屏
            mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
            // 通过硬件加速播放动画
            mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            mScreenshotView.buildLayer();
            mScreenshotAnimation.start();
        }
    });
}

2.10 notifyScreenshotError

截屏出现错误通知用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void notifyScreenshotError(Context context, NotificationManager nManager, int msgResId) {
    Resources r = context.getResources();
    String errorMsg = r.getString(msgResId);

    // 重新利用现有通知以通知用户错误信息
    Notification.Builder b = new Notification.Builder(context, NotificationChannels.ALERTS)
        .setTicker(r.getString(R.string.screenshot_failed_title))
        .setContentTitle(r.getString(R.string.screenshot_failed_title))
        .setContentText(errorMsg)
        .setSmallIcon(R.drawable.stat_notify_image_error)
        .setWhen(System.currentTimeMillis())
        .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
        .setCategory(Notification.CATEGORY_ERROR)
        .setAutoCancel(true)
        .setColor(context.getColor(
                    com.android.internal.R.color.system_notification_accent_color));

    final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
            Context.DEVICE_POLICY_SERVICE);
    final Intent intent = dpm.createAdminSupportIntent(
            DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE);
    if (intent != null) {
        final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
                context, 0, intent, 0, null, UserHandle.CURRENT);
        b.setContentIntent(pendingIntent);
    }

    SystemUI.overrideNotificationAppName(context, b, true);

    Notification n = new Notification.BigTextStyle(b)
            .bigText(errorMsg)
            .build();
    nManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, n);
}

2.11 ScreenshotActionReceiver

代理分享或编辑intent的 Receiver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static class ScreenshotActionReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        try {
            ActivityManager.getService().closeSystemDialogs(SYSTEM_DIALOG_REASON_SCREENSHOT);
        } catch (RemoteException e) {
        }

        Intent actionIntent = intent.getParcelableExtra(SHARING_INTENT);

        // If this is an edit & default editor exists, route straight there.
        String editorPackage = context.getResources().getString(R.string.config_screenshotEditor);
        if (actionIntent.getAction() == Intent.ACTION_EDIT &&
                editorPackage != null && editorPackage.length() > 0) {
            actionIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
            final NotificationManager nm =
                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
        } else {
            PendingIntent chooseAction = PendingIntent.getBroadcast(context, 0,
                    new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
            actionIntent = Intent.createChooser(actionIntent, null,
                    chooseAction.getIntentSender())
                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
        }

        ActivityOptions opts = ActivityOptions.makeBasic();
        opts.setDisallowEnterPictureInPictureWhileLaunching(true);

        context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT);
    }
}

2.12 TargetChosenReceiver

选择分享或编辑目标后移除截图的通知

1
2
3
4
5
6
7
8
9
public static class TargetChosenReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 移除通知
        final NotificationManager nm =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
    }
}

2.13 DeleteScreenshotReceiver

从存储里移除截图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class DeleteScreenshotReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
            return;
        }

        // 移除通知,先获取NotificationManager
        final NotificationManager nm =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        // 获取SCREENSHOT_URI_ID,构建Uri
        final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
        // 移除截屏通知
        nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);

        // 从媒体存储中删除图片,后台任务串行执行
        new DeleteImageInBackgroundTask(context).execute(uri);
    }
}