Android更换头像 适配7.0

更换头像是个对一群人毫无存在感的功能,但是对另外一群人炒鸡常用的功能。

6.0相机需要申请动态权限,7.0认为直接使用本地真实路径Uri不安全会抛出异常。
针对这两个比较大的改变,在下站在了一堆巨人的肩膀上蹦了几下,由其是郭神。
文中几处关键代码出自其出版的《第一行代码》第二版的8.3章节。

本文结构

  1. 最终效果展示
  2. 关键代码说明
  3. 主要代码逻辑
  4. github项目地址
  5. 补充

最终效果展示

布局.png
图片剪切.png
展示已剪切图片.png

备注:MIUI系统优化了剪切功能,允许放大缩小,即使设置了固定的目标值。

关键代码说明

相机相关
//尝试打开相机 无权限则申请权限
private void tryOpenCamera() {
        //判断sdk大于6.0则先请求动态访问相机的权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if ((ContextCompat.checkSelfPermission(getActivity(),
                    Manifest.permission.READ_CONTACTS) !=
                    PackageManager.PERMISSION_GRANTED)) {
                //进入到这里代表没有权限,下面申请权限
                //用户点击同意或拒绝后会进入onRequestPermissionsResult
                ProfilePicFragment.this.requestPermissions(
                   new String[]{Manifest.permission.CAMERA}, 
                   REQUEST_PERMISSION_CAMERA_CODE);
            } else {
                //sdk大于6.0,但已有权限
                openCamera();
            }
        } else {
            //sdk小于6.0 注意:Manifest中应已配置了相机的使用权限 
            openCamera();
        }
    }
   //打开相机
    private void openCamera() {
        initFile();//定义用户存储拍摄图片的文件
        // 启动相机程序
        Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        startActivityForResult(intent, TAKE_PHOTO);//拍摄成功后将跳转至onActivityResult
    }

imageUri是用于储存照片的文件路径,在initFile()中设置其值。

 //创建文件并获取其路径的Uri
    private void initFile() {
        // 创建File对象,用于存储拍照后生成的图片
        File outputImage = new File(path + profilePicFileName);//使用应用关联缓存目录存放图片
        try {
            if (outputImage.exists()) {
                outputImage.delete();
            }
            outputImage.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (Build.VERSION.SDK_INT < 24) {
            //7.0之前,调用Uri的FromFile()方法将File对象转换为Uri对象,
             这个Uri对象标识着user_profile_picture.jpg这张图片的本地真实路径
            imageUri = Uri.fromFile(outputImage);
        } else {
            //7.0之后,直接使用本地真实路径的Uri被认为是不安全的,会抛出FileUriExposed异常。
            //因此使用FileProvider的getUriForFile方法将Uri转换成一个封装过的Uri对象
            //FileProvider是一个特殊的内容提供器,我们需要在Manifest中对其进行定义
           //(具体请查询Manifest中<provider>..<provider/>)
            imageUri = FileProvider.getUriForFile(getActivity(), 
                           BuildConfig.APPLICATION_ID+".fileprovider", outputImage);
        }
    }

使用FileProvider需要在Manifest中配置以下代码,并添加res/xml/file_paths.xml文件

<!--android:authorities值应该与上一段代码中的BuildConfig.APPLICATION_ID+".fileprovider"一致-->
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"     
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

file_paths.xml文件

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="profilePic" path="" />
</paths>
<!--path属性表示共享的具体路径,设置为空表示将整个SD卡共享-->
相册相关
//尝试打开相册
 private void tryOpenAlbum() {
        //请求访问SD卡的动态权限,该权限在4.4之后被认为是危险权限
        if (ContextCompat.checkSelfPermission(getActivity(), 
              Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            //进入此处代表没有访问SD卡的权限 下面申请权限
            //用户点击同意或拒绝后会进入onRequestPermissionsResult
            ProfilePicFragment.this.requestPermissions(
                 new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 
                 REQUEST_PERMISSION_SD_CODE);
        } else {
            openAlbum();
        }
    }

主要代码逻辑

/**
 * 完成图片选择,剪裁后展示的功能
 * 代码按照用户操作思路安排方法的顺序
 */
public class ProfilePicFragment extends Fragment implements View.OnClickListener {
       ...
      变量定义
       ...

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
     ...
        //获取应用关联缓存目录
        //6.0之后,读写SD卡被列为危险权限,如果将图片放在SD卡的任何其他位置,都需要运行时权限,
        // 而使用应用关联目录可以跳过这一步
        path = getActivity().getExternalCacheDir();
     ...
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        from_album.setOnClickListener(this);
        from_camera.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.from_album:
                getPicFromAlum();//从相册获取头像图片
                break;
            case R.id.from_camera:
                getPicFromCamera();//使用相机拍摄图片
                break;
            default:
                break;
        }
    }

    /**
     * 正常从相册获取图片的逻辑
     * 尝试打开相册->有权限则打开相册->用户选择了图片->跳转至剪裁页面->剪裁成功将图片保存并展示
     */
    private void getPicFromAlum() {
             tryOpenAlbum();//尝试打开相册
    }
    private void tryOpenAlbum() {
                ....
               openAlbum()
    }
    //打开相册
    private void openAlbum() {
        ...
    }
    /**
     * 正常从相机获取图片的逻辑
     * 尝试打开相机->有权限则打开相机->用户拍摄了图片->跳转至剪裁页面->剪裁成功将图片保存并展示
     */
    private void getPicFromCamera() {
        tryOpenCamera();//尝试打开相机
    }
    private void tryOpenCamera() {
        ...
            openCamera();
        ...
    }
    //打开相机
    private void openCamera() {
        initFile();//定义用户存储拍摄图片的文件
        // 启动相机程序
       ...
    }
     //创建文件并获取其路径的Uri
    private void initFile() {
       ...
            imageUri = ...;
       ...
    }
    /**
     * 权限获取结果响应
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            ...
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.d("Result", "onActivityResult: "+requestCode);
        switch (requestCode) {
            case TAKE_PHOTO:
                   //由相机跳转而来
                  cropPhoto(imageUri);// 裁剪图片
                ...
            case CHOOSE_PHOTO:
                     //由相册跳转而来
                      cropPhoto(uri);// 裁剪图片
              ...
            case CROP_PHOTO:
                //由剪裁Activity跳转而来
                ...
        }
    }
    /**
     * 调用系统的裁剪功能
     */
    public void cropPhoto(Uri uri) {
        ..
    }
    //保存图片
    private void setPicToView(Bitmap mBitmap) {
       ...
}

github项目地址

https://github.com/snowowolf/profilepicturedemo

补充

  1. 关于7.0之后应用间共享文件的FileProvider,hongyang大神有一篇新鲜出炉的文章
    http://mp.weixin.qq.com/s/0BFFoyJdrzkfk6k66tHtyA
  2. 启动时没有加载已经上次保存的图片
    可以参考以下代码
       //对本地图片压缩后再显示
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; // 只获取图片的大小信息,而不是将整张图片载入在内存中,避免内存溢出
        BitmapFactory.decodeFile(path + imgFileName, options);
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 2; // 默认像素压缩比例,压缩为原图的1/2
        int minLen = Math.min(height, width); // 原图的最小边长
        if (minLen > 100) { // 如果原始图像的最小边长大于100dp(此处单位我认为是dp,而非px)
            float ratio = (float) minLen / 100.0f; // 计算像素压缩比例
            inSampleSize = (int) ratio;
        }
        options.inJustDecodeBounds = false; // 计算好压缩比例后,这次可以去加载原图了
        options.inSampleSize = inSampleSize; // 设置为刚才计算的压缩比例
        Bitmap bt = BitmapFactory.decodeFile(path + imgFileName, options);// 从SD卡中找头像,转换成Bitmap

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

推荐阅读更多精彩内容