前言

涉及到的源码有

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:0system_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 = falsesystem_process D/WindowManager: interceptKeyTi keyCode=25 down=false repeatCount=0 keyguardOn=false mHomePressed=false canceled=false metaState:0system_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} */@Overridepublic 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 keyguardreturn (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;}@Overridepublic 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() {@Overridepublic 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()) {@Overridepublic 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) {}}}@Overridepublic 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() {@Overridepublic void handleMessage(Message msg) {final Messenger callback = msg.replyTo;Runnable finisher = new Runnable() {@Overridepublic 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;}}};@Overridepublic IBinder onBind(Intent intent) {return new Messenger(mHandler).getBinder();}@Overridepublic 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 orientationmDisplayMatrix.reset();mDisplayMatrix.preRotate(-degrees);mDisplayMatrix.mapPoints(dims);dims[0] = Math.abs(dims[0]);dims[1] = Math.abs(dims[1]);}// Take the screenshotmScreenBitmap = 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 orientationBitmap 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 bitmapmScreenBitmap.recycle();mScreenBitmap = ss;}if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) {// Crop the screenshot to selected regionBitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height);mScreenBitmap.recycle();mScreenBitmap = cropped;}// OptimizationsmScreenBitmap.setHasAlpha(false);mScreenBitmap.prepareToDraw();// Start the post-screenshot animationstartAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,statusBarVisible, navBarVisible);}

获取屏幕的宽高和当前屏幕方向以确定是否需要旋转图片,然后通过 SurfaceControl.screenshot 截屏,好吧,再继续往下看到

public static Bitmap screenshot(int width, int height) {// TODO: should take the display as a parameterIBinder 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 animationmScreenshotView.setImageBitmap(mScreenBitmap);mScreenshotLayout.requestFocus();// Setup the animation with the screenshot just takenif (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() {@Overridepublic void onAnimationEnd(Animator animation) {// Save the screenshot once we have a bit of time nowsaveScreenshotInWorkerThread(finisher);mWindowManager.removeView(mScreenshotLayout);// Clear any references to the bitmapmScreenBitmap = null;mScreenshotView.setImageBitmap(null);}});mScreenshotLayout.post(new Runnable() {@Overridepublic void run() {// Play the shutter sound to notify that we've taken a screenshotmCameraSound.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());}@Overrideprotected 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 existmScreenshotDir.mkdirs();// media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds// for DATE_TAKENlong dateSeconds = mImageTime / 1000;// SaveOutputStream out = new FileOutputStream(mImageFilePath);image.compress(Bitmap.CompressFormat.PNG, 100, out);out.flush();out.close();// Save the screenshot to the MediaStoreContentValues 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 intentString 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 notificationPendingIntent 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// mountedSlog.e(TAG, "unable to save screenshot", e);mParams.clearImage();mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;}// Recycle the bitmap dataif (image != null) {image.recycle();}return null;}@Overrideprotected void onPostExecute(Void params) {if (mParams.errorMsgResId != 0) {// Show a message that we've failed to save the image to diskGlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,mParams.errorMsgResId);} else {// Show the final notification to indicate screenshot savedContext context = mParams.context;Resources r = context.getResources();// Create the intent to show the screenshot in galleryIntent 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 notificationmPublicNotificationBuilder.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();}@Overrideprotected 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 notificationmNotificationManager.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. android 8.1 截屏,Android8.1 MTK平台 截屏功能分析

    前言 涉及到的源码有 frameworksbaseservicescorejavacomandroidserverpolicyPhoneWindowManager.java vendormediate ...

  2. Android8.1 MTK平台 增加三指截屏(仿IOS左下角显示缩略图点击放大显示)

    效果图 修改后动画如下 系统原动画如下 三指截屏 PhoneWindowManager 同级目录下的 SystemGesturesPointerEventListener.java 主要负责处理界面的 ...

  3. Android8.1 MTK平台 Dialer修改(来电全屏、归属地显示)

    来电默认全屏 默认情况下,来电android是以通知窗口的形式显示,只在屏幕的顶部显示,现在改为全屏显示 修改位置 alps\vendor\mediatek\proprietary\packages\ ...

  4. android 改变来电号码,Android8.1 MTK平台 Dialer修改(来电全屏、归属地显示)

    来电默认全屏 默认情况下,来电android是以通知窗口的形式显示,只在屏幕的顶部显示,现在改为全屏显示 修改位置 alps\vendor\mediatek\proprietary\packages\ ...

  5. Android8.1 MTK平台 Dialer修改(通话常亮、按钮接听)

    通话保持常亮(不息屏) 在网上搜索的 Android保持屏幕常亮的方法,一种是 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SC ...

  6. Android8.1 MTK平台 系统需求定制

    1.清空短信记录接口 vendor\mediatek\proprietary\packages\apps\Mms\src\com\android\mms\transaction\NoneService ...

  7. Android8.1 MTK平台 修改 Volte 视频通话我方视角为矩形

    效果图 修改位置 vendor\mediatek\proprietary\packages\apps\Dialer\java\com\android\incallui\video\impl\Video ...

  8. Android8.1 MTK平台 修改蓝牙默认名称

    8.1 的默认蓝牙名称为 Android Bluedroid 通过搜索你会找到如下文件 device/generic/common/bluetooth/bdroid_buildcfg.h #ifnde ...

  9. Android8.1 MTK 平台 Launcher3 修改定制

    一.去掉默认 Google 搜索栏 修改位置 vendor\mediatek\proprietary\packages\apps\Launcher3\src\com\android\launcher3 ...

最新文章

  1. Navicat for MySQL连接MYSQL出错,错误代码1045的解决方法
  2. WLA-云鹰计划加速千家创新创业企业成为独角兽
  3. 【网安干货】MySQL8新特性注入技巧
  4. Problem F. Grab The Tree HDU - 6324(树形dp+博弈)
  5. 算法实现:返回单链表的倒数第pos个节点
  6. 外设驱动库开发笔记24:FM24xxx系列FRAM存储器驱动
  7. [ Ubuntu Chinese Method ] uninstall DBus, install fcitx
  8. 1.1万亿!华为第一!
  9. python 条形图 负值_Python处理JSON数据并生成条形图
  10. 把uliweb项目变成可安装的python包
  11. csdn图片排版技巧
  12. 游戏开发之测试篇3(C++)
  13. 视图控制器的View创建方式
  14. 回归标准差和残差平方和的关系_一文详解经典回归分析
  15. 汇编 - 实验 - 计算X+(Y-Z)=W
  16. 《SEM长尾搜索营销策略解密》一一2.8 长尾虽好,但核心不可或缺
  17. FPGA学习: Verilog刷题记录(16)
  18. 如何修改input提示文字样式
  19. Google广告中介(以MoPub广告接入为例)
  20. 【LeetCode 深度优先搜索专项】不同岛屿的数量 II(711)

热门文章

  1. 给开发者准备的 10 款最好的 jQuery 日历插件
  2. OAuth 2.0——授权服务开发笔记(二)
  3. 用代码证明自己闲的蛋疼(一)——cmd闪瞎狗眼
  4. 最短路径——Dijkstra算法HDU Today(hdu2112)
  5. 【转载】Makefile教程
  6. windows终止进程——taskkill
  7. 演练表创建 用户表my_user 追加了列 mysql 20210413_122410
  8. 数字三角形的显示 java
  9. 常用转义符的使用 java 0133
  10. python-正则表达式1909