Android實(shí)現(xiàn)讀取相機(jī)(相冊(cè))圖片并進(jìn)行剪裁
我們先說(shuō)一下思路,在android系統(tǒng)中就自帶了圖片剪切的應(yīng)用,所以,我們只需要將我們獲取到的相片傳給圖片剪切應(yīng)用,再將剪切好的相片返回到我們自己的界面顯示就ok了
在開(kāi)發(fā)一些APP的過(guò)程中,我們可能涉及到頭像的處理,比如從手機(jī)或者相冊(cè)獲取頭像,剪裁成自己需要的頭像,設(shè)置或上傳頭像等。網(wǎng)上一些相關(guān)的資料也是多不勝數(shù),但在實(shí)際應(yīng)用中往往會(huì)存在各種問(wèn)題,沒(méi)有一個(gè)完美的解決方案。由于近期項(xiàng)目的需求,就研究了一下,目前看來(lái)還沒(méi)有什么問(wèn)題。
這里我們只討論獲取、剪裁與設(shè)置,上傳流程根據(jù)自己的業(yè)務(wù)需求添加。先上一張流程圖:

這圖是用Google Drive的繪圖工具繪制的,不得不贊嘆Google可以把在線編輯工具做得如此強(qiáng)大。好吧,我就是Google的腦殘粉!回到主題,這是我設(shè)計(jì)的思路,接下來(lái)進(jìn)行詳細(xì)分析:
1、獲得圖片的途徑無(wú)非就兩種,第一是相機(jī)拍攝,第二是從本地相冊(cè)獲取。
2、我在SD卡上創(chuàng)建了一個(gè)文件夾,里面有兩個(gè)Uri,一個(gè)是用于保存拍照時(shí)獲得的原始圖片,一個(gè)是保存剪裁后的圖片。之前我考慮過(guò)用同一個(gè)Uri來(lái)保存圖片,但是在實(shí)踐中遇到一個(gè)問(wèn)題,當(dāng)拍照后不進(jìn)行剪裁,那么下次從SD卡拿到就是拍照保存的大圖,不僅丟失了之前剪裁的圖片,還會(huì)因?yàn)榧虞d大圖導(dǎo)致內(nèi)存崩潰?;诖丝紤],我選擇了兩個(gè)Uri來(lái)分別保存圖片。
3、相機(jī)拍攝時(shí),我們使用Intent調(diào)用系統(tǒng)相機(jī),并將設(shè)置輸出設(shè)置到SDCard\xx\photo_file.jpg,以下是代碼片段:
//調(diào)用系統(tǒng)相機(jī) Intent intentCamera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //將拍照結(jié)果保存至photo_file的Uri中,不保留在相冊(cè)中 intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imagePhotoUri); startActivityForResult(intentCamera, PHOTO_REQUEST_CAREMA);
在回調(diào)時(shí),我們需要對(duì)photo_file.jpg調(diào)用系統(tǒng)工具進(jìn)行剪裁,并設(shè)置輸出設(shè)置到SDCard\xx\crop_file.jpg,以下是代碼片段:
case PHOTO_REQUEST_CAREMA:
if (resultCode == RESULT_OK) {
//從相機(jī)拍攝保存的Uri中取出圖片,調(diào)用系統(tǒng)剪裁工具
if (imagePhotoUri != null) {
CropUtils.cropImageUri(this, imagePhotoUri, imageUri, ibUserIcon.getWidth(), ibUserIcon.getHeight(), PHOTO_REQUEST_CUT);
} else {
ToastUtils.show(this, "沒(méi)有得到拍照?qǐng)D片");
}
} else if (resultCode == RESULT_CANCELED) {
ToastUtils.show(this, "取消拍照");
} else {
ToastUtils.show(this, "拍照失敗");
}
break;
//調(diào)用系統(tǒng)的剪裁處理圖片并保存至imageUri中
public static void cropImageUri(Activity activity, Uri orgUri, Uri desUri, int width, int height, int requestCode) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(orgUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", width);
intent.putExtra("outputY", height);
intent.putExtra("scale", true);
//將剪切的圖片保存到目標(biāo)Uri中
intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);
intent.putExtra("return-data", false);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true);
activity.startActivityForResult(intent, requestCode);
}
最后,我們需要在回調(diào)中取出crop_file.jpg,因?yàn)榧舨脮r(shí),對(duì)圖片已經(jīng)進(jìn)行了壓縮,所以也不用擔(dān)心內(nèi)存的問(wèn)題,在這里我提供兩個(gè)方法,第一個(gè)是直接獲取原始圖片的Bitmap,第二個(gè)是獲取原始圖片并做成圓形,相信大多數(shù)的人對(duì)后者比較感興趣,哈哈!以下是代碼片段:
case PHOTO_REQUEST_CUT:
if (resultCode == RESULT_OK) {
Bitmap bitmap = decodeUriiAsBimap(this,imageCropUri)
} else if (resultCode == RESULT_CANCELED) {
ToastUtils.show(this, "取消剪切圖片");
} else {
ToastUtils.show(this, "剪切失敗");
}
break;
//從Uri中獲取Bitmap格式的圖片
private static Bitmap decodeUriAsBitmap(Context context, Uri uri) {
Bitmap bitmap;
try {
bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri));
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return bitmap;
}
//獲取圓形圖片
public static Bitmap getRoundedCornerBitmap(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
/* 去鋸齒 */
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
// 保證是方形,并且從中心畫(huà)
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int w;
int deltaX = 0;
int deltaY = 0;
if (width <= height) {
w = width;
deltaY = height - w;
} else {
w = height;
deltaX = width - w;
}
final Rect rect = new Rect(deltaX, deltaY, w, w);
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
// 圓形,所有只用一個(gè)
int radius = (int) (Math.sqrt(w * w * 2.0d) / 2);
canvas.drawRoundRect(rectF, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
return output;
}
4、相冊(cè)獲取時(shí),這也是最難的地方。Android 4.4以下的版本,從相冊(cè)獲取的圖片Uri能夠完美調(diào)用系統(tǒng)剪裁工具,或者直接從選取相冊(cè)是帶入剪裁圖片的Intent,而且效果非常完美。但是在Android 4.4及其以上的版本,獲取到的Uri根本無(wú)法調(diào)用系統(tǒng)剪裁工具,會(huì)直接導(dǎo)致程序崩潰。我也是研究了很久,才發(fā)現(xiàn)兩者的Uri有很大的區(qū)別,Google官方文檔中讓開(kāi)發(fā)者使用Intent.ACTION_GET_CONTENT代替以前的Action,并且就算你仍然使用以前的Action,都會(huì)返回一種新型的Uri,我個(gè)人猜測(cè)是因?yàn)镚oogle把所有的內(nèi)容獲取分享做成一個(gè)統(tǒng)一的Uri,如有不對(duì),請(qǐng)指正!想通這一點(diǎn)后,問(wèn)題就變得簡(jiǎn)單了,我把這種新型的Uri重新封裝一次,得到以為"file:\\..."標(biāo)準(zhǔn)的絕對(duì)路勁,傳入系統(tǒng)剪裁工具中,果然成功了,只是這個(gè)封裝過(guò)程及其艱難,查閱了很多資料,終于還是拿到了。下面說(shuō)下具體步驟:
第一、調(diào)用系統(tǒng)相冊(cè),以下是代碼片段:
//調(diào)用系統(tǒng)相冊(cè)
Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
photoPickerIntent.setType("image/*");
startActivityForResult(photoPickerIntent, PHOTO_REQUEST_GALLERY);
第二、在回調(diào)中,重新封裝Uri,并調(diào)用系統(tǒng)剪裁工具將輸出設(shè)置到crop_file.jpg,調(diào)用系統(tǒng)剪裁工具代碼在拍照獲取的步驟中已經(jīng)貼出,這里就不重復(fù)制造車(chē)輪了,重點(diǎn)貼重新封裝Uri的代碼,以下是代碼片段:
case PHOTO_REQUEST_GALLERY:
if (resultCode == RESULT_OK) {
//從相冊(cè)選取成功后,需要從Uri中拿出圖片的絕對(duì)路徑,再調(diào)用剪切
Uri newUri = Uri.parse("file:///" + CropUtils.getPath(this, data.getData()));
if (newUri != null) {
CropUtils.cropImageUri(this, newUri, imageUri, ibUserIcon.getWidth(),
ibUserIcon.getHeight(), PHOTO_REQUEST_CUT);
} else {
ToastUtils.show(this, "沒(méi)有得到相冊(cè)圖片");
}
} else if (resultCode == RESULT_CANCELED) {
ToastUtils.show(this, "從相冊(cè)選取取消");
} else {
ToastUtils.show(this, "從相冊(cè)選取失敗");
}
break;
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/"+ split[1];
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection,selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
private static String getDataColumn(Context context, Uri uri,String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {column};
try {
cursor = context.getContentResolver().query(uri, projection,selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
后續(xù)的系統(tǒng)剪裁工具調(diào)用跟拍照獲取步驟一致,請(qǐng)參見(jiàn)上的代碼。
5、所有步驟完成,在Nexus 5設(shè)備中的最新系統(tǒng)中測(cè)試通過(guò),在小米、三星等一些設(shè)備中表現(xiàn)也很完美。如果在你的設(shè)備上存在缺陷,一定要跟帖給我反饋,謝謝!
文章結(jié)尾附上一個(gè)網(wǎng)友的完整示例,給了我很多的參考
package com.only.android.app;
import java.io.File;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import com.only.android.R;
public class CopyOfImageScaleActivity extends Activity implements View.OnClickListener {
/** Called when the activity is first created. */
private Button selectImageBtn;
private ImageView imageView;
private File sdcardTempFile;
private AlertDialog dialog;
private int crop = 180;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.imagescale);
selectImageBtn = (Button) findViewById(R.id.selectImageBtn);
imageView = (ImageView) findViewById(R.id.imageView);
selectImageBtn.setOnClickListener(this);
sdcardTempFile = new File("/mnt/sdcard/", "tmp_pic_" + SystemClock.currentThreadTimeMillis() + ".jpg");
}
@Override
public void onClick(View v) {
if (v == selectImageBtn) {
if (dialog == null) {
dialog = new AlertDialog.Builder(this).setItems(new String[] { "相機(jī)", "相冊(cè)" }, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra("output", Uri.fromFile(sdcardTempFile));
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);// 裁剪框比例
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", crop);// 輸出圖片大小
intent.putExtra("outputY", crop);
startActivityForResult(intent, 101);
} else {
Intent intent = new Intent("android.intent.action.PICK");
intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
intent.putExtra("output", Uri.fromFile(sdcardTempFile));
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);// 裁剪框比例
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", crop);// 輸出圖片大小
intent.putExtra("outputY", crop);
startActivityForResult(intent, 100);
}
}
}).create();
}
if (!dialog.isShowing()) {
dialog.show();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (resultCode == RESULT_OK) {
Bitmap bmp = BitmapFactory.decodeFile(sdcardTempFile.getAbsolutePath());
imageView.setImageBitmap(bmp);
}
}
}
最后再啰嗦一句,功能雖然已經(jīng)實(shí)現(xiàn)了,但是實(shí)際代碼還是可以進(jìn)一步優(yōu)化的,感興趣的童鞋們可以改進(jìn)下。
- 基于Android實(shí)現(xiàn)保存圖片到本地并可以在相冊(cè)中顯示出來(lái)
- android獲取相冊(cè)圖片和路徑的實(shí)現(xiàn)方法
- android照相、相冊(cè)獲取圖片剪裁報(bào)錯(cuò)的解決方法
- 解決Android從相冊(cè)中獲取圖片出錯(cuò)圖片卻無(wú)法裁剪問(wèn)題的方法
- android中打開(kāi)相機(jī)、打開(kāi)相冊(cè)進(jìn)行圖片的獲取示例
- Android實(shí)現(xiàn)保存圖片到本地并在相冊(cè)中顯示
- Android實(shí)現(xiàn)長(zhǎng)按圖片保存至相冊(cè)功能
- Android獲取本地相冊(cè)圖片和拍照獲取圖片的實(shí)現(xiàn)方法
- Android 圖片存儲(chǔ)到指定路徑和相冊(cè)的方法
- Android實(shí)現(xiàn)選擇相冊(cè)圖片并顯示功能
相關(guān)文章
android studio3.0.1無(wú)法啟動(dòng)Gradle守護(hù)進(jìn)程的解決方法
這篇文章主要為大家詳細(xì)介紹了android studio3.0.1無(wú)法啟動(dòng)Gradle守護(hù)進(jìn)程的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Android使用ViewPager實(shí)現(xiàn)頂部tabbar切換界面
這篇文章主要為大家詳細(xì)介紹了使用ViewPager實(shí)現(xiàn)頂部tabbar切換界面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
實(shí)例講解Android應(yīng)用中自定義組合控件的方法
這篇文章主要介紹了實(shí)例講解Android應(yīng)用中自定義組合控件的方法,通過(guò)例子講解了view組合控件及自定義屬性的用法,需要的朋友可以參考下2016-04-04
Android中Intent傳遞對(duì)象的兩種方法Serializable,Parcelable
這篇文章主要介紹了Android中的傳遞有兩個(gè)方法,一個(gè)是Serializable,另一個(gè)是Parcelable,對(duì)intent傳遞對(duì)象的兩種方法感興趣的朋友一起學(xué)習(xí)吧2016-01-01
Android利用SurfaceView實(shí)現(xiàn)簡(jiǎn)單計(jì)時(shí)器
這篇文章主要為大家詳細(xì)介紹了Android利用SurfaceView實(shí)現(xiàn)一個(gè)簡(jiǎn)單計(jì)時(shí)器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
在Android打包中區(qū)分測(cè)試和正式環(huán)境淺析
這篇文章主要給大家介紹了關(guān)于在Android打包中如何區(qū)分測(cè)試和正式環(huán)境的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起看看吧。2017-10-10
Android逆向入門(mén)之常見(jiàn)Davlik字節(jié)碼解析
Dalvik是Google公司自己設(shè)計(jì)用于Android平臺(tái)的虛擬機(jī)。Dalvik虛擬機(jī)是Google等廠商合作開(kāi)發(fā)的Android移動(dòng)設(shè)備平臺(tái)的核心組成部分之一,本篇文章我們來(lái)詳細(xì)解釋常見(jiàn)Davlik字節(jié)碼2021-11-11
總結(jié)Android中多線程更新應(yīng)用的頁(yè)面信息的方式
這篇文章主要介紹了總結(jié)Android中多線程更新應(yīng)用的頁(yè)面信息的方式,文中共總結(jié)了runOnUiThread、Handler、AsyncTask異步以及View直接在UI線程中更新的方法,需要的朋友可以參考下2016-02-02

