頭像設(shè)置是Android 開發(fā)中比較常見的需求戳粒,基本內(nèi)容有:
- 通過相機(jī)拍取一張圖片,或者從圖庫(kù)、文件中選擇一張圖片
- 對(duì)圖片進(jìn)行裁剪
- 上傳圖片祖很,監(jiān)聽上傳進(jìn)度
通過相機(jī)拍取圖片
調(diào)用相機(jī)拍照需要兩個(gè)步驟:
- 應(yīng)用內(nèi)生成一個(gè)空文件愿险,使用
FileProvider
獲取文件的Uri
颇蜡,照片最后會(huì)保存在這個(gè)文件里价说。一定要確保這個(gè)文件所在目錄在FileProvider
的共享目錄下
- 使用
Intent
啟動(dòng)相機(jī),并將文件Uri
傳入(相當(dāng)于告訴相機(jī)將照片保存到這個(gè)文件里)
創(chuàng)建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
創(chuàng)建文件,注意文件一定要在FileProvider
共享文件的目錄中缤弦,否則會(huì)報(bào)錯(cuò):
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
啟動(dòng)相機(jī)拍照并傳入生成的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);
}
這樣當(dāng)拍照成功后照片就存儲(chǔ)在我們創(chuàng)建的文件中了
從圖庫(kù)或文件選擇圖片
從圖庫(kù)選擇文件:
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);
從圖庫(kù)或文件選擇圖片:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, REQUEST_IMAGE_PICK);
獲取結(jié)果:
@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;
}
}
對(duì)圖片進(jìn)行裁剪
由于系統(tǒng)提供了圖片裁剪的功能,這樣就減小了開發(fā)者自己實(shí)現(xiàn)圖片裁剪的難度碍沐,這里使用系統(tǒng)提供的圖片裁剪功能狸捅。和拍照類似,為了獲得裁剪后的圖片累提,我們也需要?jiǎng)?chuàng)建一個(gè)空文件尘喝,并將其傳入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);
// 使圖片處于可裁剪狀態(tài)
intent.putExtra("crop", "true");
// 裁剪框的比例(根據(jù)需要顯示的圖片比例進(jìn)行設(shè)置)
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();
// 處理錯(cuò)誤
showDialog("打開文件失敗");
return;
}
cropImageUri = FileProvider.getUriForFile(getContext(), AUTHORITY, cropFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImageUri);
// 設(shè)置裁剪區(qū)域的形狀斋陪,默認(rèn)為矩形瞧省,也可設(shè)置為原形
intent.putExtra("circleCrop", false);
// 設(shè)置圖片的輸出格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
// return-data=true傳遞的為縮略圖,小米手機(jī)默認(rèn)傳遞大圖鳍贾,所以會(huì)導(dǎo)致onActivityResult調(diào)用失敗
intent.putExtra("return-data", false);
// 是否需要人臉識(shí)別
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);
}
}
好了鞍匾,現(xiàn)在來梳理下拍照、選圖片骑科、裁剪的流程:
- 通過
Initent
啟動(dòng)系統(tǒng)功能并傳入處理后圖片的保存位置(如果需要)
- 通過
onActivityResult
獲取調(diào)用是否成功橡淑,并做進(jìn)一步處理
注意:
- 因?yàn)橄到y(tǒng)權(quán)限的限制,可能需要對(duì)目標(biāo)程序加入對(duì)應(yīng)的讀寫權(quán)限
-
選擇圖片返回的
Uri
直接做為圖片裁剪的源文件可能導(dǎo)致出錯(cuò)咆爽,這個(gè)時(shí)候需要將原來的Uri
復(fù)制到我們自己生成的文件中梁棠,再啟動(dòng)圖片裁剪
圖片上傳并顯示進(jìn)度
圖片上傳本質(zhì)是文件上傳,這里使用Retrofit
添加進(jìn)度回調(diào)接口:
public interface ProgressRequestListener {
void onRequestProgress(long bytesSend, long contentLength, boolean done);
}
編寫ProgressRequestBody
類實(shí)現(xiàn)插入進(jìn)度回調(diào):
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);
//必須調(diào)用flush斗埂,否則最后一部分?jǐn)?shù)據(jù)可能不會(huì)被寫入
bufferedSink.flush();
}
private Sink sink(Sink sink) {
return new ForwardingSink(sink) {
//當(dāng)前寫入字節(jié)數(shù)
long bytesWritten = 0L;
//總字節(jié)長(zhǎng)度符糊,避免多次調(diào)用contentLength()方法
long contentLength = 0L;
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
if (contentLength == 0) {
//獲得contentLength的值,后續(xù)不再調(diào)用
contentLength = contentLength();
}
//增加當(dāng)前寫入的字節(jié)數(shù)
bytesWritten += byteCount;
//回調(diào)
for (ProgressRequestListener listener : listeners) {
listener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
}
}
};
}
}
編寫服務(wù)器接口:
@Multipart
@POST("xxx")
Observable<FileBean> uploadAvatar(@Part MultipartBody.Part part);
服務(wù)器接口調(diào)用:
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); // 根據(jù)需要填寫名字
return serverApi.uploadAvatar(body);
}
好了呛凶,我們只需要把裁剪后的圖片上傳男娄,并添加ProgressRequestListener
,頭像設(shè)置就搞定了~
參考文章
官方培訓(xùn)
Android 啟動(dòng)系統(tǒng)相機(jī)漾稀,相冊(cè)模闲,裁剪圖片及6.0權(quán)限管理
你需要知道的Android拍照適配方案
Android之圖片選擇與裁剪
Retrofit 2.0 超能實(shí)踐(三),輕松實(shí)現(xiàn)多文件/圖片上傳/Json字符串/表單
OkHttp與Retrofit上傳文件詳解
Retrofit Multipart多文件上傳