这篇文章介绍系统如何实现屏幕截取操作,并为拦截截屏事件提供思路。下篇文章 Android源码系列(22) – TakeScreenshotService 将介绍截图如何写入系统磁盘。源码版本 Android 28。
一、TakeScreenshotService
TakeScreenshotService 是 Service 的子类,通过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 的内部类。
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);
}
}
|