圖片選擇器在手機應用中家常便飯,設置頭像、聊天傳圖等常見相似場景都須要使用。爲了保持不一樣設備上體驗的一致性和較好的兼容性,比較穩妥的作法是在應用內自實現相機拍照、相冊選圖和圖片裁剪功能。可是,這個實現過程比較複雜,費時費力。更多時候,或者說在項目初期,咱們都會選擇直接調用系統提供的這些功能來完成一個圖片選擇器。然而,因爲安卓設備的多樣性,總會遇到各類各樣的兼容問題。本文就來總結總結,調用系統相機、相冊和裁剪功能實現圖片選擇器的過程當中,咱們須要注意的一些地方。java
這裏簡單使用一個示例代碼,演示調用系統相機或相冊,獲取圖片,而後使用系統裁剪功能處理圖片,並顯示到一個 ImageButton 視圖裏面:android
public class MainActivity extends FragmentActivity {
public static final int REQUEST_CAMERA = 1;
public static final int REQUEST_ALBUM = 2;
public static final int REQUEST_CROP = 3;
public static final String IMAGE_UNSPECIFIED = "image/*";
private ImageButton mPictureIb;
private File mImageFile;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPictureIb = (ImageButton) findViewById(R.id.ib_picture);
}
public void onClickPicker(View v) {
new AlertDialog.Builder(this)
.setTitle("選擇照片")
.setItems(new String[]{"拍照", "相冊"}, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if (i == 0) {
selectCamera();
} else {
selectAlbum();
}
}
})
.create()
.show();
}
private void selectCamera() {
createImageFile();
if (!mImageFile.exists()) {
return;
}
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);
}
private void selectAlbum() {
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_UNSPECIFIED);
startActivityForResult(albumIntent, REQUEST_ALBUM);
}
private void cropImage(Uri uri){
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(intent, REQUEST_CROP);
}
private void createImageFile() {
mImageFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".jpg");
try {
mImageFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "出錯啦", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (RESULT_OK != resultCode) {
return;
}
switch (requestCode) {
case REQUEST_CAMERA:
cropImage(Uri.fromFile(mImageFile));
break;
case REQUEST_ALBUM:
createImageFile();
if (!mImageFile.exists()) {
return;
}
Uri uri = data.getData();
if (uri != null) {
cropImage(uri);
}
break;
case REQUEST_CROP:
mPictureIb.setImageURI(Uri.fromFile(mImageFile));
break;
}
}
}複製代碼
效果如圖(不一樣設備,系統功能呈現有所不一樣):程序員
看似完美,你覺得上述代碼就能結束了的話,那就大錯特錯啦!這裏面還有一些兼容問題要處理,還有一些地方須要特殊說明。數組
調用系統相機實現拍照功能的核心代碼以下:緩存
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);複製代碼
其中 MediaStore.EXTRA_OUTPUT
數據表示,拍照所得圖片保存到指定目錄下的文件(通常會在 SD 卡中建立當前應用的目錄,並建立臨時文件保存圖片)。而後,在 onActivityResult 方法中根據文件路徑獲取圖片。服務器
若是不爲 intent 添加該數據的話,將在 onActivityResult 的 intent 對象中返回一個 Bitmap 對象,經過以下代碼獲取:微信
Bitmap bmp = data.getParcelableExtra("data");複製代碼
值得注意的是,這裏的 Bitmap 對象是拍照所得圖片的一個縮略圖,尺寸很小!系統這麼作也是充分考慮到應用的內存佔用問題。試想一下,現在手機設備中高清相機拍出來的照片,一張圖的大小高達十幾兆,若是返回這麼大的圖片,內存佔用至關嚴重,況且不少時候知識臨時使用而已。因此,調用系統相機時,通常都會添加 MediaStore.EXTRA_OUTPUT
參數,避免返回 Bitmap 對象。固然,這麼作也能保證應用產生的數據,包括文件,都能存儲在應用目錄下,方便清理緩存時統一清除。網絡
部分手機,好比三星手機,調用系統相機拍照所得的照片可能會發生自動旋轉問題,常見爲旋轉 90°。因此,要求咱們在拍照以後,使用圖片以前,判斷圖片是否發生過旋轉,若是是,要將照片旋轉回來。app
這是獲取圖片旋轉角度的代碼:框架
/** * 獲取圖片旋轉角度 * @param path 圖片路徑 * @return */
private int parseImageDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}複製代碼
這是根據指定角度旋轉圖片的代碼:
/** * 圖片旋轉操做 * * @param bm 須要旋轉的圖片 * @param degree 旋轉角度 * @return 旋轉後的圖片 */
private Bitmap rotateBitmap(Bitmap bm, int degree) {
Bitmap returnBm = null;
Matrix matrix = new Matrix();
matrix.postRotate(degree);
try {
returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
}
if (returnBm == null) {
returnBm = bm;
}
if (bm != returnBm) {
bm.recycle();
}
return returnBm;
}複製代碼
在部分手機,調用系統拍照功能時,可能會發生橫豎屏切換過程,致使返回應用時當前 Activity 發生銷燬重建,各個生命週期又從新走了一遍。此時,一些應用內的變量數據可能丟失,使用時容易發生空值異常,進而致使 app 崩潰退出。
爲了不這種現象,咱們須要在 AndroidManifest.xml 文件的對應 <activity>
標籤中添加屬性:
android:configChanges="orientation|screenSize"複製代碼
這樣,當發生屏幕旋轉時,不會致使 Activity 銷燬重建,而是執行 onConfigurationChanged()
方法:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}複製代碼
示例中調用系統裁剪的代碼以下:
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(intent, REQUEST_CROP);複製代碼
能夠看出,調用系統裁剪功能,須要設置一些 Extra 參數,不少人容易在這裏產生疑惑,不知如何取捨,如何設值。這裏列舉一下經常使用的 Extra 名字、值類型和做用:
須要注意的是:
第一,設置 return-data 參數爲 true 時,返回的 Bitmap 對象也爲縮略圖,獲取方式與前面所述相機拍照獲取 Bitmap 的方式一致;
第二,調用系統相冊並裁剪時,若是使用MediaStore.EXTRA_OUTPUT參數,Uri 儘可能不要設置爲源文件對應的 Uri 值,另作保存,不損壞系統相冊中的源圖文件;
第三,根據經驗,outputX 與 outputY 值設置太大時,容易出現卡屏現象;
第四,能夠不設置 outputX 與 outputY 參數,使用戶根據自身按比例自由裁剪,就像示例代碼這樣。
你可能會用到 setImageURI()
方法給 ImageView 設置圖片內容,這裏也有一個地方須要注意。咱們先看一下這個方法的源碼:
public void setImageURI(Uri uri) {
if (mResource != 0 ||
(mUri != uri &&
(uri == null || mUri == null || !uri.equals(mUri)))) {
updateDrawable(null);
mResource = 0;
mUri = uri;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}複製代碼
能夠看到,這裏的 uri 參數在內部持有緩存變量,當屢次調用該方法而 uri 參數值不變時,圖片展現內容不變。問題就在這,若是你屢次拍照或裁剪保存的圖片文件路徑相同時,雖然每次處理事後實際存儲的文件內容發生變化,但因爲路徑相同,uri 參數一致,致使屢次調用 setImageURI()
設置圖片內容時,ImageView 顯示內容不變!這也是爲何示例代碼中我用時間戳處理圖片文件名的緣由所在,保證每次存儲的圖片路徑不一樣。
有時候,咱們須要根據 Uri 獲取文件路徑。好比若是你不須要使用裁剪功能的話,調用系統相冊選擇圖片後返回的就是一個 Uri 對象,咱們須要從這個 Uri 對象中解析出對應的圖片文件路徑,便於上傳至服務器等後續處理。
好比,這個 Uri 對象多是:
content://media/external/images/media/3066
不少朋友相信有過這樣的經驗,使用 toString() 或者 getPath() 方法獲取 Uri 對象所對應的文件路徑,其實這是錯誤的!經過 getPath() 獲取的結果字符串是:
media/external/images/media/3066
而正確的獲取方式是:
private String parseFilePath(Uri uri) {
String[] filePathColumn = { MediaStore.Images.Media.DATA };
Cursor cursor = getContentResolver().query(uri, filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String picturePath = cursor.getString(columnIndex);
cursor.close();
return picturePath;
}複製代碼
其對應的文件路徑應該是這個樣子的:
/storage/emulated/0/Pictures/Screenshots/S70302-131606.jpg
如今不少網絡框架內部都作了封裝處理,上傳圖片時只須要傳遞一個文件路徑便可。可是,少數狀況下,根據服務器須要,咱們要對圖片文件字節流編碼後再上傳。這是使用 Base64 編碼並根據字節數組獲取字符串的處理過程:
public static String fileToBase64String(String filePath) {
File photoFile = new File(filePath);
try {
FileInputStream fis = new FileInputStream(photoFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream(10000);
byte[] buffer = new byte[1000];
while (fis.read(buffer)!=-1) {
baos.write(buffer);
}
baos.close();
fis.close();
return Arrays.toString(Base64.encode(baos.toByteArray(), Base64.DEFAULT));
}catch (IOException e) {
e.printStackTrace();
}
return null;
}複製代碼
當上傳多張圖片至服務器時,爲了提高傳輸效率,每每會採用 zip 格式壓縮處理。這裏提供一個遞歸壓縮代碼,方便你們有須要的時候借鑑參考:
public String zipCompass(String filePath){
File zipFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".zip");
try{
//指定了兩個待壓縮的文件,都在assets目錄中
String[] filenames = new String[]{ "activity_main.xml", "strings.xml" };
FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos);
int i = 1;
//枚舉filenames中的全部待壓縮文件
while (i <= filenames.length){
//從filenames數組中取出當前待壓縮的文件名,做爲壓縮後的名稱,以保證壓縮先後文件名一致
ZipEntry zipEntry = new ZipEntry(filenames[i - 1]);
//打開當前的zipEntry對象
zos.putNextEntry(zipEntry);
FileInputStream is = new FileInputStream(filePath);
byte[] buffer = new byte[8192];
int count = 0;
//寫入數據
while ((count = is.read(buffer)) >= 0){
zos.write(buffer, 0, count);
}
zos.flush();
zos.closeEntry();
is.close();
i++;
}
zos.finish();
zos.close();
return zipFile.getAbsolutePath();
}
catch (Exception e){
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
return null;
}
}複製代碼
說了這麼多,別忘了在 AndroidManifest.xml 文件中添加系統權限(前面示例代碼中沒有考慮到 Android 6.0 運行時權限的問題,實際使用時注意添加處理):
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />複製代碼
安卓筆記俠:專一於 Android 開發,和程序員的感悟~