1. 程式人生 > >android App內監聽截圖加二維碼

android App內監聽截圖加二維碼

Android截圖功能是一個常用的功能,可以方便的用來分享或者傳送給好友,本文介紹瞭如何實現app內截圖監控功能,當發現使用者在我們的app內進行了截圖操作時,進行對圖片的二次操作,例如新增二維碼,公司logo等一系列*

專案地址

測試截圖:

image

截圖原理

Android系統並沒有提供截圖通知相關的API,需要我們自己利用系統能提供的相關特性變通實現。Android系統有一個媒體資料庫,每拍一張照片,或使用系統截圖擷取一張圖片,都會把這張圖片的詳細資訊加入到這個媒體資料庫,併發出內容改變通知,我們可以利用內容觀察者(ContentObserver)監聽媒體資料庫的變化,當資料庫有變化時,獲取最後插入的一條圖片資料,如果該圖片符合特定的規則,則認為被截圖了。

判斷依據

當ContentObserver監聽到媒體資料庫的資料改變, 在有資料改變時 獲取最後插入資料庫的一條圖片資料, 如果符合以下規則, 則認為截圖了:

  1. 時間判斷,圖片的生成時間在開始監聽之後,並與當前時間相隔10秒內:開始監聽後生成的圖片才有意義,相隔10秒內說明是剛剛生成的
  2. 尺寸判斷,圖片的尺寸沒有超過螢幕的尺寸:圖片尺寸超過螢幕尺寸,不可能是截圖圖片
  3. 路徑判斷,圖片路徑符合包含特定的關鍵詞:這一點是關鍵,截圖圖片的儲存路徑通常包含“screenshot”

這些判斷是為了增加截圖檢測結果的可靠性,防止誤報,防止遺漏。其中截圖圖片的路徑正常Android系統儲存的路徑格式, 例如我的是:“外部儲存器/storage/emulated/0/Pictures/Screenshots/Screenshot_2017-08-03-15-42-58.png”,但Android系統碎片化嚴重,加上其他第三方截圖APP等,所以路徑關鍵字除了檢查是否包含“screenshot”外,還可以適當增加其他關鍵字,詳見最後的監聽器完整程式碼。這種監聽截圖的方法也不是100%準確,例如某些被root的機器使用第三方截圖APP自定義儲存路徑,還比如通過ADB命令在電腦上獲取手機螢幕快照均不能監聽到,但這也是目前可行性最高的方法,對於絕大多數使用者都比較靠譜。

程式碼描述

監聽截圖

public class ScreenShotListenManager {
    private static final String TAG = "ScreenShotListenManager";

    /**
     * 讀取媒體資料庫時需要讀取的列
     */
    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, }; /** * 截圖依據中的路徑判斷關鍵字 */ 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" }; private static Point sScreenRealSize; /** * 已回撥過的路徑 */ private final static List<String> sHasCallbackPaths = new ArrayList<String>(); private Context mContext; private OnScreenShotListener mListener; private long mStartListenTime; /** * 內部儲存器內容觀察者 */ private MediaContentObserver mInternalObserver; /** * 外部儲存器內容觀察者 */ private MediaContentObserver mExternalObserver; /** * 執行在 UI 執行緒的 Handler, 用於執行監聽器回撥 */ private final Handler mUiHandler = new Handler(Looper.getMainLooper()); private ScreenShotListenManager(Context context) { if (context == null) { throw new IllegalArgumentException("The context must not be null."); } mContext = context; // 獲取螢幕真實的解析度 if (sScreenRealSize == null) { sScreenRealSize = getRealScreenSize(); if (sScreenRealSize != null) { Log.d(TAG, "Screen Real Size: " + sScreenRealSize.x + " * " + sScreenRealSize.y); } else { Log.w(TAG, "Get screen real size failed."); } } } public static ScreenShotListenManager newInstance(Context context) { assertInMainThread(); return new ScreenShotListenManager(context); } /** * 啟動監聽 */ public void startListen() { assertInMainThread(); // sHasCallbackPaths.clear(); // 記錄開始監聽的時間戳 mStartListenTime = System.currentTimeMillis(); // 建立內容觀察者 mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler); mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler); // 註冊內容觀察者 mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver ); mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver ); } /** * 停止監聽 */ public void stopListen() { assertInMainThread(); // 登出內容觀察者 if (mInternalObserver != null) { try { mContext.getContentResolver().unregisterContentObserver(mInternalObserver); } catch (Exception e) { e.printStackTrace(); } mInternalObserver = null; } if (mExternalObserver != null) { try { mContext.getContentResolver().unregisterContentObserver(mExternalObserver); } catch (Exception e) { e.printStackTrace(); } mExternalObserver = null; } // 清空資料 mStartListenTime = 0; // sHasCallbackPaths.clear(); //切記!!!:必須設定為空 可能mListener 會隱式持有Activity導致釋放不掉 mListener = null; } /** * 處理媒體資料庫的內容改變 */ private void handleMediaContentChange(Uri contentUri) { Cursor cursor = null; try { // 資料改變時查詢資料庫中最後加入的一條資料 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" ); if (cursor == null) { Log.e(TAG, "Deviant logic."); return; } if (!cursor.moveToFirst()) { Log.d(TAG, "Cursor no data."); return; } // 獲取各列的索引 int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN); int widthIndex = -1; int heightIndex = -1; if (Build.VERSION.SDK_INT >= 16) { widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH); heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT); } // 獲取行資料 String data = cursor.getString(dataIndex); long dateTaken = cursor.getLong(dateTakenIndex); int width = 0; int height = 0; if (widthIndex >= 0 && heightIndex >= 0) { width = cursor.getInt(widthIndex); height = cursor.getInt(heightIndex); } else { // API 16 之前, 寬高要手動獲取 Point size = getImageSize(data); width = size.x; height = size.y; } // 處理獲取到的第一行資料 handleMediaRowData(data, dateTaken, width, height); } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } } private Point getImageSize(String imagePath) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(imagePath, options); return new Point(options.outWidth, options.outHeight); } /** * 處理獲取到的一行資料 */ private void handleMediaRowData(String data, long dateTaken, int width, int height) { if (checkScreenShot(data, dateTaken, width, height)) { Log.d(TAG, "ScreenShot: path = " + data + "; size = " + width + " * " + height + "; date = " + dateTaken); if (mListener != null && !checkCallback(data)) { mListener.onShot(data); } } else { // 如果在觀察區間媒體資料庫有資料改變,又不符合截圖規則,則輸出到 log 待分析 Log.w(TAG, "Media content changed, but not screenshot: path = " + data + "; size = " + width + " * " + height + "; date = " + dateTaken); } } /** * 判斷指定的資料行是否符合截圖條件 */ private boolean checkScreenShot(String data, long dateTaken, int width, int height) { /* * 判斷依據一: 時間判斷 */ // 如果加入資料庫的時間在開始監聽之前, 或者與當前時間相差大於10秒, 則認為當前沒有截圖 if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) { return false; } /* * 判斷依據二: 尺寸判斷 */ if (sScreenRealSize != null) { // 如果圖片尺寸超出螢幕, 則認為當前沒有截圖 if (!((width <= sScreenRealSize.x && height <= sScreenRealSize.y) || (height <= sScreenRealSize.x && width <= sScreenRealSize.y))) { return false; } } /* * 判斷依據三: 路徑判斷 */ if (TextUtils.isEmpty(data)) { return false; } data = data.toLowerCase(); // 判斷圖片路徑是否含有指定的關鍵字之一, 如果有, 則認為當前截圖了 for (String keyWork : KEYWORDS) { if (data.contains(keyWork)) { return true; } } return false; } /** * 判斷是否已回撥過, 某些手機ROM截圖一次會發出多次內容改變的通知; <br/> * 刪除一個圖片也會發通知, 同時防止刪除圖片時誤將上一張符合截圖規則的圖片當做是當前截圖. */ private boolean checkCallback(String imagePath) { if (sHasCallbackPaths.contains(imagePath)) { Log.d(TAG, "ScreenShot: imgPath has done" + "; imagePath = " + imagePath); return true; } // 大概快取15~20條記錄便可 if (sHasCallbackPaths.size() >= 20) { for (int i = 0; i < 5; i++) { sHasCallbackPaths.remove(0); } } sHasCallbackPaths.add(imagePath); return false; } /** * 獲取螢幕解析度 */ private Point getRealScreenSize() { Point screenSize = null; try { screenSize = new Point(); WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Display defaultDisplay = windowManager.getDefaultDisplay(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { defaultDisplay.getRealSize(screenSize); } else { try { Method mGetRawW = Display.class.getMethod("getRawWidth"); Method mGetRawH = Display.class.getMethod("getRawHeight"); screenSize.set( (Integer) mGetRawW.invoke(defaultDisplay), (Integer) mGetRawH.invoke(defaultDisplay) ); } catch (Exception e) { screenSize.set(defaultDisplay.getWidth(), defaultDisplay.getHeight()); e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } return screenSize; } public Bitmap createScreenShotBitmap(Context context, String screenFilePath) { View v = LayoutInflater.from(context).inflate(R.layout.share_screenshot_layout, null); ImageView iv = (ImageView) v.findViewById(R.id.iv); Bitmap bitmap = BitmapFactory.decodeFile(screenFilePath); iv.setImageBitmap(bitmap); //整體佈局 Point point = getRealScreenSize(); v.measure(View.MeasureSpec.makeMeasureSpec(point.x, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(point.y, View.MeasureSpec.EXACTLY)); v.layout(0, 0, point.x, point.y); // Bitmap result = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.RGB_565); Bitmap result = Bitmap.createBitmap(v.getWidth(), v.getHeight() + dp2px(context, 140), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(result); c.drawColor(Color.WHITE); // Draw view to canvas v.draw(c); return result; } private int dp2px(Context ctx, float dp) { float scale = ctx.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } /** * 設定截圖監聽器 */ public void setListener(OnScreenShotListener listener) { mListener = listener; } public interface OnScreenShotListener { void onShot(String imagePath); } private static void assertInMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { StackTraceElement[] elements = Thread.currentThread().getStackTrace(); String methodMsg = null; if (elements != null && elements.length >= 4) { methodMsg = elements[3].toString(); } throw new IllegalStateException("Call the method must be in main thread: " + methodMsg); } } /** * 媒體內容觀察者(觀察媒體資料庫的改變) */ private class MediaContentObserver extends ContentObserver { private Uri mContentUri; public MediaContentObserver(Uri contentUri, Handler handler) { super(handler); mContentUri = contentUri; } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); handleMediaContentChange(mContentUri); } } }

全域性使用

我們需求是要在APP中全域性都能監聽截圖操作,所以,我們只需要在BaseActivity中進行監聽就可以了。

@Override
protected void onResume() {
    super.onResume();
    startScreenShotListen();
}

@Override
protected void onPause() {
    super.onPause();
    stopScreenShotListen();
}

/**
 * 監聽
 */
private void startScreenShotListen() {
    if (!isHasScreenShotListener && screenShotListenManager != null) {
        screenShotListenManager.setListener(new ScreenShotListenManager.OnScreenShotListener() {
            @Override
            public void onShot(String imagePath) {

                path = imagePath;
                Log.d("msg", "BaseActivity -> onShot: " + "獲得截圖路徑:" + imagePath);

                MyDialog ksDialog = MyDialog.getInstance()
                        .init(BaseActivity.this, R.layout.dialog_layout)
                        .setCancelButton("取消", null)
                        .setPositiveButton("檢視", new MyDialog.OnClickListener() {
                            @Override
                            public void OnClick(View view) {
                                Bitmap screenShotBitmap = screenShotListenManager.createScreenShotBitmap(mContext, path);

                                // 此處只要分享這個合成的Bitmap圖片就行了
                                // 為了演示,故寫下面程式碼
                                screenShotIv.setImageBitmap(screenShotBitmap);
                            }
                        });

                screenShotIv = (ImageView) ksDialog.getView(R.id.iv);
                progressBar = (ProgressBar) ksDialog.getView(R.id.avLoad);
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        progressBar.setVisibility(View.GONE);
                        Glide.with(mContext).load(path).into(screenShotIv);

                    }
                }, 1500);
            }
        });
        screenShotListenManager.startListen();
        isHasScreenShotListener = true;
    }
}

/**
 * 停止監聽
 */
private void stopScreenShotListen() {
    if (isHasScreenShotListener && screenShotListenManager != null) {
        screenShotListenManager.stopListen();
        isHasScreenShotListener = false;
    }
}

至此APP內監聽截圖操作就完成了,我們需要在baseActivity中執行監聽並執行相應操作,不需要寫更多程式碼。

相關推薦

android App

Android截圖功能是一個常用的功能,可以方便的用來分享或者傳送給好友,本文介紹瞭如何實現app內截圖監控功能,當發現使用者在我們的app內進行了截圖操作時,進行對圖片的二次操作,例如新增二維碼,公司logo等一系列*。 專案地址 測試截圖:

Android 長按識別 zxing

#基於 Zxing, 初學Android 程式碼質量不高 //長按,通過zxing讀取圖片,判斷是否有二維碼 bigImage.setOnLongClickListener(new View.OnLongClickListener() { @Overrid

在cocos creator裏添

loading element cnblogs htm prevent touch func tee brush 在cocos creator裏添加二維碼,剛開始用webview做,在微信裏,安卓可以識別,但是IOS識別不了。後來用了地址跳轉的方法實現了,但是每次返回的時候

iOS開發-生成圖片【附中間帶有小】(QRCode)

獲取 options reat 很多 mapr 過濾 生成二維碼 image bit 生成二維碼圖片也是項目中常用到的,二維碼的掃描Git上有很多好用的,這裏主要說下二維碼的生成 1.普通二維碼 1.1 方法 /** 生成二維碼 QRStering:字符串 image

【轉】如何在您的PCB大作上添

designer 處理 加工 想法 圖片 程序 使用 ict 共享 開篇先給大家來段新聞截選: “8月20日,新加坡總理李顯龍在國慶群眾大會上演講時,稱中國移動支付(電子支付)領先全球,新加坡的移動支付還很落後,上海路邊攤都有移動支付,新加坡

PHP生成帶logo的兩種方法

width 調用 height word api 生成 table 中間 log 本文主要和大家分享PHP生成帶logo圖像二維碼的兩種方法,主要以文字和代碼的形式和大家分享,希望能幫助到大家。 一、利用Google API生成二維碼Google提供了較為完善的二維碼生

瀏覽器網頁網址喚起微信app跳轉到指定任意頁面識別方法ticket生成研究

“weixin://dl/stickers” “weixin://dl/games” “weixin://dl/moments” “weixin://dl/add” “weixin://dl/shopping” “weixin://dl/groupchat” “weixin://dl/scan” “

利用同程wx.17u.cn微信ticket協議調起微信app跳轉到任意站url教程

wx.17u.cn是同程旗下和微信合作的weixin://協議介面,該介面是通用的標準瀏覽器h5協議。方法如下生成: //成熟平臺案例www.wxticket.com app搜尋下載“同程攻略” 下載同程攻略app https://sj.qq.com/myapp/detail.htm?apk

小程式中圖片點選預覽、長按識別的問題

通過自己的測試以及各類部落格資料的查詢,總結如下: 1.小程式中的圖片不能識別除小程式碼以外的二維碼 2.並且僅在 wx.previewImage 中支援長按識別 官方文件(wx.previewImage元件) html程式碼(這裡我就簡單的添加了一張圖片做測

帶動態背景生成器

ani graph 設置 idt thead for循環 分解 處理 jar BitQR-Code Through image generate QR-Code . 一個優雅的 QR 二維碼生成器 Github項目地址 : https://github.com/Caster

Android中Webview與原生介面互動及掃描功能實現

最近專案中有一個新的需求,大致是這樣的:APP中通過WebView展示一個第三方的HTML5介面,使用者可以在HTML5介面中呼叫Android攝像頭進行二維碼掃描,並將掃描結果顯示在HTML5介面。這顯然涉及到了Android原生與WebView之前的傳值

微信小程式識別跳轉 圖片預覽,放大圖片 + 識別 出現的坑2個

wx.previewImage(OBJECT) 預覽圖片。自己程式碼 微信小程式圖片的放大,開啟圖片, changebig:function(e){ var that=this let bigimage = that.data.api + '/Uploads/' +

Android使用zxing-android-embedded(由zxing開發)實現生成和掃描

前言: 目前二維碼(條形碼)的使用非常廣泛,所以啊,就想去實現以下嘛,最簡單的方法就是選擇開源庫了。 在網上一查開源庫還是很多的,介紹使用最多的就是zxing,所以這裡也就是用zxing了。但是由於zxing開源庫太大了,有很多不是Android要用的,所以

小程式中圖片點選預覽、長按轉發、儲存、識別

隨著小程式的發展微信小程式中為了更加方便使用者體驗,在小程式中新增圖片預覽、長按轉發、儲存、收藏、識別圖中二維碼等 前端程式碼: <view wx:for="{{imgalist}

微信建瀏覽器 長按識別 功能的兩三個坑與解決方案

其實版主沒有找到根本原因;根本原因是,META裡面設的width問題 ,也就是頁面縮放引起的,跟定位什麼的沒有關係.需要用JS初始化頁面來控制縮放,就能解決問題. 我找到合適的解決方法了,類似等高佈局的原理,注意: 1.給你的二維碼加上一個巨大的pa

長按識別

iv_zxingimag .setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View viewm) { sav

Android記錄貼:使用ZXing來實現掃描

參考資料 前言 最近一個專案需要用到掃描二維碼的功能,在網上查了一下,都是使用google的ZXing開源庫來實現的 第一步:匯入依賴 匯入依賴包,目前最新的是3.3.2,可以通過這裡來檢視最新版本 implementation 'com.go

Android 文本接口TextWatcher詳解

n) sta listener ret vertical ear top ica lock TextWatcher是一個用來監聽文本變化的接口,使用該接口可以很方便的對可顯示文本控件和可編輯文本控件中的文字進行監聽和修改 TextWatcher接口中定義了三個方法: p

android view 轉Bitmap 生成

directory ora ESS 使用 stack return android 簡易 ref 文章鏈接:https://mp.weixin.qq.com/s/FQmYfT-KYiDbp-0HzK_Hpw 項目中經常會用到分享的功能,有分享鏈接也有分享圖片,其中分享圖

Android App檢測更新新版本APK

調用 led com nta contex context smis 手動 方法 Rayland主板雖然作為一塊基於Android的工控板,但是很多設備廠商並不想讓用戶看到Android系統信息。所以APK默認設置為開機啟動項、img去除了Android頭部和底部菜單。但是