截屏功能是大家经常用到的一项功能,使用截屏功能能便捷且快速地把手机当前屏幕显示的信息以图片的形式存储到手机中。
截屏操作一般有两种途径:
下拉手机屏幕上方状态栏点击快捷功能面板中的截屏(部分手机品牌中支持此功能)。同时按住手机侧方的电源键+音量减键来实现截屏。这两种截屏的方式其根本原理是一样的,实现的流程都是一样的,不同的是触发方式。
一、截屏
a. 电源键+音量键截屏
Android 系统中对各类物理与虚拟按键的处理都是在 Framework 层中的 PhoneWindowManager.java 内实现。
对电源键的监听处理 /frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java @Override public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { … case KeyEvent.KEYCODE_POWER: {//匹配电源键 // Any activity on the power button stops the accessibility shortcut cancelPendingAccessibilityShortcutAction(); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down) {//当电源键被按下状态时 interceptPowerKeyDown(event, interactive); } else { interceptPowerKeyUp(event, interactive, canceled); } break; } … } private void interceptPowerKeyDown(KeyEvent event, boolean interactive) { .... // Latch power key state to detect screenshot chord. if (interactive && !mScreenshotChordPowerKeyTriggered && (event.getFlags() &KeyEvent.FLAG_FALLBACK) == 0) { mScreenshotChordPowerKeyTriggered = true; //Power 键被触发的标示符 mScreenshotChordPowerKeyTime = event.getDownTime(); //获取 Power 键的触发时间 interceptScreenshotChord(); } } private void interceptScreenshotChord() { if (mScreenshotChordEnabled //系统是否开启截屏功能 &&mScreenshotChordVolumeDownKeyTriggered //音量减键已被按下 &&mScreenshotChordPowerKeyTriggered //电源减键已被按下 && !mA11yShortcutChordVolumeUpKeyTriggered) { //音量减键未被按下 final long now = SystemClock.uptimeMillis(); //获取当前的时间 //当前时间要小于或等于音量减键被按下时间+150S if (now < = mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS // 当前时间要小于或等于电源键被按下时间+150S && now < = mScreenshotChordPowerKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) { mScreenshotChordVolumeDownKeyConsumed = true; cancelPendingPowerKeyAction(); mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); } } } //截屏是有两种模式,一种是全屏截屏,另一种是区域截屏。默认是全屏截屏 private class ScreenshotRunnable implements Runnable { private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;//默认截屏方式是全屏截取 public void setScreenshotType(int screenshotType) { mScreenshotType = screenshotType; } @Override public void run() { takeScreenshot(mScreenshotType);//执行 takeScreenshot 方法 } } private static final String SYSUI_PACKAGE = "com.android.systemui"; private static final String SYSUI_SCREENSHOT_SERVICE = "com.android.systemui.screenshot.TakeScreenshotService"; private void takeScreenshot(final int screenshotType) { synchronized (mScreenshotLock) { if (mScreenshotConnection != null) { return; } final ComponentNameserviceComponent = new ComponentName(SYSUI_PACKAGE, SYSUI_SCREENSHOT_SERVICE); final Intent serviceIntent = new Intent(); serviceIntent.setComponent(serviceComponent); ServiceConnection conn = new ServiceConnection() { .... if (mContext.bindServiceAsUser(serviceIntent, conn, Context.BIND_AUTO_CREATE |Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, UserHandle.CURRENT)) { mScreenshotConnection = conn; mHandler.postDelayed(mScreenshotTimeout, 10000); } } 2、 响应截屏请求 /frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { final Messenger callback = msg.replyTo;//获取客户端传过来的 Messenger 对象 Runnable finisher = new Runnable() { @Override public void run() { Message reply = Message.obtain(null, 1); try { //Messenger 双向通信,在服务端用远程客户端的 Messenger 对象给客户端发送信息 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; default: Log.d(TAG, "Invalid screenshot option: " + msg.what); } } } /frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java private void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, Rect crop) { int rot = mDisplay.getRotation(); int width = crop.width(); int height = crop.height(); //执行 SurfaceControl.screenshot 方法并返回截屏图片 mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot); if (mScreenBitmap == null) { notifyScreenshotError(mContext, mNotificationManager, R.string.screenshot_failed_to_capture_text); finisher.run(); return; } // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); // Start the post-screenshot animation startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,statusBarVisible, navBarVisible); } void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { mDisplay.getRealMetrics(mDisplayMetrics); takeScreenshot(finisher, statusBarVisible, navBarVisible, new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); } /frameworks/base/core/java/android/view/SurfaceControl.java // SurfaceControl::screenshot 重截方法, @UnsupportedAppUsage private static void screenshot(IBinder display, Surface consumer, RectsourceCrop, int width, int height, int minLayer, int maxLayer, booleanallLayers, booleanuseIdentityTransform) { if (display == null) { throw new IllegalArgumentException("displayToken must not be null"); } if (consumer == null) { throw new IllegalArgumentException("consumer must not be null"); } nativeScreenshot(display, consumer, sourceCrop, width, height, minLayer, maxLayer, allLayers, useIdentityTransform); } // 一步步跟进执行方法,最后到 SurfaceControl 的本地方法 nativeScreenshot private static native void nativeScreenshot(IBinderdisplayToken, Surface consumer, RectsourceCrop, int width, int height, int minLayer, int maxLayer, booleanallLayers, booleanuseIdentityTransform); /frameworks/base/core/jni/android_view_SurfaceControl.cpp static void nativeScreenshot(JNIEnv* env, jclass clazz, jobject displayTokenObj, jobject surfaceObj, jobject sourceCropObj, jint width, jint height, jint minLayer, jint maxLayer, bool allLayers, bool useIdentityTransform) { sp<IBinder> displayToken = ibinderForJavaObject(env, displayTokenObj); if (displayToken == NULL) { return; } sp<Surface> consumer = android_view_Surface_getSurface(env, surfaceObj); if (consumer == NULL) { return; } Rect sourceCrop; if (sourceCropObj != NULL) { sourceCrop = rectFromObj(env, sourceCropObj); } if (allLayers) { minLayer = INT32_MIN; maxLayer = INT32_MAX; } sp<GraphicBuffer> buffer; ScreenshotClient::capture(displayToken, sourceCrop, width, height, minLayer, maxLayer, useIdentityTransform, 0, &buffer); Surface::attachAndQueueBuffer(consumer.get(), buffer); } /frameworks/native/libs/gui/SurfaceComposerClient.cpp status_t ScreenshotClient::capture(const sp<IBinder>& display, Rect sourceCrop, uint32_t reqWidth, uint32_t reqHeight, int32_t minLayerZ, int32_t maxLayerZ, bool useIdentityTransform, uint32_t rotation, sp<GraphicBuffer>* outBuffer) { sp<ISurfaceComposer> s(ComposerService::getComposerService()); if (s == NULL) return NO_INIT; status_t ret = s->captureScreen(display, outBuffer, sourceCrop, reqWidth, reqHeight, minLayerZ, maxLayerZ, useIdentityTransform, static_cast<ISurfaceComposer::Rotation>(rotation)); if (ret != NO_ERROR) { return ret; } return ret; } /frameworks/native/libs/gui/ISurfaceComposer.cpp virtual status_t captureScreen(const sp<IBinder>& display, sp<GraphicBuffer>* outBuffer, Rect sourceCrop, uint32_t reqWidth, uint32_t reqHeight, int32_t minLayerZ, int32_t maxLayerZ, bool useIdentityTransform,::Rotation rotation) { Parcel data, reply; data.writeInterfaceToken(ISurfaceComposer::getInterfaceDescriptor()); data.writeStrongBinder(display); data.write(sourceCrop); data.writeUint32(reqWidth); data.writeUint32(reqHeight); data.writeInt32(minLayerZ); data.writeInt32(maxLayerZ); data.writeInt32(static_cast<int32_t>(useIdentityTransform)); data.writeInt32(static_cast<int32_t>(rotation)); status_t err = remote()->transact(BnSurfaceComposer::CAPTURE_SCREEN, data, &reply); if (err != NO_ERROR) { return err; } err = reply.readInt32(); if (err != NO_ERROR) { return err; } *outBuffer = new GraphicBuffer(); reply.read(**outBuffer); return err; } 3、 截屏动画及图片保存 /frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible) { … //构建动画 ValueAnimatorscreenshotDropInAnim = createScreenshotDropInAnimation(); ValueAnimatorscreenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h, statusBarVisible, navBarVisible); mScreenshotAnimation = new AnimatorSet(); //整合动画 mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim); //添加截屏动画的监听事件 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { saveScreenshotInWorkerThread(finisher); //在异步线程中保存截屏图片 mScreenBitmap = 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();//开始播放动画 } }); } private void saveScreenshotInWorkerThread(Runnable finisher) { SaveImageInBackgroundData data = new SaveImageInBackgroundData(); data.context = mContext; data.image = mScreenBitmap;//native 层返回的截屏位图 data.iconSize = mNotificationIconSize;//系统提示中 icon 尺寸 data.finisher = finisher; data.previewWidth = mPreviewWidth;//图片预览宽 data.previewheight = mPreviewHeight;//图片预览高 if (mSaveInBgTask != null) { mSaveInBgTask.cancel(false); } mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager).execute();//执行异步线程 } SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, NotificationManager nManager) { … mImageTime = System.currentTimeMillis();//截屏时间戳 String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));//转换时间格式 mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);// 图片的文件名称 mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME);//保存图片的文件目录名称 mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath();//保存图片的路径 mImageWidth = data.image.getWidth();//图片宽 mImageHeight = data.image.getHeight();//图片高 … } //构造方法执行之后会进入到此类 SaveImageInBackgroundTask 的 doInBackground() 方法中,该方法主要用于保存图片到本地及生成具有分享、删除、编辑的 Intent 意图操作的提示 notification @Override protected Void doInBackground(Void... params) { … //创建图片保存文件目录 mScreenshotDir.mkdirs(); //应用输出流 FileOutputStream 把数据流写入本地文件。 OutputStream out = new FileOutputStream(mImageFilePath); image.compress(Bitmap.CompressFormat.PNG, 100, out); out.flush(); out.close(); // 将图片信息以 Uri 方式保存入系统媒体库 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); … } //如果截屏图保存成功则会对属性 mParams.errorMsgResId 赋值为 0;如果保存失败则对属性 mParams.errorMsgResId 赋值为失败提示语资源 ID 值。在 doInBackground 方法中任务执行完成后需要将执行结果反馈给主线程,以便主线程更新相应界面,此工作就需要在 AsyncTask 的 onPostExecute 方法完成,此时进入该方法截屏图片保存到本地的结果已明确,所以就先对图片保存结果做判断,并对不同结果弹出相应的提示通知 notification,最后执行 finisher 的 run 方法向客户端反馈截屏结果。 @Override protected void onPostExecute(Void params) { … if (mParams.errorMsgResId != 0) { // 存储截屏图片失败,弹出通知提示用户 GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager, mParams.errorMsgResId); } else { //创建图片浏览Intent 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); ….. //弹出之都创建的notification mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, NotificationBuilder.build()); } //最后运行finisher实例的run方法向截屏请求者返回操作结果 mParams.finisher.run(); mParams.clearContext(); … }