弃掉Android 4.4获取系统图片出错之坑,实现 自定义相册库

2017年2月份,笔者为了一个项目搞了几天的相册,项目比较急,所以应付了事。昨天突然想起要把这个坑填上,所以“重操旧业”吧。

说到相册,我们首先拥有打开系统文件的权限,其次要获取系统相册中的图片。众所周知,要获取一张图片资源,你没有该图片的“资源路径”是万万行不通的,

在Google Android 4.4 [KitKat] 之前路径返回的相册路径[真实的路径]是这个【content://media/external/images/media/53470】;当Google Android 4.4 [KitKat] 出来

的时候,一种新的相册路径[姑且称为假的路径]是这个[content://com.android.providers.media.documents/document/image%3A48974]  ,也就是说Google 推出的原

生Android路径是两种。

然而中国作为全世界最大的Android 手机厂商国家,基于Android 开放的源代码 ,定制属于自己的一套Android系统 ,为了凸显各自手机系统的优点和特性,加大

市场的竞争优势,一份由Google 给出的 Android 源代码 被改的“面目全非” 。当然,这种现象在互联网发展的历史上又不是没有出现过。比如:OSI [开放系统互联] 当时在

Internet发展早期,致力余研究一种标准协议,以用来屏蔽不同硬件厂商的硬件之间存在差异,提出了分层概念模型和接口的概念,当时TCP/IP算是“义军”突起,强势以

“方便用户[硬件厂商]”的宗旨,抢占市场大量份额,导致OSI这个真正应用于Internet,在这里我们不讨论OSI协议和TCP/IP协议的异同。

由此我们可知道,真正符合用户需求,便利用户才能在当今互联网+物联网时代拔得头筹。

下面我们说说中国各大Android手机厂商之间Android 系统相册返回的图片路径:

Google Android 4.4 之前:content://media/external/images/media/53470

Google Android 4.4 时:content://com.android.providers.media.documents/document/image%3A48974

诺基亚6 Android 7.0 [图库]: content://media/external/images/media/3781

诺基亚6 Android 7.0 [文件管理]:content://com.fihtdc.filemanager.provider/shared_files/ storage/emulated/0/%E8%A1%A8%E6%83%85%E7%8E%8B%E5%9B%BD/
               %E6%89%98%E9%A9%AC%E6%96%AF%E5%B0%8F%E7%81%AB%E8%BD%A6/%E6%89%98%E9%A9%AC%E6%96%

AF%E5% B0%8F%E7%81%AB%E8%BD%A6/ 006uBje5ly1fd3z5c0pb3j306e064q2z_1.jpg

小米 3 :file:///storage/emulated/0/DCIM/Camera/IMG_20160325_233243.jpg

小米 5 Android 7.0 :content://com.miui.gallery.open/raw/%2Fstorage%2Femulated%2F0%2FDCIM%2FCamera%2FIMG_20170707_161721.jpg
content://com.android.fileexplorer.fileprovider/external_files/%E8%A1%A8%E6%83%85%E7%8E%8B%E5%9B%BD/e78319cc

7f0000010128350da8d32327.gif

其他的就不一一列举了,相信路径还是存在差异的,针对不同的路径,我们需要写不同的方法来解析相对相应的路径以获取图片资源。你想一下,

系统相册给你的解析方法就那几个,真的能解析到每一个返回的图片路径吗?不说不同牌子的手机系统存在其他的差异,我只能说这是很不现实的。

我们能不能有一种好的解决方法,统一处理这些存在差异的路径,然后提供一个方法,直接获取图片资源。显然这个想法以来于系统相册是行不通的,

我能想到的是既然系统的相册行不通,我就自己写一个 自定义相册库 ,全局搜索文件系统内的图片[这里的全局搜索文件其实Android系统已经帮我们

做了,其实当你启动Android的时候,手机内部会启动想用的程序帮你检查你手机内部内部的文件,然后以数据库表格的形式存储这些数据信息,包括资源

路径,当然这里面的肯定会有优化算法的,并不是每一次启动Android手机都会全局扫描系统文件吧,不然会严重影响用户体验,这个我们是可以证明的:

当你为一台新手机重装系统,第一回启动手机是,是不是觉得特别慢,更新系统重启是不是是是不是特别慢?想想就知道Android手机系统在进行一些初始

化操作,其中文件检索和数据收集肯定会操作的,只是隐藏掉没有给用户提示罢了]。既然全局搜索文件的工作系统已经帮我们做了,我们就可以直接跳过这

一步骤,我们需要需要做的就是统一图片路径,就像设计模式一书中讲到的适配器模式一样【你把最原始的数据给我,经过适配器处理后,给你想要的数据】。

稍加整理,【搞成SDK,这样就可以在各个项目中用了】。

这样处理之后,这种由系统相册导致图片路径的差异就直接被我们屏蔽掉了。仔细想一想,上面的描述是不是想和有分层概念模型和接口的影子呢?

那么问题来了?自定义相册库行不行的通呢?我记得以前和一位实验室的同学在讨论,QQ和微信是怎么处理的相册,那么神奇,几乎适配所有的Android手机,

直到昨天和一位大神聊天,自己又长知识了。QQ和微信告诉我们,这是行的通的,而且QQ和微信也是这么做的


1:我们先不谈“自定义相册库”,我们看看网上那类雷同的文章,没有突出问题的实质,只是阐述了问题的表象。直接上代码

public class AlbumUtil {
    /**
     * 获取文件路径
     */
    @TargetApi(19)
    public static String getFilePath(Context context, Uri uri ,Intent intent){
        boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0] ;
                if ("primary".equalsIgnoreCase(type) == true) {
                    return "${Environment.getExternalStorageDirectory()}/${split[1]}" ;
                }
            } else if (isDownloadsDocument(uri)) {
                //DownloadsProvider
                final String id  = DocumentsContract.getDocumentId(uri) ;
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
                return getDataColumn(context, contentUri, null, null) ;
            } else if (isMediaDocument(uri)) {
                //MediaProvider
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split  = docId.split(":");
                final String  type = split[0];
                Uri contentUri = null;
                if ("image" == type) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video" == type) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio" == type) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }
                final String selection = "_id=?" ;
                final String[] selectionArgs = new String[] { split[1] };
                return getDataColumn(context, contentUri, selection, selectionArgs) ;
            }// MediaProvider
            // DownloadsProvider
        } else if ("content".equalsIgnoreCase(uri.getScheme()) == true) {
            // MediaStore (and general)
            // Return the remote address
            if (isGooglePhotosUri(uri))
                return uri.getLastPathSegment();
            return getDataColumn(context, uri, null, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme()) == true) {
            // File
            return uri.getPath() ;
        }else if (DocumentsContract.isDocumentUri(context, uri) == false){
            getImagePath(context,uri,intent);
        }
        return null ;
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.

     * @param context The context.
     * *
     * @param uri The Uri to query.
     * *
     * @param selection (Optional) Filter used in the query.
     * *
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * *
     * @return The value of the _data column, which is typically a file path.
     */
    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs){
        Cursor cursor = null ;
        final String column = "_data";
        final String[] projection = { column };
        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                final int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
            if (cursor == null){
                return uri.getPath();
            }
        } finally {
            if (cursor != null)
                cursor.close();
            return null ;
        }
        //return null ;
    }

    /**
     * @param uri The Uri to check.
     * *
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    private static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * *
     * @return Whether the Uri authority is DownloadsProvider.
     */
    private static boolean isDownloadsDocument(Uri uri){
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * *
     * @return Whether the Uri authority is MediaProvider.
     */
    private static boolean isMediaDocument(Uri uri){
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }
    //content://com.android.providers.media.documents/document/image%3A48974
    /**
     * @param uri The Uri to check.
     * *
     * @return Whether the Uri authority is Google Photos.
     */
    private static boolean isGooglePhotosUri(Uri uri){
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }

    /**
     * 获取图片路径
     */
    public static String getImagePath(Context context, Uri uri, Intent data){
        Uri selectedImage = data.getData();
        //Log.e(TAG, selectedImage.toString());
        if (selectedImage != null) {
            String uriStr = selectedImage.toString();
            String path = uriStr.substring(10, uriStr.length());
            if (path.startsWith("com.sec.android.gallery3d")) {
                Log.e("Method selectImage", "It's auto backup pic path:" + selectedImage.toString());
                return null ;
            }
        }
        final String[] filePathColumn = { MediaStore.Images.Media.DATA };
        Cursor cursor = context.getContentResolver().query(selectedImage, filePathColumn, null, null, null);
        String mImgPath ;
        if (cursor != null){
            cursor.moveToFirst();
            int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
            String picturePath = cursor.getString(columnIndex);
            mImgPath = picturePath;
            cursor.close();
        }else
        {
            mImgPath = selectedImage.getPath();
        }

        return mImgPath;
    }

  
}

	    //使用intent调用系统提供的相册功能,使用startActivityForResult是为了获取用户选择的图片
            //只打开图库
            //Intent getAlbum = new Intent(Intent.ACTION_PICK,android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            // getAlbum.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");
            // startActivityForResult(getAlbum, IMAGE_CODE);

            //打开了图库、图片、最近图片
            Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("image/jpeg");
            if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.KITKAT){
                startActivityForResult(intent, SELECT_PIC_KITKAT);
            }else{
                startActivityForResult(intent, IMAGE_CODE);
            }


这里的SELECT_PIC_KITKAT和IMAGE_CODE值可以随便取,他们的作痛主要是在onActivityResult(int requestCode ,int resultCode,Intent intent)方法

里面区分到底调用哪个方法来通过Uri uri  来获得 期望“统一”不存在差异的图片路径资源 ,可惜针对的主体是系统相册,不是自定义的相册库,也只能“哭了”。

protected void onActivityResult(int requestCode, int resultCode, Intent data){

        //此处的 RESULT_OK 是系统自定义得一个常量,-1表示获取数据成功
        if (resultCode == -1){
            Bitmap bitmap = null;
            //外界的程序访问ContentProvider所提供数据 可以通过ContentResolver接口
            ContentResolver resolver = getContentResolver();
            //此处的用于判断接收的Activity是不是你想要的那个,IMAGE_CODE为自定义的
            if (requestCode == IMAGE_CODE || requestCode==SELECT_PIC_KITKAT) {
                Uri uri = data.getData();
                System.out.println("phone's path:"+uri.toString());
                String path = "" ;
                if (requestCode == IMAGE_CODE) {
                    path = SDCardDataPickUtil.getFilePath(AlbumActivity.this, uri, data);
                }
                else if (requestCode==SELECT_PIC_KITKAT) {
                    path = SDCardDataPickUtil.getFilePath(AlbumActivity.this, uri, data);
                    if (path == null || "".equals(path))
                        path = SDCardDataPickUtil.getImagePath(AlbumActivity.this, uri, data);
                }
                mPath = path ;
                editor.putString("path",path);
                editor.commit();
                imgPath.setText(path);
                imgShow.setImageBitmap(loadFromSdCard(path));
               
            }
        }else{
            return;
        }


    }



上面这几段段代码,相信你在网上看到太多次了,导致不知道原版是谁[我听说是有人从国外翻译+自己的理解]?我个人观点是第一个吃螃蟹的人别人会夸他勇气可嘉,

第二个乃至以后吃螃蟹的人就不会有人这么夸了,除非有人提出一些独特见解,才有可能收到部分人的夸奖。


2、我们谈一谈 自定义相册库 , 大家可参考这篇文章,我99%还是出自这篇文章 。Android 自定义本地图片加载库,仿微信相册

下面是真片文章作者的思路:

总结一下微信的本地图片加载有以下几个特点,也是提高用户体验的关键点
1、缩略图挨个加载,一个一个加载完毕,直到屏幕所有缩略图都加载完成
2、不等当前屏的所有缩略图加载完,迅速向下滑,滑动停止时立即加载停止页面的图片
3、已经加载成功的缩略图,不管滑出去多远,滑回来的时候不需要重新加载
4、在相册以外的环境中,需要让imageView的宽高比例随图片的宽高比例自动伸缩,而且要在图片加载完毕之前就要预留占位空间

为了满足上面几个要求,主要采用以下几个方法:
0、为了防止图片加载出来OOM,需要对分辨率和颜色的位数进行缩小到合适范围,同时采用LRU [操作胸痛中一种页面置换算法:最近最久为使用] 缓存
1、采用一个定长线程池,线程池的大小等于CPU的数量+1,把所有缩略图加载任务都交给线程池执行,以获得最快的加载效率。
2、在用户快速滑动的时候,没有加载完毕的划走了的图片立即停止加载,将所占线程让出来,让新的加载任务执行。
3、已经加载成功的缩略图,保存到sd卡中,下次再滑动回来的时候,直接从sd卡加载以前保存好的小图,不经过线程池。
4、对于三星这样的手机,其图片全都是宽度大于高度,方向用exif进行记录,图片加载器要读出exif的方向信息,然后通过矩阵进行旋转


代码我就不着办过来了,大家有兴趣可以点击“Android 自定义本地图片相册库,仿微信

这是该文章在Github上的源代码链接:https://github.com/AlexZhuo/AlxImageLoader


最后笔者在重点说一下:

既然我们的程序要和相同文件程序进行数据交互,Android给我们提供了一个强大的组件 “内容提供器” ,不了解的读者可以取百度,这里笔者只做简要的概述。

内容提供器(Content Provider)主要用于在不同应用程序之间实现数据共享的功能,他提供那个了一套完整的机制,允许一个程序访问另外一个程序中的数据,

同时还能保证被访问数据的安全性。

用法有两种:1、使用现有的内容给提供器来读取和操作相应程序中的数据【Android自带的内容提供器:电话簿、短信、媒体、日历等程序】

      2、创建自己的内容提供器给我们的应用程序的数据提供外部访问接口

具体的介绍请看官网:https://developer.android.com/guide/topics/providers/content-providers.html 【应该需要翻墙吧】

读者需要了解的是:ContentResolver 的基本用法[【https://developer.android.com/reference/android/content/ContentResolver.html】 和ContentResolver自带的增删改查方法,笔者认为这篇文章对ContenTResolver的查询方法介绍比较详细   Android 学习笔记 Contacts (一)ContentResolver query 参数详解

如果读者对 projection 【投影】词语感到困惑的话,可以取看下数据库相关的数据,比如【数据库系统概论 第五版】,书中对这个词语有非常详细的描述。

下面是 自定义相册库  最重要的代码:

    /**
     * 从系统相册里面取出图片的uri
     */
    public static void get500PhotoFromLocalStorage(final Context context, final LookUpPhotosCallback completeCallback) {
        new AlxMultiTask<Void,Void,ArrayList<SelectPhotoEntity>>(){

            @Override
            protected ArrayList<SelectPhotoEntity> doInBackground(Void... params) {
                ArrayList<SelectPhotoEntity> allPhotoArrayList = new ArrayList<>();

                Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                ContentResolver mContentResolver = context.getContentResolver();//得到内容处理者实例

                String sortOrder = MediaStore.Images.Media.DATE_MODIFIED + " desc";//设置拍摄日期为倒序
                Log.i("Alex","准备查找图片");
                // 只查询jpeg和png的图片
                Cursor mCursor = mContentResolver.query(mImageUri, new String[]{MediaStore.Images.Media.DATA},
                        MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?",
                        new String[]{"image/jpeg", "image/png"}, sortOrder+" limit 500");
                if (mCursor == null) return allPhotoArrayList;
                int size = mCursor.getCount();
                Log.i("Alex","查到的size是"+size);
                if (size == 0) return allPhotoArrayList;
                for (int i = 0; i < size; i++) {//遍历全部图片
                    mCursor.moveToPosition(i);
                    String path = mCursor.getString(0);// 获取图片的路径
                    SelectPhotoEntity entity = new SelectPhotoEntity();
                    entity.url = path;//将图片的uri放到对象里去
                    allPhotoArrayList.add(entity);
                }
                mCursor.close();
                return allPhotoArrayList;
            }

没错,这个方法是从 系统相册中 取出图片的URL,此时的URL格式是没有多大差异的。看到这里读者可能会有疑惑,这不是瞎扯嘛,请注意,这里的得到的路径是

真是的路径,都是这种路径【/storage/emulated/***/***/文件名.后缀名】,而手机系统自带相册最初资源的路径也是种格式,只不过是当当我们选中图片后

返回给我们的资源的路径不同,导致存在很大差异,就如文章刚开头提到的哪集中路径。所以我们 自定义相册库 的目的在意统一资源路径,说白了就是保持最原始的资

源路径[/storage/emulated] ,其实 自定义相册库 就相当余一个 中间件 ,确保存储区的资源通过原始路径能提供给用户[这里的用户指的是开发人员]使用,就不比再为

资源路径不统一的差异而苦恼了。


  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值