最近项目中有个截图反馈的功能要做成sdk供业务方使用,类似支付宝中的功能,但是功能更复杂
实现思路:
- 监听截图
- 显示监听结果加跳转交互
对于实现监听截图的功能,前辈们已经做了很多,这里采用MediaContentObserver
的解决方案,详情可查看 。
坑点梳理
- 部分机型一次截图,会有多次回调(vivo x9 2次)
- vivo Y51A 截图关键字为汉字
截图
- 截图加载在部分机型出现
decoder->decode returned false
加载失败 - 悬浮窗权限兼容问题(
WindowManager.LayoutParams.TYPE_PHONE
在O版本中废弃,实际运行结果为,赋予了悬浮窗权限仍报没有权限error) - 大部分机型的截图是png格式,有些为jpg
- 安卓7.1.1 miui 9.1 小米 mix2 由于全面屏的缘故,截图高度为2160大于屏幕高度1980
实现
/** * 媒体内容观察者(观察媒体数据库的改变) */ private class MediaContentObserver extends ContentObserver { private Uri mContentUri; //MediaStore.Images.Media.INTERNAL_CONTENT_URI //MediaStore.Images.Media.EXTERNAL_CONTENT_URI public MediaContentObserver(Uri contentUri, Handler handler) { super(handler); mContentUri = contentUri; } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); processMediaContentChange(mContentUri); } }复制代码
在监听到有内容改变时,去查询图片内容
/** * 读取媒体数据库时需要读取的列 */ private static final String[] MEDIA_PROJECTIONS = { MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN, }; /** * 读取媒体数据库时需要读取的列, 其中 WIDTH 和 HEIGHT 字段在 API 16 以后才有 */ private static final String[] MEDIA_PROJECTIONS_API_16 = { MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN, MediaStore.Images.ImageColumns.WIDTH, MediaStore.Images.ImageColumns.HEIGHT, }; Cursor cursor = mContext.getContentResolver().query( contentUri, Build.VERSION.SDK_INT < 16 ? MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1" );复制代码
留意这里的 MediaStore.Images.ImageColumns.DATE_ADDED
,实测发现在vivo x9上无效,需要查询 MediaStore.Images.ImageColumns.DATE_MODIFIED
才能获得
之后根据图片大小和图片所在文件夹或文件名检查是否符合为一张截图,部分截图关键字如下:
private static final String[] KEYWORDS = { "screenshot", "screen_shot", "screen-shot", "screen shot", "screencapture", "screen_capture", "screen-capture", "screen capture", "screencap", "screen_cap", "screen-cap", "screen cap", "截屏" };复制代码
在此,顺带说下系统的截图流程
如果留意下,会发现每次截图,系统都有相应的日志输出
04-27 16:51:14.291 9240-9240/? D/TakeScreenshotService: send Broadcast, URI:file:///storage/emulated/0/截屏/截屏_20180427_165114.jpg04-27 16:51:14.301 1939-1949/? D/Parcel: acquire_object ret:0 size:2097152 release_object ret:0 size:209715204-27 16:51:14.301 1939-1939/? D/MediaScannerReceiver: action: android.intent.action.MEDIA_SCANNER_SCAN_FILE path: /storage/emulated/0/截屏/截屏_20180427_165114.jpg复制代码
com.android.systemui.screenshot.TakeScreenshotService.java...省略部分代码// 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);//1 } switch (msg.what) { case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: mScreenshot.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0);//2 break; case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: mScreenshot.takeScreenshotPartial(finisher, msg.arg1 > 0, msg.arg2 > 0);//3 break; }复制代码
从片段1,2,3清楚第看到实际进行截图的是GlobalScreenshot
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, int x, int y, int width, int height) { //实际截屏 mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]); //... if (requiresRotation) { // Rotate the screenshot to the current orientation //... 旋转截图 } if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) { // Crop the screenshot to selected region //... } // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); // 开始截屏动画展示 startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, statusBarVisible, navBarVisible); } /** * Starts the animation after taking the screenshot */ private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible) { //省略部分代码 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); } }); //省略部分代码 } /** * Creates a new worker thread and saves the screenshot to the media store. */ private void saveScreenshotInWorkerThread(Runnable finisher) { SaveImageInBackgroundData data = new SaveImageInBackgroundData(); data.context = mContext; data.image = mScreenBitmap; data.iconSize = mNotificationIconSize; data.finisher = finisher; data.previewWidth = mPreviewWidth; data.previewheight = mPreviewHeight; if (mSaveInBgTask != null) { mSaveInBgTask.cancel(false); } mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager) .execute();//开启后台任务,保存截图 } 这里我们关注下保存的内容,在SaveImageInBackgroundTask#doInBackground()中 // Save OutputStream out = new FileOutputStream(mImageFilePath); image.compress(Bitmap.CompressFormat.PNG, 100, out); out.flush(); out.close(); //将图片写到存储空间,注意,这里部分机型存储的为jpg图片 // 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);//更新媒体信息复制代码