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

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

通过相机拍取一张图片,或者从图库、文件中选择一张图片

对图片进行裁剪

上传图片,监听上传进度

通过相机拍取图片

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

应用内生成一个空文件,使用FileProvider获取文件的Uri,照片最后会保存在这个文件里。一定要确保这个文件所在目录在FileProvider的共享目录下

使用Intent启动相机,并将文件Uri传入(相当于告诉相机将照片保存到这个文件里)

创建FileProvider:

在AndroidManifest.xml中添加provider,注意android:authorities属性:

android:name="android.support.v4.content.FileProvider"

android:authorities="com.xxx.xxx.fileProvider"

android:exported="false"

android:grantUriPermissions="true">

android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/file_paths"/>

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

name="download"

path="Download"/>

name="my_images"

path="Pictures/"/>

具体使用方式参见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);

}

生成Uri,AUTHORITY即AndroidManifest.xml中android: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 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);

}

}

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

通过Initent启动系统功能并传入处理后图片的保存位置(如果需要)

通过onActivityResult获取调用是否成功,并做进一步处理

注意:

因为系统权限的限制,可能需要对目标程序加入对应的读写权限

选择图片返回的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 listeners;

private BufferedSink bufferedSink;

public ProgressRequestBody(RequestBody requestBody, List 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 uploadAvatar(@Part MultipartBody.Part part);

服务器接口调用:

public Observable 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多文件上传


龙盛乾园
为什么会有雀斑