溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android App內(nèi)監(jiān)聽截圖加二維碼功能代碼

發(fā)布時間:2020-09-26 11:23:18 來源:腳本之家 閱讀:202 作者:zhihui_520 欄目:移動開發(fā)

Android截屏功能是一個常用的功能,可以方便的用來分享或者發(fā)送給好友,本文介紹了如何實現(xiàn)app內(nèi)截屏監(jiān)控功能,當(dāng)發(fā)現(xiàn)用戶在我們的app內(nèi)進行了截屏操作時,進行對圖片的二次操作,例如添加二維碼,公司logo等一系列*。

項目地址

測試截圖:

Android App內(nèi)監(jiān)聽截圖加二維碼功能代碼

截屏原理

android系統(tǒng)并沒有提供截屏通知相關(guān)的API,需要我們自己利用系統(tǒng)能提供的相關(guān)特性變通實現(xiàn)。Android系統(tǒng)有一個媒體數(shù)據(jù)庫,每拍一張照片,或使用系統(tǒng)截屏截取一張圖片,都會把這張圖片的詳細信息加入到這個媒體數(shù)據(jù)庫,并發(fā)出內(nèi)容改變通知,我們可以利用內(nèi)容觀察者(ContentObserver)監(jiān)聽媒體數(shù)據(jù)庫的變化,當(dāng)數(shù)據(jù)庫有變化時,獲取最后插入的一條圖片數(shù)據(jù),如果該圖片符合特定的規(guī)則,則認為被截屏了。

判斷依據(jù)

當(dāng)ContentObserver監(jiān)聽到媒體數(shù)據(jù)庫的數(shù)據(jù)改變, 在有數(shù)據(jù)改變時 獲取最后插入數(shù)據(jù)庫的一條圖片數(shù)據(jù), 如果符合以下規(guī)則, 則認為截屏了:

  • 時間判斷,圖片的生成時間在開始監(jiān)聽之后,并與當(dāng)前時間相隔10秒內(nèi):開始監(jiān)聽后生成的圖片才有意義,相隔10秒內(nèi)說明是剛剛生成的
  • 尺寸判斷,圖片的尺寸沒有超過屏幕的尺寸:圖片尺寸超過屏幕尺寸,不可能是截屏圖片
  • 路徑判斷,圖片路徑符合包含特定的關(guān)鍵詞:這一點是關(guān)鍵,截屏圖片的保存路徑通常包含“screenshot”

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

代碼描述

監(jiān)聽截屏

public class ScreenShotListenManager {
  private static final String TAG = "ScreenShotListenManager";
  /**
   * 讀取媒體數(shù)據(jù)庫時需要讀取的列
   */
  private static final String[] MEDIA_PROJECTIONS = {
      MediaStore.Images.ImageColumns.DATA,
      MediaStore.Images.ImageColumns.DATE_TAKEN,
  };
  /**
   * 讀取媒體數(shù)據(jù)庫時需要讀取的列, 其中 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)部存儲器內(nèi)容觀察者
   */
  private MediaContentObserver mInternalObserver;
  /**
   * 外部存儲器內(nèi)容觀察者
   */
  private MediaContentObserver mExternalObserver;
  /**
   * 運行在 UI 線程的 Handler, 用于運行監(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;
    // 獲取屏幕真實的分辨率
    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);
  }
  /**
   * 啟動監(jiān)聽
   */
  public void startListen() {
    assertInMainThread();
//    sHasCallbackPaths.clear();
    // 記錄開始監(jiān)聽的時間戳
    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 會隱式持有Activity導(dǎo)致釋放不掉
    mListener = null;
  }
  /**
   * 處理媒體數(shù)據(jù)庫的內(nèi)容改變
   */
  private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;
    try {
      // 數(shù)據(jù)改變時查詢數(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 之前, 寬高要手動獲取
        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ù)據(jù)庫的時間在開始監(jiān)聽之前, 或者與當(dāng)前時間相差大于10秒, 則認為當(dāng)前沒有截屏
    if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) {
      return false;
    }
    /*
     * 判斷依據(jù)二: 尺寸判斷
     */
    if (sScreenRealSize != null) {
      // 如果圖片尺寸超出屏幕, 則認為當(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)鍵字之一, 如果有, 則認為當(dāng)前截屏了
    for (String keyWork : KEYWORDS) {
      if (data.contains(keyWork)) {
        return true;
      }
    }
    return false;
  }
  /**
   * 判斷是否已回調(diào)過, 某些手機ROM截屏一次會發(fā)出多次內(nèi)容改變的通知; <br/>
   * 刪除一個圖片也會發(fā)通知, 同時防止刪除圖片時誤將上一張符合截屏規(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中進行監(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);
                // 此處只要分享這個合成的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)聽截圖加二維碼功能代碼,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI