Android 头像设置 图片上传 基本解决方案

头像设置是Android 开发中比较常见的需求,基本内容有:

  • 通过相机拍取一张图片,或者从图库、文件中选择一张图片
  • 对图片进行裁剪
  • 上传图片,监听上传进度

通过相机拍取图片

调用相机拍照需要两个步骤:

  1. 应用内生成一个空文件,使用FileProvider获取文件的Uri,照片最后会保存在这个文件里。一定要确保这个文件所在目录在FileProvider的共享目录下
  • 使用Intent启动相机,并将文件Uri传入(相当于告诉相机将照片保存到这个文件里)

创建FileProvider
AndroidManifest.xml中添加provider,注意android:authorities属性:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.xxx.xxx.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
</provider>

/res/xml/目录下新建file_paths.xml文件,定义FileProvider共享文件的目录:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="download"
        path="Download"/>
    <external-files-path
        name="my_images"
        path="Pictures/"/>
</paths>

具体使用方式参见API FileProvider

创建文件,注意文件一定要在FileProvider共享文件的目录中,否则会报错:

public static File createImageFile(Context context) throws IOException {
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
    String imageFileName = "IMG_" + timeStamp;
    File dir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    return File.createTempFile(imageFileName, ".jpg", dir);
}

生成UriAUTHORITYAndroidManifest.xmlandroid:authorities属性:

File imageFile = FileUtil.createImageFile(this);
Uri imageUri = FileProvider.getUriForFile(getContext(), AUTHORITY, imageFile);

使用Intent启动相机拍照并传入生成的Uri

Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
}

这样当拍照成功后照片就存储在我们创建的文件中了


从图库或文件选择图片

从图库选择文件:

Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        startActivityForResult(intent, REQUEST_IMAGE_PICK);

从图库或文件选择图片:

Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, REQUEST_IMAGE_PICK);

获取结果:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    switch (requestCode) {
        case REQUEST_IMAGE_PICK:
        if (resultCode == RESULT_OK) {
            Uri uri = data.getData();
            if (uri != null) {
                // 获得了图片
            } else {
                // TODO 处理异常
            }
        }
        break;
    }
}

对图片进行裁剪

由于系统提供了图片裁剪的功能,这样就减小了开发者自己实现图片裁剪的难度,这里使用系统提供的图片裁剪功能。和拍照类似,为了获得裁剪后的图片,我们也需要创建一个空文件,并将其传入Intent

void startCropImage(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    // 使图片处于可裁剪状态
    intent.putExtra("crop", "true");
    // 裁剪框的比例(根据需要显示的图片比例进行设置)
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    // 让裁剪框支持缩放
    intent.putExtra("scale", true);
    // 裁剪后图片的大小(注意和上面的裁剪比例保持一致)
    intent.putExtra("outputX", 300);
    intent.putExtra("outputY", 300);
    // 传递原图路径
    try {
        cropFile = FileUtil.getCacheImageFile(this);
    } catch (IOException e) {
        e.printStackTrace();
        // 处理错误
        showDialog("打开文件失败");
        return;
    }
    cropImageUri = FileProvider.getUriForFile(getContext(), AUTHORITY, cropFile);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImageUri);
    // 设置裁剪区域的形状,默认为矩形,也可设置为原形
    intent.putExtra("circleCrop", false);
    // 设置图片的输出格式
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    // return-data=true传递的为缩略图,小米手机默认传递大图,所以会导致onActivityResult调用失败
    intent.putExtra("return-data", false);
    // 是否需要人脸识别
    intent.putExtra("noFaceDetection", true);
    List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    for (ResolveInfo resolveInfo : resInfoList) {
        String packageName = resolveInfo.activityInfo.packageName;
        grantUriPermission(packageName, cropImageUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    }
    if (resInfoList.size() > 0) {
        startActivityForResult(intent, REQUEST_IMAGE_CROP);
    }
}

好了,现在来梳理下拍照、选图片、裁剪的流程:

  1. 通过Initent启动系统功能并传入处理后图片的保存位置(如果需要)
  • 通过onActivityResult获取调用是否成功,并做进一步处理

注意:

  1. 因为系统权限的限制,可能需要对目标程序加入对应的读写权限
  • 选择图片返回的Uri直接做为图片裁剪的源文件可能导致出错,这个时候需要将原来的Uri复制到我们自己生成的文件中,再启动图片裁剪

图片上传并显示进度

图片上传本质是文件上传,这里使用Retrofit

添加进度回调接口:

public interface ProgressRequestListener {
    void onRequestProgress(long bytesSend, long contentLength, boolean done);
}

编写ProgressRequestBody类实现插入进度回调:

public class ProgressRequestBody extends RequestBody {

    private final RequestBody requestBody;
    private final List<ProgressRequestListener> listeners;
    private BufferedSink bufferedSink;

    public ProgressRequestBody(RequestBody requestBody, List<ProgressRequestListener> listeners) {
        this.requestBody = requestBody;
        this.listeners = listeners;
    }

    @Override
    public MediaType contentType() {
        return requestBody.contentType();
    }

    @Override
    public long contentLength() throws IOException {
        return requestBody.contentLength();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        //包装
        bufferedSink = Okio.buffer(sink(sink));
        //写入
        requestBody.writeTo(bufferedSink);
        //必须调用flush,否则最后一部分数据可能不会被写入
        bufferedSink.flush();
    }

    private Sink sink(Sink sink) {
        return new ForwardingSink(sink) {
            //当前写入字节数
            long bytesWritten = 0L;
            //总字节长度,避免多次调用contentLength()方法
            long contentLength = 0L;

            @Override
            public void write(Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
                if (contentLength == 0) {
                    //获得contentLength的值,后续不再调用
                    contentLength = contentLength();
                }
                //增加当前写入的字节数
                bytesWritten += byteCount;
                //回调
                for (ProgressRequestListener listener : listeners) {
                    listener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
                }
            }
        };
    }
}

编写服务器接口:

@Multipart
@POST("xxx")
Observable<FileBean> uploadAvatar(@Part MultipartBody.Part part);

服务器接口调用:

public Observable<FileBean> uploadAvatar(File imageFile) {
    RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), imageFile);
    ProgressRequestBody progressRequestBody = new ProgressRequestBody(requestBody, requestListeners);
    MultipartBody.Part body =
            MultipartBody.Part.createFormData("name", "filename", progressRequestBody);  // 根据需要填写名字
    return serverApi.uploadAvatar(body);
}

好了,我们只需要把裁剪后的图片上传,并添加ProgressRequestListener,头像设置就搞定了~


参考文章

官方培训
Android 启动系统相机,相册,裁剪图片及6.0权限管理
你需要知道的Android拍照适配方案
Android之图片选择与裁剪
Retrofit 2.0 超能实践(三),轻松实现多文件/图片上传/Json字符串/表单
OkHttp与Retrofit上传文件详解
Retrofit Multipart多文件上传

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

推荐阅读更多精彩内容