支付寶截圖反饋功能實現

Harlber發表於2018-04-25

最近專案中有個截圖反饋的功能要做成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);//更新媒體資訊
複製程式碼

相關文章