前言
12月中旬產(chǎn)品提出了一個(gè)需求义黎,截屏分享的功能咖城。我想這個(gè)需求網(wǎng)上已經(jīng)一大堆文章了。所以這里我就大致說一下聚至。
解決方案
1、FileObserver監(jiān)聽截圖文件目錄數(shù)據(jù)改變本橙。
2扳躬、ContentProvider監(jiān)聽數(shù)據(jù)的改變。
FileObserver
不熟悉FileObserver的同學(xué)請(qǐng)點(diǎn)擊這里,采用FileObserver方式
則需要根據(jù)廠商所在的截屏文件文件夾路徑進(jìn)行適配甚亭,這點(diǎn)就有點(diǎn)煩哦贷币。所以最終我選擇ContentProvider的方式監(jiān)聽文件數(shù)據(jù)的變動(dòng)。
ContentObserver
ContentProvider用于將應(yīng)用數(shù)據(jù)共享出去狂鞋,ContentObserver 內(nèi)容觀察者用于獲取共享數(shù)據(jù)片择,使用它即可監(jiān)聽到數(shù)據(jù)的變更。
創(chuàng)建內(nèi)容觀察者對(duì)象
public class CaptureFileObserver extends ContentObserver {
private final Uri mContentUri;
private final CaptureCallback mCaptureCallback;
public CaptureFileObserver(Uri contentUri, CaptureCallback captureCallback, Handler handler) {
super(handler);
mCaptureCallback = captureCallback;
mContentUri = contentUri;
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
// 觸發(fā)了截屏 注意這里會(huì)多次回調(diào)
if (mCaptureCallback != null){
mCaptureCallback.onMediaFileChanged(mContentUri);
}
}
/**
* 內(nèi)容觀察者回調(diào)事件
*/
public interface CaptureCallback {
void onMediaFileChanged(Uri contentUri);
}
}
當(dāng)數(shù)據(jù)發(fā)生變化之后骚揍,將會(huì)回調(diào)onChange()方法通知我們數(shù)據(jù)發(fā)生了變化字管。
注冊(cè)內(nèi)容觀察者
public abstract class MediaFileBaseObserver implements CaptureFileObserver.CaptureCallback {
protected Context mContext;
private final Handler mHandler = new Handler(Looper.getMainLooper());
/**
* 獲取截屏事件回調(diào)
*/
protected CaptureCallback mCaptureCallback;
private final CaptureFileObserver mCaptureInternalFileObserver;
private final CaptureFileObserver mCaptureExternalFileObserver;
private final Uri[] mContentUris = {Media.INTERNAL_CONTENT_URI, Media.EXTERNAL_CONTENT_URI};
protected final ContentResolver mContentResolver;
protected long mStartListenTime;
public MediaFileBaseObserver(Context context) {
mContext = context;
mContentResolver = mContext.getContentResolver();
// 內(nèi)部外部媒體文件的監(jiān)聽
mCaptureInternalFileObserver = new CaptureFileObserver(mContentUris[0], this, mHandler);
mCaptureExternalFileObserver = new CaptureFileObserver(mContentUris[1], this, mHandler);
}
/**
* 開始進(jìn)行捕捉截屏監(jiān)聽
*/
public void registerCaptureListener(){
// 記錄開始監(jiān)聽的時(shí)間 算是一個(gè)圖片是否是截屏的一個(gè)指標(biāo)
mStartListenTime = System.currentTimeMillis();
// 注意 第二個(gè)boolean參數(shù) 要設(shè)置為true 不然有些機(jī)型由于多媒體文件層級(jí)不同 導(dǎo)致變化監(jiān)聽不到 所以設(shè)置后代文件夾發(fā)生了文件改變也要進(jìn)行通知
mContentResolver.registerContentObserver(mContentUris[0],true, mCaptureInternalFileObserver);
mContentResolver.registerContentObserver(mContentUris[1],true, mCaptureExternalFileObserver);
}
/**
* 解除綁定
*/
public void unregisterCaptureListener(){
mContentResolver.unregisterContentObserver(mCaptureInternalFileObserver);
mContentResolver.unregisterContentObserver(mCaptureExternalFileObserver);
}
/**
* 設(shè)置回調(diào)監(jiān)聽
* @param captureCallback 回調(diào)
*/
public void setCaptureCallbackListener(CaptureCallback captureCallback){
mCaptureCallback = captureCallback;
}
@Override
public void onMediaFileChanged(Uri contentUri) {
acquireTargetFile(contentUri);
}
/**
* 獲取目標(biāo)的文件
* @param contentUri 內(nèi)容URI
*/
abstract void acquireTargetFile(Uri contentUri);
}
這里我們對(duì)外部存儲(chǔ)圖片文件夾和內(nèi)部存儲(chǔ)圖片文件夾進(jìn)行注冊(cè)監(jiān)聽啰挪。若發(fā)生了文件變化,則從這兩個(gè)路徑中拿所有的圖片文件路徑嘲叔,并且進(jìn)行按照?qǐng)D片的添加順序進(jìn)行降序排序并且限制數(shù)量為1亡呵,也就是說取第一張圖片。
內(nèi)部存儲(chǔ)
content://media/internal/images/media
外部存儲(chǔ)
content://media/external/images/media
public class MediaImageObserver extends MediaFileBaseObserver {
private static final String TAG = MediaImageObserver.class.getSimpleName();
@SuppressLint("StaticFieldLeak")
private static volatile MediaImageObserver mInstance = null;
private static final String[] MEDIA_STORE_IMAGE = {
MediaStore.Images.ImageColumns.DATA,
// 時(shí)間 這里不能用 Date_ADD 因?yàn)槭敲爰?jí) 按時(shí)間篩選不準(zhǔn)確
MediaStore.Images.ImageColumns.DATE_TAKEN,
// 寬
MediaStore.Images.ImageColumns.WIDTH
};
// 截屏關(guān)鍵詞 隨時(shí)補(bǔ)充
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", "Screenshot","截屏"
};
// 按照日期插入的順序取第一條
private final static String QUERY_ORDER_SQL = ImageColumns.DATE_ADDED + " DESC LIMIT 1";
private final Point mPoint;
public static MediaFileBaseObserver getInstance(Application application) {
if (mInstance == null) {
synchronized (MediaFileBaseObserver.class) {
if (mInstance == null) {
mInstance = new MediaImageObserver(application.getApplicationContext());
}
}
}
return mInstance;
}
public MediaImageObserver(Context context) {
super(context);
mPoint = ScreenUtil.getRealScreenSize(context);
}
@Override
void acquireTargetFile(Uri contentUri) {
Cursor cursor = null;
try {
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
Bundle bundle = new Bundle();
// 按照文件時(shí)間
bundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{FileColumns.DATE_TAKEN});
// 降序
bundle.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
// 取第一張
bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
cursor = mContentResolver.query(contentUri, MEDIA_STORE_IMAGE, bundle,null);
} else {
// 查找
cursor = mContentResolver.query(contentUri, MEDIA_STORE_IMAGE, null, null, QUERY_ORDER_SQL);
}
findImagePathByCursor(cursor);
} catch (Exception e) {
if (e.getMessage() != null) {
Log.e(TAG, e.getMessage());
} else {
e.printStackTrace();
}
}finally {
if (cursor != null && !cursor.isClosed()){
cursor.close();
}
}
}
private void findImagePathByCursor(Cursor cursor) {
if (cursor == null) {
return;
}
if (!cursor.moveToFirst()){
Log.d(TAG,"Cannot find newest image file");
return;
}
// 獲取 文件索引
int imageColumnIndexData = cursor.getColumnIndex(ImageColumns.DATA);
int imageCreateDateIndexData = cursor.getColumnIndex(ImageColumns.DATE_TAKEN);
int imageWidthColumnIndexData = cursor.getColumnIndex(ImageColumns.WIDTH);
String imagePath = cursor.getString(imageColumnIndexData);
int imageWidth = cursor.getInt(imageWidthColumnIndexData);
long imageCreateDate = cursor.getLong(imageCreateDateIndexData);
// 時(shí)間判斷 判斷截屏?xí)r間 與 截屏圖片實(shí)際生成時(shí)間的差
if (imageCreateDate < mStartListenTime) {
return;
}
// 這里只判斷width 長(zhǎng)截屏無法判斷
if (mPoint != null && mPoint.x != imageWidth){
return;
}
// path 為空
if (TextUtils.isEmpty(imagePath)){
return;
}
// 判斷關(guān)鍵詞
String lowerCasePath = imagePath.toLowerCase();
// 關(guān)鍵詞比對(duì)
for (String keyword : KEYWORDS) {
if (lowerCasePath.contains(keyword)){
if (mCaptureCallback != null) {
mCaptureCallback.capture(imagePath);
}
break;
}
}
}
}
代碼很簡(jiǎn)單硫戈,不過有個(gè)坑在于當(dāng)我們采用以下的查詢方法的時(shí)候锰什,在編譯版本30,Android 11機(jī)型下丁逝,會(huì)報(bào)一個(gè)異常汁胆。
private final static String QUERY_ORDER_SQL = ImageColumns.DATE_ADDED + " DESC LIMIT 1";
mContentResolver.query(contentUri, MEDIA_STORE_IMAGE, null, null, QUERY_ORDER_SQL);
費(fèi)了一番查找最終找到,若在Android 11 版本后進(jìn)行共享數(shù)據(jù)的查詢霜幼,需要使用ContentReslover#query()方法參數(shù)為Bundle的方法嫩码,查看官方文檔,將查詢條件使用Bundle組裝并跨進(jìn)程傳輸罪既。詳細(xì)問題解決方案
總結(jié)
截屏分享Android原生并沒有提供相關(guān)的Api铸题,讓我們獲取,但是解決辦法還是有的琢感,就是通過ContentObserver進(jìn)行對(duì)內(nèi)外存儲(chǔ)文件的變動(dòng)的監(jiān)聽丢间,之后根據(jù)ContentResolver進(jìn)行Query查詢,并進(jìn)行排序篩選驹针,在進(jìn)行二次一系列的條件篩選烘挫,最終找到我們那張截圖的圖片。
補(bǔ)充 2021/02/05
問題1
在應(yīng)用到實(shí)際項(xiàng)目中時(shí)牌捷,發(fā)現(xiàn)當(dāng)應(yīng)用退出到后臺(tái)時(shí)墙牌,用戶截取圖片的時(shí)候,會(huì)將非該應(yīng)用的截圖響應(yīng)到自己的應(yīng)用中暗甥,并觸發(fā)分享喜滨,這導(dǎo)致分享不合乎邏輯。
解決辦法在感知到文件系統(tǒng)發(fā)生變化時(shí)撤防,判斷一下當(dāng)前應(yīng)用是否處于前臺(tái)即可虽风。
/**
* 判斷app是否在后臺(tái)啊
*
* @return 0 在后臺(tái) 1 在前臺(tái) 2 不存在
*/
public static int isBackground(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.processName.equals(context.getPackageName())) {
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED) {
return 2;
} else if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return 1;
}
}
}
return 2;
}
問題2
在某些低端機(jī)型,比如紅米6等寄月,由于使用數(shù)據(jù)庫查詢cursor 比較慢辜膝,導(dǎo)致分享回調(diào)有延遲,用戶可能跳轉(zhuǎn)到其他的界面了漾肮,才展示彈窗厂抖,影響用戶體驗(yàn),因此這邊做了一個(gè)等待延遲條件克懊,判斷當(dāng)前時(shí)間與最終截圖回調(diào)時(shí)間做對(duì)比忱辅,設(shè)定一個(gè)閾值攔截七蜘。
問題3
截屏黑名單,有些界面涉及到用戶敏感信息墙懂,所以就不觸發(fā)用戶截屏橡卤。
@Override
protected void onCreate(Bundle savedInstanceState) {
// 禁掉截屏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
super.onCreate(savedInstanceState);
}