Android截屏功能是一個(gè)常用的功能,可以方便的用來分享或者發(fā)送給好友,本文介紹了如何實(shí)現(xiàn)app內(nèi)截屏監(jiān)控功能,當(dāng)發(fā)現(xiàn)用戶在我們的app內(nèi)進(jìn)行了截屏操作時(shí),進(jìn)行對(duì)圖片的二次操作,例如添加二維碼,公司logo等一系列*。
成都創(chuàng)新互聯(lián)公司專注于寧津企業(yè)網(wǎng)站建設(shè),成都響應(yīng)式網(wǎng)站建設(shè)公司,商城開發(fā)。寧津網(wǎng)站建設(shè)公司,為寧津等地區(qū)提供建站服務(wù)。全流程按需求定制制作,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務(wù)
項(xiàng)目地址
測試截圖:
截屏原理
android系統(tǒng)并沒有提供截屏通知相關(guān)的API,需要我們自己利用系統(tǒng)能提供的相關(guān)特性變通實(shí)現(xiàn)。Android系統(tǒng)有一個(gè)媒體數(shù)據(jù)庫,每拍一張照片,或使用系統(tǒng)截屏截取一張圖片,都會(huì)把這張圖片的詳細(xì)信息加入到這個(gè)媒體數(shù)據(jù)庫,并發(fā)出內(nèi)容改變通知,我們可以利用內(nèi)容觀察者(ContentObserver)監(jiān)聽媒體數(shù)據(jù)庫的變化,當(dāng)數(shù)據(jù)庫有變化時(shí),獲取最后插入的一條圖片數(shù)據(jù),如果該圖片符合特定的規(guī)則,則認(rèn)為被截屏了。
判斷依據(jù)
當(dāng)ContentObserver監(jiān)聽到媒體數(shù)據(jù)庫的數(shù)據(jù)改變, 在有數(shù)據(jù)改變時(shí) 獲取最后插入數(shù)據(jù)庫的一條圖片數(shù)據(jù), 如果符合以下規(guī)則, 則認(rèn)為截屏了:
這些判斷是為了增加截屏檢測結(jié)果的可靠性,防止誤報(bào),防止遺漏。其中截屏圖片的路徑正常Android系統(tǒng)保存的路徑格式, 例如我的是:“外部存儲(chǔ)器/storage/emulated/0/Pictures/Screenshots/Screenshot_2017-08-03-15-42-58.png”,但Android系統(tǒng)碎片化嚴(yán)重,加上其他第三方截屏APP等,所以路徑關(guān)鍵字除了檢查是否包含“screenshot”外,還可以適當(dāng)增加其他關(guān)鍵字,詳見最后的監(jiān)聽器完整代碼。這種監(jiān)聽截屏的方法也不是100%準(zhǔn)確,例如某些被root的機(jī)器使用第三方截屏APP自定義保存路徑,還比如通過ADB命令在電腦上獲取手機(jī)屏幕快照均不能監(jiān)聽到,但這也是目前可行性最高的方法,對(duì)于絕大多數(shù)用戶都比較靠譜。
代碼描述
監(jiān)聽截屏
public class ScreenShotListenManager { private static final String TAG = "ScreenShotListenManager"; /** * 讀取媒體數(shù)據(jù)庫時(shí)需要讀取的列 */ private static final String[] MEDIA_PROJECTIONS = { MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN, }; /** * 讀取媒體數(shù)據(jù)庫時(shí)需要讀取的列, 其中 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, }; /** * 截屏依據(jù)中的路徑判斷關(guān)鍵字 */ 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; /** * 已回調(diào)過的路徑 */ private final static List<String> sHasCallbackPaths = new ArrayList<String>(); private Context mContext; private OnScreenShotListener mListener; private long mStartListenTime; /** * 內(nèi)部存儲(chǔ)器內(nèi)容觀察者 */ private MediaContentObserver mInternalObserver; /** * 外部存儲(chǔ)器內(nèi)容觀察者 */ private MediaContentObserver mExternalObserver; /** * 運(yùn)行在 UI 線程的 Handler, 用于運(yùn)行監(jiān)聽器回調(diào) */ 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; // 獲取屏幕真實(shí)的分辨率 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); } /** * 啟動(dòng)監(jiān)聽 */ public void startListen() { assertInMainThread(); // sHasCallbackPaths.clear(); // 記錄開始監(jiān)聽的時(shí)間戳 mStartListenTime = System.currentTimeMillis(); // 創(chuàng)建內(nèi)容觀察者 mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler); mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler); // 注冊內(nèi)容觀察者 mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver ); mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver ); } /** * 停止監(jiān)聽 */ public void stopListen() { assertInMainThread(); // 注銷內(nèi)容觀察者 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; } // 清空數(shù)據(jù) mStartListenTime = 0; // sHasCallbackPaths.clear(); //切記!?。?必須設(shè)置為空 可能mListener 會(huì)隱式持有Activity導(dǎo)致釋放不掉 mListener = null; } /** * 處理媒體數(shù)據(jù)庫的內(nèi)容改變 */ private void handleMediaContentChange(Uri contentUri) { Cursor cursor = null; try { // 數(shù)據(jù)改變時(shí)查詢數(shù)據(jù)庫中最后加入的一條數(shù)據(jù) 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); } // 獲取行數(shù)據(jù) 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 之前, 寬高要手動(dòng)獲取 Point size = getImageSize(data); width = size.x; height = size.y; } // 處理獲取到的第一行數(shù)據(jù) 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); } /** * 處理獲取到的一行數(shù)據(jù) */ 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 { // 如果在觀察區(qū)間媒體數(shù)據(jù)庫有數(shù)據(jù)改變,又不符合截屏規(guī)則,則輸出到 log 待分析 Log.w(TAG, "Media content changed, but not screenshot: path = " + data + "; size = " + width + " * " + height + "; date = " + dateTaken); } } /** * 判斷指定的數(shù)據(jù)行是否符合截屏條件 */ private boolean checkScreenShot(String data, long dateTaken, int width, int height) { /* * 判斷依據(jù)一: 時(shí)間判斷 */ // 如果加入數(shù)據(jù)庫的時(shí)間在開始監(jiān)聽之前, 或者與當(dāng)前時(shí)間相差大于10秒, 則認(rèn)為當(dāng)前沒有截屏 if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) { return false; } /* * 判斷依據(jù)二: 尺寸判斷 */ if (sScreenRealSize != null) { // 如果圖片尺寸超出屏幕, 則認(rèn)為當(dāng)前沒有截屏 if (!((width <= sScreenRealSize.x && height <= sScreenRealSize.y) || (height <= sScreenRealSize.x && width <= sScreenRealSize.y))) { return false; } } /* * 判斷依據(jù)三: 路徑判斷 */ if (TextUtils.isEmpty(data)) { return false; } data = data.toLowerCase(); // 判斷圖片路徑是否含有指定的關(guān)鍵字之一, 如果有, 則認(rèn)為當(dāng)前截屏了 for (String keyWork : KEYWORDS) { if (data.contains(keyWork)) { return true; } } return false; } /** * 判斷是否已回調(diào)過, 某些手機(jī)ROM截屏一次會(huì)發(fā)出多次內(nèi)容改變的通知; <br/> * 刪除一個(gè)圖片也會(huì)發(fā)通知, 同時(shí)防止刪除圖片時(shí)誤將上一張符合截屏規(guī)則的圖片當(dāng)做是當(dāng)前截屏. */ 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); } /** * 設(shè)置截屏監(jiān)聽器 */ 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); } } /** * 媒體內(nèi)容觀察者(觀察媒體數(shù)據(jù)庫的改變) */ 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中全局都能監(jiān)聽截屏操作,所以,我們只需要在BaseActivity中進(jìn)行監(jiān)聽就可以了。
@Override protected void onResume() { super.onResume(); startScreenShotListen(); } @Override protected void onPause() { super.onPause(); stopScreenShotListen(); } /** * 監(jiān)聽 */ 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); // 此處只要分享這個(gè)合成的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; } } /** * 停止監(jiān)聽 */ private void stopScreenShotListen() { if (isHasScreenShotListener && screenShotListenManager != null) { screenShotListenManager.stopListen(); isHasScreenShotListener = false; } }
至此APP內(nèi)監(jiān)聽截屏操作就完成了,我們需要在baseActivity中執(zhí)行監(jiān)聽并執(zhí)行相應(yīng)操作,不需要寫更多代碼。
源碼地址>>
總結(jié)
以上所述是小編給大家介紹的Android App內(nèi)監(jiān)聽截圖加二維碼功能代碼,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)創(chuàng)新互聯(lián)網(wǎng)站的支持!
當(dāng)前名稱:AndroidApp內(nèi)監(jiān)聽截圖加二維碼功能代碼
文章鏈接:http://www.rwnh.cn/article16/gdihdg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站營銷、域名注冊、網(wǎng)站導(dǎo)航、電子商務(wù)、ChatGPT、商城網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)