Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总

图片选择器在手机应用中屡见不鲜,设置头像、聊天传图等常见类似场景都需要使用。为了保持不同设备上体验的一致性和较好的兼容性,比较稳妥的做法是在应用内自实现相机拍照、相册选图和图片裁剪功能。但是,这个实现过程比较复杂,费时费力。更多时候,或者说在项目初期,我们都会选择直接调用系统提供的这些功能来完成一个图片选择器。然而,由于安卓设备的多样性,总会遇到各种各样的兼容问题。本文就来总结总结,调用系统相机、相册和裁剪功能实现图片选择器的过程中,我们需要注意的一些地方。

示例代码


这里简单使用一个示例代码,演示调用系统相机或相册,获取图片,然后使用系统裁剪功能处理图片,并显示到一个 ImageButton 视图里面:

public class MainActivity extends FragmentActivity {

    public static final int REQUEST_CAMERA = 1;
    public static final int REQUEST_ALBUM = 2;
    public static final int REQUEST_CROP = 3;

    public static final String IMAGE_UNSPECIFIED = "image/*";

    private ImageButton mPictureIb;

    private File mImageFile;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPictureIb = (ImageButton) findViewById(R.id.ib_picture);
    }

    public void onClickPicker(View v) {
        new AlertDialog.Builder(this)
                .setTitle("选择照片")
                .setItems(new String[]{"拍照", "相册"}, new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        if (i == 0) {
                            selectCamera();
                        } else {
                            selectAlbum();
                        }
                    }
                })
                .create()
                .show();
    }
    
    private void selectCamera() {
        createImageFile();
        if (!mImageFile.exists()) {
            return;
        }

        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
        startActivityForResult(cameraIntent, REQUEST_CAMERA);
    }

    private void selectAlbum() {
        Intent albumIntent = new Intent(Intent.ACTION_PICK);
        albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_UNSPECIFIED);
        startActivityForResult(albumIntent, REQUEST_ALBUM);
    }

    private void cropImage(Uri uri){
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
        intent.putExtra("crop", "true");
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
        startActivityForResult(intent, REQUEST_CROP);
    }

    private void createImageFile() {
        mImageFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".jpg");
        try {
            mImageFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "出错啦", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (RESULT_OK != resultCode) {
            return;
        }
        switch (requestCode) {
            case REQUEST_CAMERA:
                cropImage(Uri.fromFile(mImageFile));
                break;

            case REQUEST_ALBUM:
                createImageFile();
                if (!mImageFile.exists()) {
                    return;
                }

                Uri uri = data.getData();
                if (uri != null) {
                    cropImage(uri);
                }
                break;

            case REQUEST_CROP:
                mPictureIb.setImageURI(Uri.fromFile(mImageFile));
                break;
        }
    }

}

效果如图(不同设备,系统功能呈现有所不同):

看似完美,你以为上述代码就能结束了的话,那就大错特错啦!这里面还有一些兼容问题要处理,还有一些地方需要特殊说明。

拍照图片存储问题


调用系统相机实现拍照功能的核心代码如下:

Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);

其中 MediaStore.EXTRA_OUTPUT 数据表示,拍照所得图片保存到指定目录下的文件(一般会在 SD 卡中创建当前应用的目录,并创建临时文件保存图片)。然后,在 onActivityResult 方法中根据文件路径获取图片。

如果不为 intent 添加该数据的话,将在 onActivityResult 的 intent 对象中返回一个 Bitmap 对象,通过如下代码获取:

Bitmap bmp = data.getParcelableExtra("data");

值得注意的是,这里的 Bitmap 对象是拍照所得图片的一个缩略图,尺寸很小!系统这么做也是充分考虑到应用的内存占用问题。试想一下,如今手机设备中高清相机拍出来的照片,一张图的大小高达十几兆,如果返回这么大的图片,内存占用相当严重,何况很多时候知识临时使用而已。所以,调用系统相机时,一般都会添加 MediaStore.EXTRA_OUTPUT 参数,避免返回 Bitmap 对象。当然,这么做也能保证应用产生的数据,包括文件,都能存储在应用目录下,方便清理缓存时统一清除。

拍照图片旋转问题


部分手机,比如三星手机,调用系统相机拍照所得的照片可能会发生自动旋转问题,常见为旋转 90°。所以,要求我们在拍照之后,使用图片之前,判断图片是否发生过旋转,如果是,要将照片旋转回来。

这是获取图片旋转角度的代码:

/**
 * 获取图片旋转角度
 * @param path 图片路径
 * @return
 */
private int parseImageDegree(String path) {
    int degree = 0;
    try {
        ExifInterface exifInterface = new ExifInterface(path);
        int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
        switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_90:
                degree = 90;
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                degree = 180;
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                degree = 270;
                break;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return degree;
}

这是根据指定角度旋转图片的代码:

/**
 * 图片旋转操作
 *
 * @param bm 需要旋转的图片
 * @param degree 旋转角度
 * @return 旋转后的图片
 */
private Bitmap rotateBitmap(Bitmap bm, int degree) {
    Bitmap returnBm = null;

    Matrix matrix = new Matrix();
    matrix.postRotate(degree);
    try {
        returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
    } catch (OutOfMemoryError e) {
    }
    if (returnBm == null) {
        returnBm = bm;
    }
    if (bm != returnBm) {
        bm.recycle();
    }
    return returnBm;
}

横竖屏切换问题


在部分手机,调用系统拍照功能时,可能会发生横竖屏切换过程,导致返回应用时当前 Activity 发生销毁重建,各个生命周期又重新走了一遍。此时,一些应用内的变量数据可能丢失,使用时容易发生空值异常,进而导致 app 崩溃退出。

为了避免这种现象,我们需要在 AndroidManifest.xml 文件的对应 <activity> 标签中添加属性:

android:configChanges="orientation|screenSize"

这样,当发生屏幕旋转时,不会导致 Activity 销毁重建,而是执行 onConfigurationChanged() 方法:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
}

调用系统裁剪问题


示例中调用系统裁剪的代码如下:

Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(intent, REQUEST_CROP);

可以看出,调用系统裁剪功能,需要设置一些 Extra 参数,很多人容易在这里产生疑惑,不知如何取舍,如何设值。这里列举一下常用的 Extra 名字、值类型和作用:

  • crop:String 类型数据,发送裁剪信号
  • aspectXaspectY:int 类型数据,设置裁剪框的 X 与 Y 值比例
  • outputXoutputY:int 类型数据,设置裁剪输出的图片大小
  • scale:boolean 类型数据,设置是否支持裁剪缩放
  • return-data:boolean 类型数据,设置是否在 onActivityResult 方法的 intent 值中返回 Bitmap 对象
  • MediaStore.EXTRA_OUTPUT:Uri 类型数据,设置是否将裁剪结果保存到指定文件中

需要注意的是:

第一,设置 return-data 参数为 true 时,返回的 Bitmap 对象也为缩略图,获取方式与前面所述相机拍照获取 Bitmap 的方式一致;

第二,调用系统相册并裁剪时,如果使用MediaStore.EXTRA_OUTPUT参数,Uri 尽量不要设置为源文件对应的 Uri 值,另做保存,不损坏系统相册中的源图文件;

第三,根据经验,outputX 与 outputY 值设置太大时,容易出现卡屏现象;

第四,可以不设置 outputX 与 outputY 参数,使用户根据自身按比例自由裁剪,就像示例代码这样。

setImageURI() 注意事项


你可能会用到 setImageURI() 方法给 ImageView 设置图片内容,这里也有一个地方需要注意。我们先看一下这个方法的源码:

public void setImageURI(Uri uri) {
    if (mResource != 0 ||
            (mUri != uri &&
             (uri == null || mUri == null || !uri.equals(mUri)))) {
        updateDrawable(null);
        mResource = 0;
        mUri = uri;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        resolveUri();

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
}

可以看到,这里的 uri 参数在内部持有缓存变量,当多次调用该方法而 uri 参数值不变时,图片展示内容不变。问题就在这,如果你多次拍照或裁剪保存的图片文件路径相同时,虽然每次处理过后实际存储的文件内容发生变化,但由于路径相同,uri 参数一致,导致多次调用 setImageURI() 设置图片内容时,ImageView 显示内容不变!这也是为什么示例代码中我用时间戳处理图片文件名的原因所在,保证每次存储的图片路径不同。

根据 Uri 获取文件地址


有时候,我们需要根据 Uri 获取文件路径。比如如果你不需要使用裁剪功能的话,调用系统相册选择图片后返回的就是一个 Uri 对象,我们需要从这个 Uri 对象中解析出对应的图片文件路径,便于上传至服务器等后续处理。

比如,这个 Uri 对象可能是:

content://media/external/images/media/3066

很多朋友相信有过这样的经验,使用 toString() 或者 getPath() 方法获取 Uri 对象所对应的文件路径,其实这是错误的!通过 getPath() 获取的结果字符串是:

media/external/images/media/3066

而正确的获取方式是:

private String parseFilePath(Uri uri) {
    String[] filePathColumn = { MediaStore.Images.Media.DATA };
    Cursor cursor = getContentResolver().query(uri, filePathColumn, null, null, null);
    cursor.moveToFirst();
    int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
    String picturePath = cursor.getString(columnIndex);
    cursor.close();
    return picturePath;
}

其对应的文件路径应该是这个样子的:

/storage/emulated/0/Pictures/Screenshots/S70302-131606.jpg

Base64 文件编码处理


现在很多网络框架内部都做了封装处理,上传图片时只需要传递一个文件路径即可。但是,少数情况下,根据服务器需要,我们要对图片文件字节流编码后再上传。这是使用 Base64 编码并根据字节数组获取字符串的处理过程:

public static String fileToBase64String(String filePath) {
    File photoFile = new File(filePath);
    try {
        FileInputStream fis = new FileInputStream(photoFile);
        ByteArrayOutputStream baos = new ByteArrayOutputStream(10000);
        byte[] buffer = new byte[1000];
        while (fis.read(buffer)!=-1) {
            baos.write(buffer);
        }
        baos.close();
        fis.close();
        return Arrays.toString(Base64.encode(baos.toByteArray(), Base64.DEFAULT));
    }catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

zip 压缩文件处理


当上传多张图片至服务器时,为了提升传输效率,往往会采用 zip 格式压缩处理。这里提供一个递归压缩代码,方便大家有需要的时候借鉴参考:

public String zipCompass(String filePath){
    File zipFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".zip");
    try{
        //指定了两个待压缩的文件,都在assets目录中  
        String[] filenames = new String[]{ "activity_main.xml", "strings.xml" };
        FileOutputStream fos = new FileOutputStream(zipFile);
        ZipOutputStream zos = new ZipOutputStream(fos);
        int i = 1;
        //枚举filenames中的所有待压缩文件  
        while (i <= filenames.length){
            //从filenames数组中取出当前待压缩的文件名,作为压缩后的名称,以保证压缩前后文件名一致  
            ZipEntry zipEntry = new ZipEntry(filenames[i - 1]);
            //打开当前的zipEntry对象  
            zos.putNextEntry(zipEntry);

            FileInputStream is = new FileInputStream(filePath);
            byte[] buffer = new byte[8192];
            int count = 0;
            //写入数据  
            while ((count = is.read(buffer)) >= 0){
                zos.write(buffer, 0, count);
            }
            zos.flush();
            zos.closeEntry();
            is.close();
            i++;

        }
        zos.finish();
        zos.close();
        return zipFile.getAbsolutePath();
    }
    catch (Exception e){
        Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
        return null;
    }
}

添加系统权限


说了这么多,别忘了在 AndroidManifest.xml 文件中添加系统权限(前面示例代码中没有考虑到 Android 6.0 运行时权限的问题,实际使用时注意添加处理):

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容