前言

涉及到的源码有

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列表

Android8.1 MTK平台 截屏功能分析的更多相关文章

  1. Android8.1 MTK平台 SystemUI源码分析之 网络信号栏显示刷新

    SystemUI系列文章 Android8.1 MTK平台 SystemUI源码分析之 Notification流程 Android8.1 MTK平台 SystemUI源码分析之 电池时钟刷新 And ...

  2. MonoGame 3.2 下,截屏与 Texture2D 的保存

    10月20日注:后来发现了这篇博文(英文),XNA 中的 Color 实际上是与 Alpha 值自左乘(premultiplied)的,这也解释了直接用 0xARGB 转译而颜色异常的原因. 注意,由 ...

  3. OpenGL 多视图与截屏

    最近看红宝书学习 OpenGL 一段时间了,写了简单的 demo 程序温习一下知识. 主要是 使用 glScissor 多视图显示画面和使用 glReadPixels 给画面截屏,使用显示列表(dis ...

  4. javascript实现当前页面截屏

    javascript实现当前页面截屏 一.前言 有客户要求能对用户当前页面进行指定区域截屏,类似qq截屏的实现效果.比如用户在处理工作的时候,将当前页面录入后的一些信息进行截图下载保存.但又不能安装任 ...

  5. DXGI快速截屏录屏技术

    DXGI快速截屏录屏技术 概述   很多地方都需要用到截屏/录屏技术,比如桌面直播,桌面录制等等.在微软Windows平台,有很多截屏的接口,不过大多数性能并不理想,Windows8以后微软引入了一套 ...

  6. Android 截屏与 WebView 长图分享经验总结

    最近在做新业务需求的同时,我们在 Android 上遇到了一些之前没有碰到过的问题,截屏分享. WebView 生成长图以及长图在各个分享渠道分享时图片模糊甚至分享失败等问题,在这过程中踩了很多坑,到 ...

  7. 关于Unity中GrabPass截屏的使用和Shader的组织优化

    GrabPass截屏 可以用来截屏,截屏后把纹理传给下一个通道使用. 1:使用抓屏通道, GrabPass {} 或 GrabPass { “ 纹理名称”}; 使用GrabPass {}后,可以用_G ...

  8. IOS中摇一摇实现截屏(可实现问题反馈的功能)

    有一段时间没有更新博客了,今天更新一篇关于最近工作中用到的一个功能,先简单描述一下:我们知道,测试人员在测试客户端产品时,当出现问题或者BUG的时候,都得先对页面截图,然后从相册中选择截图,加上一段描 ...

  9. Unity通过指定摄像机截屏

    简介 介于照抄网上之前的截图教程,然后在实际应用过程中出现了一些小小的问题,修正了一下下,特此分享一下 PS:代码在后面 原理 原理很简单,就是将一个相机的内容渲染到一个贴图上,然后将贴图保存为图片 ...

随机推荐

  1. vue项目缓存最佳实践

    需求 在开发vue的项目中有遇到了这样一个需求:一个视频列表页面,展示视频名称和是否收藏,点击进去某一项观看,可以收藏或者取消收藏,返回的时候需要记住列表页面的页码等状态,同时这条视频的收藏状态也需要 ...

  2. 1. 在Mac OS中配置CMake的详细图文教程

    CMake是一个比make更高级的跨平台的安装.编译.配置工具,可以用简单的语句来描述所有平台的安装(编译过程).并根据不同平台.不同的编译器,生成相应的Makefile或者project文件.本文主 ...

  3. Mysql CPU使用率长期100%的解决思路备忘

    最近一台服务器的CPU使用率长期保持在100%的状态,查看进程发现是Mysql服务导致的.于是搜索各方资料,终于成功解决问题.备忘以及分享一下,希望可以帮助各位新手朋友. (服务器运行环境是Windo ...

  4. 从后端到前端之Vue(一)写个表格试试水

    目录: 1.脚本式开发. 2.工程化开发 3.工程化和脚本的区别 4.来个table试试水 4,1.目标 4.2.思路 4.3.设计与编码 4.4.效果 5.业务分离 6.功能拓展——个性化设置    ...

  5. 星际旅行(欧拉路,欧拉回路)(20190718 NOIP模拟测试5)

    瞎搞了一个ans+=du*(du-1)/2 wa20分,好桑心(话外音:居然还有二十分,出题人太周到了) 还是判欧拉路 题解没太仔细想,感觉还是kx的思路明白 具体就是:因为每条边要走两遍,可以把一条 ...

  6. Junit简单的案例

    Calculator: public class Calculator { public double add(double number1, double number2) { return num ...

  7. 找bug的过程

    关于昨天程序出差我找bug的过程记录 昨天才程序 https://www.cnblogs.com/pythonywy/p/11006273.html ├── xxxx │ ├── src.py │ └ ...

  8. 一个测试文件与源文件位于不同模块时Jacoco覆盖率配置的例子

    问题描述: 我们有个多模块项目,由于种种原因(更常见的可能是需要集成测试)测试文件和源文件不在一个模块,Jacoco的覆盖率无法正确显示,查询了一些资料,发现中文的例子比较少,就把我自己的Demo贴一 ...

  9. 使用nginx+tomcat实现动静分离

    动态资源与静态资源的区别 微微的概括一下 静态资源: 当用户多次访问这个资源,资源的源代码永远不会改变的资源. 动态资源:当用户多次访问这个资源,资源的源代码可能会发送改变. 什么是动静分离 动静分离 ...

  10. spring与mybatis整合(扫描Mapper接口)

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" ...