Android 截屏分享

作者: 巴黎没有摩天轮Li | 来源:发表于2021-01-05 10:21 被阅读0次

    前言

    12月中旬产品提出了一个需求,截屏分享的功能。我想这个需求网上已经一大堆文章了。所以这里我就大致说一下。

    解决方案

    1、FileObserver监听截图文件目录数据改变。
    2、ContentProvider监听数据的改变。

    FileObserver

    不熟悉FileObserver的同学请点击这里,采用FileObserver方式
    则需要根据厂商所在的截屏文件文件夹路径进行适配,这点就有点烦哦。所以最终我选择ContentProvider的方式监听文件数据的变动。

    ContentObserver

    ContentProvider用于将应用数据共享出去,ContentObserver 内容观察者用于获取共享数据,使用它即可监听到数据的变更。

    创建内容观察者对象

    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);
            // 触发了截屏 注意这里会多次回调
            if (mCaptureCallback != null){
                mCaptureCallback.onMediaFileChanged(mContentUri);
            }
        }
        /**
         * 内容观察者回调事件
         */
        public interface CaptureCallback {
    
            void onMediaFileChanged(Uri contentUri);
        }
    }
    
    

    当数据发生变化之后,将会回调onChange()方法通知我们数据发生了变化。

    注册内容观察者

    public abstract class MediaFileBaseObserver implements CaptureFileObserver.CaptureCallback {
        protected Context mContext;
        private final Handler mHandler = new Handler(Looper.getMainLooper());
        /**
         * 获取截屏事件回调
         */
        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();
            // 内部外部媒体文件的监听
            mCaptureInternalFileObserver = new CaptureFileObserver(mContentUris[0], this, mHandler);
            mCaptureExternalFileObserver = new CaptureFileObserver(mContentUris[1], this, mHandler);
        }
        /**
         * 开始进行捕捉截屏监听
         */
        public void registerCaptureListener(){
            // 记录开始监听的时间 算是一个图片是否是截屏的一个指标
            mStartListenTime = System.currentTimeMillis();
            // 注意 第二个boolean参数 要设置为true 不然有些机型由于多媒体文件层级不同 导致变化监听不到 所以设置后代文件夹发生了文件改变也要进行通知
            mContentResolver.registerContentObserver(mContentUris[0],true, mCaptureInternalFileObserver);
            mContentResolver.registerContentObserver(mContentUris[1],true, mCaptureExternalFileObserver);
        }
        /**
         * 解除绑定
         */
        public void unregisterCaptureListener(){
            mContentResolver.unregisterContentObserver(mCaptureInternalFileObserver);
            mContentResolver.unregisterContentObserver(mCaptureExternalFileObserver);
        }
        /**
         * 设置回调监听
         * @param captureCallback 回调
         */
        public void setCaptureCallbackListener(CaptureCallback captureCallback){
            mCaptureCallback = captureCallback;
        }
        @Override
        public void onMediaFileChanged(Uri contentUri) {
            acquireTargetFile(contentUri);
        }
        /**
         * 获取目标的文件
         * @param contentUri 内容URI
         */
        abstract void acquireTargetFile(Uri contentUri);
    }
    

    这里我们对外部存储图片文件夹和内部存储图片文件夹进行注册监听。若发生了文件变化,则从这两个路径中拿所有的图片文件路径,并且进行按照图片的添加顺序进行降序排序并且限制数量为1,也就是说取第一张图片。

    内部存储
    content://media/internal/images/media
    外部存储
    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,
                // 时间 这里不能用 Date_ADD 因为是秒级 按时间筛选不准确
                MediaStore.Images.ImageColumns.DATE_TAKEN,
                // 宽
                MediaStore.Images.ImageColumns.WIDTH
        };
        // 截屏关键词 随时补充
        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.O) {
                    Bundle bundle = new Bundle();
                    // 按照文件时间
                    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);
            // 时间判断 判断截屏时间 与 截屏图片实际生成时间的差
            if (imageCreateDate < mStartListenTime) {
               return;
            }
            // 这里只判断width 长截屏无法判断
            if (mPoint != null && mPoint.x != imageWidth){
                return;
            }
            // path 为空
            if (TextUtils.isEmpty(imagePath)){
                return;
            }
            // 判断关键词
            String lowerCasePath = imagePath.toLowerCase();
            // 关键词比对
            for (String keyword : KEYWORDS) {
                if (lowerCasePath.contains(keyword)){
                    if (mCaptureCallback != null) {
                        mCaptureCallback.capture(imagePath);
                    }
                    break;
                }
            }
        }
    }
    

    代码很简单,不过有个坑在于当我们采用以下的查询方法的时候,在编译版本30,Android 11机型下,会报一个异常。

    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
    费了一番查找最终找到,若在Android 11 版本后进行共享数据的查询,需要使用ContentReslover#query()方法参数为Bundle的方法,查看官方文档,将查询条件使用Bundle组装并跨进程传输。详细问题解决方案

    总结

    截屏分享Android原生并没有提供相关的Api,让我们获取,但是解决办法还是有的,就是通过ContentObserver进行对内外存储文件的变动的监听,之后根据ContentResolver进行Query查询,并进行排序筛选,在进行二次一系列的条件筛选,最终找到我们那张截图的图片。

    相关文章

      网友评论

        本文标题:Android 截屏分享

        本文链接:https://www.haomeiwen.com/subject/ywwloktx.html