涉及到的源码有html
frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.javajava
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.javaandroid
按键处理都是在 PhoneWindowManager 中,真正截屏的功能实如今 GlobalScreenshot 中, PhoneWindowManager 和 systemui 经过 bind TakeScreenshotService 来实现截屏功能shell
通常未通过特殊定制的 Android 系统,截屏都是经过同时按住音量下键和电源键来截屏,后来咱们使用的一些华为、oppo等厂商的系统你会发现能够经过三指滑动来截屏,下一篇咱们会定制此功能,并且截屏显示风格相似 iphone 在左下角显示截屏缩略图,点击可跳转放大查看,3s 无操做后向左自动滑动消失。api
好了,如今咱们先来理一下系统截屏的流程app
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() 方法,在此到处理按键操做iphone
/** {@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()ide
// 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 时,延时执行截屏任务 mScreenshotRunnableoop
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(); }
若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间post
紧接着看下 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 的通知。
参考文章