支付宝截图反馈功能实现

最近项目中有个截图反馈的功能要做成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.jpg
04-27 16:51:14.301 1939-1949/? D/Parcel: acquire_object  ret:0 size:2097152
    release_object  ret:0  size:2097152
04-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);//更新媒体信息
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值