android 8.1 截屏,Android8.1 MTK平台 截屏功能分析

前言

涉及到的源码有

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 {

.....

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;

常用按键对应值

778bc4fedb03

ebFZ5R.png

这样文章开头提到的三指截屏操作,我们就可以加在 PhoneWindowManager 中,当手势监听获取到三指时,只需调用截屏的两行代码即可

总结

在 PhoneWindowManager 的 dispatchUnhandledKey 方法中处理App无法处理的按键事件,当然也包括音量减少键和电源按键的组合按键

通过一系列的调用启动 TakeScreenshotService 服务,并通过其执行截屏的操作。

具体的截屏代码是在 native 层实现的。

截屏操作时候,若截屏失败则直接发送截屏失败的 notification 通知。

截屏之后,若截屏成功,则先执行截屏的动画,并在动画效果执行完毕之后,发送截屏成功的 notification 的通知。

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值