編輯:關於Android編程
Android截屏功能是一個常用的功能,可以方便的用來分享或者發送給好友,本文介紹了如何實現app內截屏監控功能,當發現用戶在我們的app內進行了截屏操作時,進行對圖片的二次操作,例如添加二維碼,公司logo等一系列*。
項目地址
測試截圖:
截屏原理
android系統並沒有提供截屏通知相關的API,需要我們自己利用系統能提供的相關特性變通實現。Android系統有一個媒體數據庫,每拍一張照片,或使用系統截屏截取一張圖片,都會把這張圖片的詳細信息加入到這個媒體數據庫,並發出內容改變通知,我們可以利用內容觀察者(ContentObserver)監聽媒體數據庫的變化,當數據庫有變化時,獲取最後插入的一條圖片數據,如果該圖片符合特定的規則,則認為被截屏了。
判斷依據
當ContentObserver監聽到媒體數據庫的數據改變, 在有數據改變時 獲取最後插入數據庫的一條圖片數據, 如果符合以下規則, 則認為截屏了:
這些判斷是為了增加截屏檢測結果的可靠性,防止誤報,防止遺漏。其中截屏圖片的路徑正常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開發一個簡單的健康食譜軟件。用到的相關技術如下所示:提供GridView和ListView的基本使用利用univers
ELF是類Unix類系統,當然也包括Android系統上的可執行文件格式(也包括.so和.o類文件)。可以理解為Android系統上的exe或者dll文件&
一、第一種錯誤:錯誤日志大體是這樣:The project is using an unsupported version of the Android Gradle p
0x01.簡介AsyncTask is designed to be a helper class around Thread and Handler and does