前言 涉及到的源碼有 frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java vendor\mediatek\proprietary\packages\apps\SystemUI\src ...
前言
涉及到的源碼有
frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java
vendor\mediatek\proprietary\packages\apps\SystemUI\src\com\android\systemui\screenshot\TakeScreenshotService.java
vendor\mediatek\proprietary\packages\apps\SystemUI\src\com\android\systemui\screenshot\GlobalScreenshot.java
按鍵處理都是在 PhoneWindowManager 中,真正截屏的功能實現在 GlobalScreenshot 中, PhoneWindowManager 和 systemui 通過 bind TakeScreenshotService 來實現截屏功能
流程
一般未經過特殊定製的 Android 系統,截屏都是通過同時按住音量下鍵和電源鍵來截屏,後來我們使用的一些華為、oppo等廠商的系統你會發現可以通過三指滑動來截屏,下一篇我們會定製此功能,而且截屏顯示風格類似 iphone 在左下角顯示截屏縮略圖,點擊可跳轉放大查看,3s 無操作後向左自動滑動消失。
好了,現在我們先來理一下系統截屏的流程
system_process D/WindowManager: interceptKeyTi keyCode=25 down=true repeatCount=0 keyguardOn=false mHomePressed=false canceled=false metaState:0
system_process D/WindowManager: interceptKeyTq keycode=25 interactive=true keyguardActive=false policyFlags=22000000 down =false canceled = false isWakeKey=false mVolumeDownKeyTriggered =true result = 1 useHapticFeedback = false isInjected = false
system_process D/WindowManager: interceptKeyTi keyCode=25 down=false repeatCount=0 keyguardOn=false mHomePressed=false canceled=false metaState:0
system_process D/WindowManager: interceptKeyTq keycode=26 interactive=true keyguardActive=false policyFlags=22000000 down =false canceled = false isWakeKey=false mVolumeDownKeyTriggered =false result = 1 useHapticFeedback = false isInjected = false
上面是按下音量下鍵和電源鍵的日誌,音量下鍵對應 keyCode=25 ,電源鍵對應 keyCode=26,來看到 PhoneWindowManager 中的 interceptKeyBeforeQueueing() 方法,在此處處理按鍵操作
/** {@inheritDoc} */
@Override
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
if (!mSystemBooted) {
// If we have not yet booted, don't let key events do anything.
return 0;
}
.....
if (DEBUG_INPUT) {
Log.d(TAG, "interceptKeyTq keycode=" + keyCode
+ " interactive=" + interactive + " keyguardActive=" + keyguardActive
+ " policyFlags=" + Integer.toHexString(policyFlags));
}
.....
// Handle special keys.
switch (keyCode) {
.......
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (down) {
if (interactive && !mScreenshotChordVolumeDownKeyTriggered
&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
mScreenshotChordVolumeDownKeyTriggered = true;
mScreenshotChordVolumeDownKeyTime = event.getDownTime();
mScreenshotChordVolumeDownKeyConsumed = false;
cancelPendingPowerKeyAction();
interceptScreenshotChord();
interceptAccessibilityShortcutChord();
}
} else {
mScreenshotChordVolumeDownKeyTriggered = false;
cancelPendingScreenshotChordAction();
cancelPendingAccessibilityShortcutAction();
}
}
....
}
看到 KEYCODE_VOLUME_DOWN 中,記錄當前按下音量下鍵的時間 mScreenshotChordVolumeDownKeyTime,cancelPendingPowerKeyAction() 移除電源鍵長按消息 MSG_POWER_LONG_PRESS,來看下核心方法 interceptScreenshotChord()
// Time to volume and power must be pressed within this interval of each other.
private static final long SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS = 150;
private void interceptScreenshotChord() {
if (mScreenshotChordEnabled
&& mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered
&& !mA11yShortcutChordVolumeUpKeyTriggered) {
final long now = SystemClock.uptimeMillis();
if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
&& now <= mScreenshotChordPowerKeyTime
+ SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
mScreenshotChordVolumeDownKeyConsumed = true;
cancelPendingPowerKeyAction();
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
}
}
}
只有當電源鍵按下時 mScreenshotChordPowerKeyTriggered 才為 true, 當兩個按鍵的按下時間都大於 150 時,延時執行截屏任務 mScreenshotRunnable
private long getScreenshotChordLongPressDelay() {
if (mKeyguardDelegate.isShowing()) {
// Double the time it takes to take a screenshot from the keyguard
return (long) (KEYGUARD_SCREENSHOT_CHORD_DELAY_MULTIPLIER *
ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout());
}
return ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout();
}
若當前輸入框是打開狀態,則延時時間為輸入框關閉時間加上系統配置的按鍵超時時間,若當前輸入框沒有打開則直接是系統配置的按鍵超時處理時間
緊接著看下 mScreenshotRunnable 都做了什麼操作
private class ScreenshotRunnable implements Runnable {
private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;
public void setScreenshotType(int screenshotType) {
mScreenshotType = screenshotType;
}
@Override
public void run() {
takeScreenshot(mScreenshotType);
}
}
private final ScreenshotRunnable mScreenshotRunnable = new ScreenshotRunnable();
可以看到線上程中調用了 takeScreenshot(),預設不設置截屏類型就是全屏,截屏類型有 TAKE_SCREENSHOT_SELECTED_REGION 選定的區域 和 TAKE_SCREENSHOT_FULLSCREEN 全屏兩種類型
// Assume this is called from the Handler thread.
private void takeScreenshot(final int screenshotType) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
return;
}
final ComponentName serviceComponent = new ComponentName(SYSUI_PACKAGE,
SYSUI_SCREENSHOT_SERVICE);
final Intent serviceIntent = new Intent();
serviceIntent.setComponent(serviceComponent);
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != this) {
return;
}
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, screenshotType);
final ServiceConnection myConn = this;
Handler h = new Handler(mHandler.getLooper()) {
@Override
public void handleMessage(Message msg) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection == myConn) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
mHandler.removeCallbacks(mScreenshotTimeout);
}
}
}
};
msg.replyTo = new Messenger(h);
msg.arg1 = msg.arg2 = 0;
if (mStatusBar != null && mStatusBar.isVisibleLw())
msg.arg1 = 1;
if (mNavigationBar != null && mNavigationBar.isVisibleLw())
msg.arg2 = 1;
try {
messenger.send(msg);
} catch (RemoteException e) {
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
mHandler.removeCallbacks(mScreenshotTimeout);
notifyScreenshotError();
}
}
}
};
if (mContext.bindServiceAsUser(serviceIntent, conn,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
UserHandle.CURRENT)) {
mScreenshotConnection = conn;
mHandler.postDelayed(mScreenshotTimeout, 10000);
}
}
}
takeScreenshot 中通過 bind SystemUI中的 TakeScreenshotService 建立連接,連接成功後通過 Messenger 在兩個進程中傳遞消息通行,有點類似 AIDL,關於 Messenger 的介紹可參考 Android進程間通訊之 messenger Messenger 主要傳遞當前的 mStatusBar 和 mNavigationBar 是否可見,再來看 TakeScreenshotService 中如何接收處理
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;
Runnable finisher = new Runnable() {
@Override
public void run() {
Message reply = Message.obtain(null, 1);
try {
callback.send(reply);
} catch (RemoteException e) {
}
}
};
// If the storage for this user is locked, we have no place to store
// the screenshot, so skip taking it instead of showing a misleading
// animation and error notification.
if (!getSystemService(UserManager.class).isUserUnlocked()) {
Log.w(TAG, "Skipping screenshot because storage is locked!");
post(finisher);
return;
}
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;
}
}
};
@Override
public IBinder onBind(Intent intent) {
return new Messenger(mHandler).getBinder();
}
@Override
public boolean onUnbind(Intent intent) {
if (mScreenshot != null) mScreenshot.stopScreenshot();
return true;
}
}
可以看到通過 mHandler 接收傳遞的消息,獲取截屏類型和是否要包含狀態欄、導航欄,通過創建 GlobalScreenshot 對象(真正幹活的來了),調用 takeScreenshot 執行截屏操作,繼續跟進
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
mDisplay.getRealMetrics(mDisplayMetrics);
takeScreenshot(finisher, statusBarVisible, navBarVisible, 0, 0, mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels);
}
/**
* Takes a screenshot of the current display and shows an animation.
*/
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
int x, int y, int width, int height) {
// We need to orient the screenshot correctly (and the Surface api seems to take screenshots
// only in the natural orientation of the device :!)
mDisplay.getRealMetrics(mDisplayMetrics);
float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
float degrees = getDegreesForRotation(mDisplay.getRotation());
boolean requiresRotation = (degrees > 0);
if (requiresRotation) {
// Get the dimensions of the device in its native orientation
mDisplayMatrix.reset();
mDisplayMatrix.preRotate(-degrees);
mDisplayMatrix.mapPoints(dims);
dims[0] = Math.abs(dims[0]);
dims[1] = Math.abs(dims[1]);
}
// Take the screenshot
mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
if (mScreenBitmap == null) {
notifyScreenshotError(mContext, mNotificationManager,
R.string.screenshot_failed_to_capture_text);
finisher.run();
return;
}
if (requiresRotation) {
// Rotate the screenshot to the current orientation
Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888,
mScreenBitmap.hasAlpha(), mScreenBitmap.getColorSpace());
Canvas c = new Canvas(ss);
c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
c.rotate(degrees);
c.translate(-dims[0] / 2, -dims[1] / 2);
c.drawBitmap(mScreenBitmap, 0, 0, null);
c.setBitmap(null);
// Recycle the previous bitmap
mScreenBitmap.recycle();
mScreenBitmap = ss;
}
if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) {
// Crop the screenshot to selected region
Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height);
mScreenBitmap.recycle();
mScreenBitmap = cropped;
}
// Optimizations
mScreenBitmap.setHasAlpha(false);
mScreenBitmap.prepareToDraw();
// Start the post-screenshot animation
startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
statusBarVisible, navBarVisible);
}
獲取屏幕的寬高和當前屏幕方向以確定是否需要旋轉圖片,然後通過 SurfaceControl.screenshot 截屏,好吧,再繼續往下看到
public static Bitmap screenshot(int width, int height) {
// TODO: should take the display as a parameter
IBinder displayToken = SurfaceControl.getBuiltInDisplay(
SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN);
return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true,
false, Surface.ROTATION_0);
}
這裡調用的是 nativeScreenshot 方法,它是一個 native 方法,具體的實現在JNI層,這裡就不做過多的介紹了。繼續回到我們的 takeScreenshot 方法,在調用了截屏方法 screentshot 之後,判斷是否截屏成功:
截屏失敗則調用 notifyScreenshotError 發送通知。截屏成功,則調用 startAnimation 播放動畫,來分析下動畫,後面我們會改這個動畫的效果
/**
* Starts the animation after taking the screenshot
*/
private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
boolean navBarVisible) {
// If power save is on, show a toast so there is some visual indication that a screenshot
// has been taken.
PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
if (powerManager.isPowerSaveMode()) {
Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show();
}
// Add the view for the animation
mScreenshotView.setImageBitmap(mScreenBitmap);
mScreenshotLayout.requestFocus();
// Setup the animation with the screenshot just taken
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) {
// Save the screenshot once we have a bit of time now
saveScreenshotInWorkerThread(finisher);
mWindowManager.removeView(mScreenshotLayout);
// Clear any references to the bitmap
mScreenBitmap = null;
mScreenshotView.setImageBitmap(null);
}
});
mScreenshotLayout.post(new Runnable() {
@Override
public void run() {
// Play the shutter sound to notify that we've taken a screenshot
mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mScreenshotView.buildLayer();
mScreenshotAnimation.start();
}
});
}
先判斷是否是低電量模式,若是發出已抓取屏幕截圖的 toast,然後通過 WindowManager 在屏幕中間添加一個裝有截屏縮略圖的 view,同時創建兩個動畫組合,通過 mCameraSound 播放截屏咔嚓聲並執行動畫,動畫結束後移除剛剛添加的 view,同時調用 saveScreenshotInWorkerThread 保存圖片到媒體庫,我們直接來看 SaveImageInBackgroundTask
class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
.....
SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
NotificationManager nManager) {
......
mNotificationBuilder = new Notification.Builder(context, NotificationChannels.SCREENSHOTS)
.setTicker(r.getString(R.string.screenshot_saving_ticker)
+ (mTickerAddSpace ? " " : ""))
.setContentTitle(r.getString(R.string.screenshot_saving_title))
.setContentText(r.getString(R.string.screenshot_saving_text))
.setSmallIcon(R.drawable.stat_notify_image)
.setWhen(now)
.setShowWhen(true)
.setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color))
.setStyle(mNotificationStyle)
.setPublicVersion(mPublicNotificationBuilder.build());
mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
SystemUI.overrideNotificationAppName(context, mNotificationBuilder);
mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
mNotificationBuilder.build());
}
@Override
protected Void doInBackground(Void... params) {
if (isCancelled()) {
return null;
}
// By default, AsyncTask sets the worker thread to have background thread priority, so bump
// it back up so that we save a little quicker.
Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
Context context = mParams.context;
Bitmap image = mParams.image;
Resources r = context.getResources();
try {
// Create screenshot directory if it doesn't exist
mScreenshotDir.mkdirs();
// media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds
// for DATE_TAKEN
long dateSeconds = mImageTime / 1000;
// Save
OutputStream out = new FileOutputStream(mImageFilePath);
image.compress(Bitmap.CompressFormat.PNG, 100, out);
out.flush();
out.close();
// Save the screenshot to the MediaStore
ContentValues values = new ContentValues();
ContentResolver resolver = context.getContentResolver();
values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds);
values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds);
values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth);
values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight);
values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
// Create a share intent
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
// Create a share action for the notification. Note, we proxy the call to ShareReceiver
// because RemoteViews currently forces an activity options on the PendingIntent being
// launched, and since we don't want to trigger the share sheet in this case, we will
// start the chooser activitiy directly in ShareReceiver.
PendingIntent shareAction = PendingIntent.getBroadcast(context, 0,
new Intent(context, GlobalScreenshot.ShareReceiver.class)
.putExtra(SHARING_INTENT, sharingIntent),
PendingIntent.FLAG_CANCEL_CURRENT);
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_share,
r.getString(com.android.internal.R.string.share), shareAction);
mNotificationBuilder.addAction(shareActionBuilder.build());
// Create a delete action for the notification
PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_delete,
r.getString(com.android.internal.R.string.delete), deleteAction);
mNotificationBuilder.addAction(deleteActionBuilder.build());
mParams.imageUri = uri;
mParams.image = null;
mParams.errorMsgResId = 0;
} catch (Exception e) {
// IOException/UnsupportedOperationException may be thrown if external storage is not
// mounted
Slog.e(TAG, "unable to save screenshot", e);
mParams.clearImage();
mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
}
// Recycle the bitmap data
if (image != null) {
image.recycle();
}
return null;
}
@Override
protected void onPostExecute(Void params) {
if (mParams.errorMsgResId != 0) {
// Show a message that we've failed to save the image to disk
GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,
mParams.errorMsgResId);
} else {
// Show the final notification to indicate screenshot saved
Context context = mParams.context;
Resources r = context.getResources();
// Create the intent to show the screenshot in gallery
Intent launchIntent = new Intent(Intent.ACTION_VIEW);
launchIntent.setDataAndType(mParams.imageUri, "image/png");
launchIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
final long now = System.currentTimeMillis();
// Update the text and the icon for the existing notification
mPublicNotificationBuilder
.setContentTitle(r.getString(R.string.screenshot_saved_title))
.setContentText(r.getString(R.string.screenshot_saved_text))
.setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
.setWhen(now)
.setAutoCancel(true)
.setColor(context.getColor(
com.android.internal.R.color.system_notification_accent_color));
mNotificationBuilder
.setContentTitle(r.getString(R.string.screenshot_saved_title))
.setContentText(r.getString(R.string.screenshot_saved_text))
.setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
.setWhen(now)
.setAutoCancel(true)
.setColor(context.getColor(
com.android.internal.R.color.system_notification_accent_color))
.setPublicVersion(mPublicNotificationBuilder.build())
.setFlag(Notification.FLAG_NO_CLEAR, false);
mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
mNotificationBuilder.build());
}
mParams.finisher.run();
mParams.clearContext();
}
@Override
protected void onCancelled(Void params) {
// If we are cancelled while the task is running in the background, we may get null params.
// The finisher is expected to always be called back, so just use the baked-in params from
// the ctor in any case.
mParams.finisher.run();
mParams.clearImage();
mParams.clearContext();
// Cancel the posted notification
mNotificationManager.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
}
}
簡單說下, SaveImageInBackgroundTask 構造方法中做了大量的準備工作,截屏圖片的時間命名格式、截屏通知對象創建,在 doInBackground 中將截屏圖片通過 ContentResolver 存儲至 MediaStore,再創建兩個 PendingIntent,用於分享和刪除截屏圖片,在 onPostExecute 中發送剛剛創建的 Notification 至 statuBar 顯示,到此截屏的流程就結束了。
其它
我們再回到 PhoneWindowManager 中看下,通過上面我們知道要想截屏只需通過如下兩行代碼即可
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mHandler.post(mScreenshotRunnable);
通過搜索上面的關鍵代碼,我們發現還有另外兩處也調用了截屏的代碼,一起來看下
@Override
public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
final boolean keyguardOn = keyguardOn();
final int keyCode = event.getKeyCode();
.....
else if (keyCode == KeyEvent.KEYCODE_S && event.isMetaPressed()
&& event.isCtrlPressed()) {
if (down && repeatCount == 0) {
int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION
: TAKE_SCREENSHOT_FULLSCREEN;
mScreenshotRunnable.setScreenshotType(type);
mHandler.post(mScreenshotRunnable);
return -1;
}
}
....
else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
if (down && repeatCount == 0) {
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mHandler.post(mScreenshotRunnable);
}
return -1;
}
......
}
也是在攔截按鍵消息分發之前的方法中,查看 KeyEvent 源碼,第一種情況大概網上搜索了下,應該是接外設時,同時按下 S 鍵 + Meta鍵 + Ctrl鍵即可截屏,關於 Meta 介紹可參考Meta鍵始末 第二種情況是按下截屏鍵時,對應 keyCode 為 120,可以用 adb shell input keyevent 120 模擬發現也能截屏
/** Key code constant: 'S' key. */
public static final int KEYCODE_S = 47;
/** Key code constant: System Request / Print Screen key. */
public static final int KEYCODE_SYSRQ = 120;
常用按鍵對應值
這樣文章開頭提到的三指截屏操作,我們就可以加在 PhoneWindowManager 中,當手勢監聽獲取到三指時,只需調用截屏的兩行代碼即可
總結
在 PhoneWindowManager 的 dispatchUnhandledKey 方法中處理App無法處理的按鍵事件,當然也包括音量減少鍵和電源按鍵的組合按鍵
通過一系列的調用啟動 TakeScreenshotService 服務,並通過其執行截屏的操作。
具體的截屏代碼是在 native 層實現的。
截屏操作時候,若截屏失敗則直接發送截屏失敗的 notification 通知。
截屏之後,若截屏成功,則先執行截屏的動畫,併在動畫效果執行完畢之後,發送截屏成功的 notification 的通知。
參考文章
Android 截屏方法總結
Android KeyCode列表