Android 截屏分享

前言

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);

SQL 異常.png

費(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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市损搬,隨后出現(xiàn)的幾起案子碧库,更是在濱河造成了極大的恐慌,老刑警劉巖巧勤,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嵌灰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡颅悉,警方通過查閱死者的電腦和手機(jī)伞鲫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來签舞,“玉大人,你說我怎么就攤上這事柒瓣∪宕睿” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵芙贫,是天一觀的道長(zhǎng)搂鲫。 經(jīng)常有香客問我,道長(zhǎng)磺平,這世上最難降的妖魔是什么魂仍? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮拣挪,結(jié)果婚禮上擦酌,老公的妹妹穿的比我還像新娘。我一直安慰自己菠劝,他們只是感情好赊舶,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赶诊,像睡著了一般笼平。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舔痪,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天寓调,我揣著相機(jī)與錄音,去河邊找鬼锄码。 笑死夺英,一個(gè)胖子當(dāng)著我的面吹牛晌涕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秋麸,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼渐排,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了灸蟆?” 一聲冷哼從身側(cè)響起驯耻,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炒考,沒想到半個(gè)月后可缚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斋枢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年帘靡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓤帚。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡描姚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出戈次,到底是詐尸還是另有隱情轩勘,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布怯邪,位于F島的核電站绊寻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏悬秉。R本人自食惡果不足惜澄步,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望和泌。 院中可真熱鬧村缸,春花似錦、人聲如沸武氓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聋丝。三九已至索烹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弱睦,已是汗流浹背百姓。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留况木,地道東北人垒拢。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓旬迹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親求类。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奔垦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容