Android實現(xiàn)讀取相機(相冊)圖片并進行剪裁
我們先說一下思路,在android系統(tǒng)中就自帶了圖片剪切的應(yīng)用,所以,我們只需要將我們獲取到的相片傳給圖片剪切應(yīng)用,再將剪切好的相片返回到我們自己的界面顯示就ok了
在開發(fā)一些APP的過程中,我們可能涉及到頭像的處理,比如從手機或者相冊獲取頭像,剪裁成自己需要的頭像,設(shè)置或上傳頭像等。網(wǎng)上一些相關(guān)的資料也是多不勝數(shù),但在實際應(yīng)用中往往會存在各種問題,沒有一個完美的解決方案。由于近期項目的需求,就研究了一下,目前看來還沒有什么問題。
這里我們只討論獲取、剪裁與設(shè)置,上傳流程根據(jù)自己的業(yè)務(wù)需求添加。先上一張流程圖:
這圖是用Google Drive的繪圖工具繪制的,不得不贊嘆Google可以把在線編輯工具做得如此強大。好吧,我就是Google的腦殘粉!回到主題,這是我設(shè)計的思路,接下來進行詳細分析:
1、獲得圖片的途徑無非就兩種,第一是相機拍攝,第二是從本地相冊獲取。
2、我在SD卡上創(chuàng)建了一個文件夾,里面有兩個Uri,一個是用于保存拍照時獲得的原始圖片,一個是保存剪裁后的圖片。之前我考慮過用同一個Uri來保存圖片,但是在實踐中遇到一個問題,當拍照后不進行剪裁,那么下次從SD卡拿到就是拍照保存的大圖,不僅丟失了之前剪裁的圖片,還會因為加載大圖導(dǎo)致內(nèi)存崩潰?;诖丝紤],我選擇了兩個Uri來分別保存圖片。
3、相機拍攝時,我們使用Intent調(diào)用系統(tǒng)相機,并將設(shè)置輸出設(shè)置到SDCard\xx\photo_file.jpg,以下是代碼片段:
//調(diào)用系統(tǒng)相機 Intent intentCamera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //將拍照結(jié)果保存至photo_file的Uri中,不保留在相冊中 intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imagePhotoUri); startActivityForResult(intentCamera, PHOTO_REQUEST_CAREMA);
在回調(diào)時,我們需要對photo_file.jpg調(diào)用系統(tǒng)工具進行剪裁,并設(shè)置輸出設(shè)置到SDCard\xx\crop_file.jpg,以下是代碼片段:
case PHOTO_REQUEST_CAREMA: if (resultCode == RESULT_OK) { //從相機拍攝保存的Uri中取出圖片,調(diào)用系統(tǒng)剪裁工具 if (imagePhotoUri != null) { CropUtils.cropImageUri(this, imagePhotoUri, imageUri, ibUserIcon.getWidth(), ibUserIcon.getHeight(), PHOTO_REQUEST_CUT); } else { ToastUtils.show(this, "沒有得到拍照圖片"); } } 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); //將剪切的圖片保存到目標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,因為剪裁時,對圖片已經(jīng)進行了壓縮,所以也不用擔心內(nèi)存的問題,在這里我提供兩個方法,第一個是直接獲取原始圖片的Bitmap,第二個是獲取原始圖片并做成圓形,相信大多數(shù)的人對后者比較感興趣,哈哈!以下是代碼片段:
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); // 保證是方形,并且從中心畫 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); // 圓形,所有只用一個 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、相冊獲取時,這也是最難的地方。Android 4.4以下的版本,從相冊獲取的圖片Uri能夠完美調(diào)用系統(tǒng)剪裁工具,或者直接從選取相冊是帶入剪裁圖片的Intent,而且效果非常完美。但是在Android 4.4及其以上的版本,獲取到的Uri根本無法調(diào)用系統(tǒng)剪裁工具,會直接導(dǎo)致程序崩潰。我也是研究了很久,才發(fā)現(xiàn)兩者的Uri有很大的區(qū)別,Google官方文檔中讓開發(fā)者使用Intent.ACTION_GET_CONTENT代替以前的Action,并且就算你仍然使用以前的Action,都會返回一種新型的Uri,我個人猜測是因為Google把所有的內(nèi)容獲取分享做成一個統(tǒng)一的Uri,如有不對,請指正!想通這一點后,問題就變得簡單了,我把這種新型的Uri重新封裝一次,得到以為"file:\\..."標準的絕對路勁,傳入系統(tǒng)剪裁工具中,果然成功了,只是這個封裝過程及其艱難,查閱了很多資料,終于還是拿到了。下面說下具體步驟:
第一、調(diào)用系統(tǒng)相冊,以下是代碼片段:
//調(diào)用系統(tǒng)相冊 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ù)制造車輪了,重點貼重新封裝Uri的代碼,以下是代碼片段:
case PHOTO_REQUEST_GALLERY: if (resultCode == RESULT_OK) { //從相冊選取成功后,需要從Uri中拿出圖片的絕對路徑,再調(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, "沒有得到相冊圖片"); } } else if (resultCode == RESULT_CANCELED) { ToastUtils.show(this, "從相冊選取取消"); } else { ToastUtils.show(this, "從相冊選取失敗"); } 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)用跟拍照獲取步驟一致,請參見上的代碼。
5、所有步驟完成,在Nexus 5設(shè)備中的最新系統(tǒng)中測試通過,在小米、三星等一些設(shè)備中表現(xiàn)也很完美。如果在你的設(shè)備上存在缺陷,一定要跟帖給我反饋,謝謝!
文章結(jié)尾附上一個網(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[] { "相機", "相冊" }, 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)實現(xiàn)了,但是實際代碼還是可以進一步優(yōu)化的,感興趣的童鞋們可以改進下。
相關(guān)文章
android studio3.0.1無法啟動Gradle守護進程的解決方法
這篇文章主要為大家詳細介紹了android studio3.0.1無法啟動Gradle守護進程的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-08-08Android使用ViewPager實現(xiàn)頂部tabbar切換界面
這篇文章主要為大家詳細介紹了使用ViewPager實現(xiàn)頂部tabbar切換界面,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08實例講解Android應(yīng)用中自定義組合控件的方法
這篇文章主要介紹了實例講解Android應(yīng)用中自定義組合控件的方法,通過例子講解了view組合控件及自定義屬性的用法,需要的朋友可以參考下2016-04-04Android中Intent傳遞對象的兩種方法Serializable,Parcelable
這篇文章主要介紹了Android中的傳遞有兩個方法,一個是Serializable,另一個是Parcelable,對intent傳遞對象的兩種方法感興趣的朋友一起學習吧2016-01-01Android利用SurfaceView實現(xiàn)簡單計時器
這篇文章主要為大家詳細介紹了Android利用SurfaceView實現(xiàn)一個簡單計時器,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01在Android打包中區(qū)分測試和正式環(huán)境淺析
這篇文章主要給大家介紹了關(guān)于在Android打包中如何區(qū)分測試和正式環(huán)境的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起看看吧。2017-10-10Android逆向入門之常見Davlik字節(jié)碼解析
Dalvik是Google公司自己設(shè)計用于Android平臺的虛擬機。Dalvik虛擬機是Google等廠商合作開發(fā)的Android移動設(shè)備平臺的核心組成部分之一,本篇文章我們來詳細解釋常見Davlik字節(jié)碼2021-11-11總結(jié)Android中多線程更新應(yīng)用的頁面信息的方式
這篇文章主要介紹了總結(jié)Android中多線程更新應(yīng)用的頁面信息的方式,文中共總結(jié)了runOnUiThread、Handler、AsyncTask異步以及View直接在UI線程中更新的方法,需要的朋友可以參考下2016-02-02