Android 拍照及相簿選取圖片功能,已適配Android6.0、7.0、8.0
更換頭像或者上傳圖片功能已基本是每個 APP 所具備的基礎功能了,但這對於開發者來說是一個很麻煩的事情,除機型之外,適配版本就至少要考慮這幾種情況(6.0以下版本、6.0的動態許可權、7.0的FileProvider、8.0的特殊情況)。
今天來個總結,方便自己也方便同行。
功能說明
本文的示例以下圖為準:
介面只有一個 ImageView,點選 ImageView 彈出 DialogFragment,分別是拍照和相簿選擇圖片功能,其中都帶有系統的裁剪功能,將裁剪後的圖片顯示在 ImageView 上。如果不需要裁剪功能,只需要將程式碼中的裁剪方法註釋掉即可。
6.0 以下版本
1. 許可權
<uses-feature android:name="android.hardware.camera" />
<!--相機許可權-->
<uses-permission android:name="android.permission.CAMERA" />
<!--寫入SD卡的許可權:如果你希望儲存相機拍照後的照片-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--讀取SD卡的許可權:開啟相簿選取圖片所必須的許可權-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
2. 拍照程式碼
/**
* 開啟系統相機
*/
private void openSysCamera() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
new File(Environment.getExternalStorageDirectory(), imgName)));
startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
}
其中 imgName 是拍照圖片的名字,一般以時間戳再加上自定義字串命名;CAMERA_RESULT_CODE 是自定義的一個常量,作為拍照的請求碼。
onActivityResult 處理拍照後的圖片
case CAMERA_RESULT_CODE:
tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
cropPic(Uri.fromFile(tempFile));
break;
將拍照後的圖片建立成一個 File 物件,用來裁剪,裁剪的功能程式碼是和相簿選取圖片通用的。
裁剪程式碼
/**
* 裁剪圖片
*
* @param data
*/
private void cropPic(Uri data) {
if (data == null) {
return;
}
Intent cropIntent = new Intent("com.android.camera.action.CROP");
cropIntent.setDataAndType(data, "image/*");
// 開啟裁剪:開啟的Intent所顯示的View可裁剪
cropIntent.putExtra("crop", "true");
// 裁剪寬高比
cropIntent.putExtra("aspectX", 1);
cropIntent.putExtra("aspectY", 1);
// 裁剪輸出大小
cropIntent.putExtra("outputX", 320);
cropIntent.putExtra("outputY", 320);
cropIntent.putExtra("scale", true);
/**
* return-data
* 這個屬性決定我們在 onActivityResult 中接收到的是什麼資料,
* 如果設定為true 那麼data將會返回一個bitmap
* 如果設定為false,則會將圖片儲存到本地並將對應的uri返回,當然這個uri得有我們自己設定。
* 系統裁剪完成後將會將裁剪完成的圖片儲存在我們所這設定這個uri地址上。我們只需要在裁剪完成後直接呼叫該uri來設定圖片,就可以了。
*/
cropIntent.putExtra("return-data", true);
// 當 return-data 為 false 的時候需要設定這句
// cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// 圖片輸出格式
// cropIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
// 頭像識別 會啟動系統的拍照時人臉識別
// cropIntent.putExtra("noFaceDetection", true);
startActivityForResult(cropIntent, CROP_RESULT_CODE);
}
其中的 CROP_RESULT_CODE 也是定義的靜態常量,同拍照時定義的常量相同的作用。
如果裁剪時將 return-data 設定為 false,那麼需要定義一個 Uri 來儲存裁剪後的圖片路徑。
// 裁剪屬性 cropIntent.putExtra("return-data", false); 時,使用自定義接收圖片的Uri
private static final String IMAGE_FILE_LOCATION = "file:///" + Environment.getExternalStorageDirectory().getPath() + "/temp.jpg";
private Uri imageUri = Uri.parse(IMAGE_FILE_LOCATION);
圖片裁剪完成回撥
case CROP_RESULT_CODE:
// 裁剪時,這樣設定 cropIntent.putExtra("return-data", true); 處理方案如下
if (data != null) {
Bundle bundle = data.getExtras();
if (bundle != null) {
Bitmap bitmap = bundle.getParcelable("data");
imageView.setImageBitmap(bitmap);
// 把裁剪後的圖片儲存至本地 返回路徑
String urlpath = FileUtilcll.saveFile(this, "crop.jpg", bitmap);
L.e("裁剪圖片地址->" + urlpath);
}
}
// 裁剪時,這樣設定 cropIntent.putExtra("return-data", false); 處理方案如下
// try {
// ivHead.setImageBitmap(BitmapFactory.decodeStream(
// getActivity().getContentResolver().openInputStream(imageUri)));
// } catch (FileNotFoundException e) {
// e.printStackTrace();
// }
break;
FileUtilcll 程式碼
/**
* 圖片檔案操作
*/
public class FileUtilcll {
/**
* 將Bitmap 圖片儲存到本地路徑,並返回路徑
*
* @param fileName 檔名稱
* @param bitmap 圖片
* @param資源型別,參照 MultimediaContentType 列舉,根據此型別,儲存時可自動歸類
*/
public static String saveFile(Context c, String fileName, Bitmap bitmap) {
return saveFile(c, "", fileName, bitmap);
}
public static String saveFile(Context c, String filePath, String fileName, Bitmap bitmap) {
byte[] bytes = bitmapToBytes(bitmap);
return saveFile(c, filePath, fileName, bytes);
}
/**
* Bitmap 轉 位元組陣列
*
* @param bm
* @return
*/
public static byte[] bitmapToBytes(Bitmap bm) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(CompressFormat.JPEG, 100, baos);
return baos.toByteArray();
}
public static String saveFile(Context c, String filePath, String fileName, byte[] bytes) {
String fileFullName = "";
FileOutputStream fos = null;
String dateFolder = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA)
.format(new Date());
try {
String suffix = "";
if (filePath == null || filePath.trim().length() == 0) {
filePath = Environment.getExternalStorageDirectory() + "/cxs/" + dateFolder + "/";
}
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
File fullFile = new File(filePath, fileName + suffix);
fileFullName = fullFile.getPath();
fos = new FileOutputStream(new File(filePath, fileName + suffix));
fos.write(bytes);
} catch (Exception e) {
fileFullName = "";
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
fileFullName = "";
}
}
}
return fileFullName;
}
}
到此,拍照功能就完成了,附帶了裁剪。剩下的就是相簿選取照片,這個也不難,固定程式碼,其他功能和拍照時相同的。
/**
* 開啟系統相簿
*/
private void openSysAlbum() {
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(albumIntent, ALBUM_RESULT_CODE);
}
其中的 ALBUM_RESULT_CODE 同樣是定義的常量。用來在 onActivityResult 中處理返回結果。
case ALBUM_RESULT_CODE:
// 相簿
cropPic(data.getData());
break;
和拍照公用裁剪程式碼,這裡直接呼叫即可,其他邏輯不變。
6.0動態許可權適配
除了上述功能保持不變之外,需要在程式碼中動態申請檔案讀寫許可權和相機許可權。這裡我將申請的時機寫在了剛進入頁面,而不是剛開啟 APP,也不是點選按鈕的時候。
邏輯
- 如果使用者點選了拒絕,但沒有點選“不再詢問”,這個時候再次進入介面繼續彈框;
- 如果使用者點選了拒絕,且選擇了“不再詢問”,那麼再次進入此介面將會彈框提示開啟 APP 的詳情介面,手動開啟對應許可權。
許可權申請程式碼
/**
* 初始化相機相關許可權
* 適配6.0+手機的執行時許可權
*/
private void initPermission() {
String[] permissions = new String[]{Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE};
//檢查許可權
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 之前拒絕了許可權,但沒有點選 不再詢問 這個時候讓它繼續請求許可權
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
Toast.makeText(this, "使用者曾拒絕開啟相機許可權", Toast.LENGTH_SHORT).show();
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
} else {
//註冊相機許可權
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
}
}
}
這裡我將需要的許可權統一寫在了一個陣列裡面。其中的 REQUEST_PERMISSIONS 和上面拍照定義的常量功能相同。
許可權申請回撥
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSIONS:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//成功
Toast.makeText(this, "使用者授權相機許可權", Toast.LENGTH_SHORT).show();
} else {
// 勾選了不再詢問
Toast.makeText(this, "使用者拒絕相機許可權", Toast.LENGTH_SHORT).show();
/**
* 跳轉到 APP 詳情的許可權設定頁
*
* 可根據自己的需求定製對話方塊,點選某個按鈕在執行下面的程式碼
*/
Intent intent = Util.getAppDetailSettingIntent(PhotoFromSysActivity.this);
startActivity(intent);
}
break;
}
}
其中的 getAppDetailSettingIntent()方法程式碼如下:
/**
* 獲取 APP 詳情頁面intent
*
* @return
*/
public static Intent getAppDetailSettingIntent(Context context) {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 9) {
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
} else if (Build.VERSION.SDK_INT <= 8) {
localIntent.setAction(Intent.ACTION_VIEW);
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
}
return localIntent;
}
到這,許可權這個適配問題就解決了,接下來該是 Android 7.0 的適配了,這裡我們只需要修改拍照的功能即可,相簿是沒有問題的。
Android7.0 適配
Android 7.0 就是 File 路徑的變更,需要使用 FileProvider 來做,下面看拍照的程式碼。
拍照程式碼修改
/**
* 開啟系統相機
*/
private void openSysCamera() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
// new File(Environment.getExternalStorageDirectory(), imgName)));
// File file = new File(Environment.getExternalStorageDirectory(), imgName);
try {
file = createOriImageFile();
} catch (IOException e) {
e.printStackTrace();
}
if (file != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
imgUriOri = Uri.fromFile(file);
} else {
imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", file);
}
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriOri);
startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
}
}
File 物件的建立和 拍照圖片的 Uri 物件建立方式更改。建立原影像儲存的程式碼如下:
/**
* 建立原影像儲存的檔案
*
* @return
* @throws IOException
*/
private File createOriImageFile() throws IOException {
String imgNameOri = "HomePic_" + new SimpleDateFormat(
"yyyyMMdd_HHmmss").format(new Date());
File pictureDirOri = new File(getExternalFilesDir(
Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/OriPicture");
if (!pictureDirOri.exists()) {
pictureDirOri.mkdirs();
}
File image = File.createTempFile(
imgNameOri, /* prefix */
".jpg", /* suffix */
pictureDirOri /* directory */
);
imgPathOri = image.getAbsolutePath();
return image;
}
拍照回撥程式碼修改
case CAMERA_RESULT_CODE:
// tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
// cropPic(Uri.fromFile(tempFile));
// 適配 Android7.0+
cropPic(getImageContentUri(file));
break;
getImageContentUri() 程式碼如下:
/**
* 7.0以上獲取裁剪 Uri
*
* @param imageFile
* @return
*/
private Uri getImageContentUri(File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID},
MediaStore.Images.Media.DATA + "=? ",
new String[]{filePath}, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor
.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (imageFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
return getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
程式碼適配解決完了,還有一個 FileProvider 問題需要做如下配置。
- 在 res 目錄下建立一個名為 xml 的資料夾,並在其下建立一個名為 file_paths.xml 檔案,其內容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="images"
path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
<external-path
name="images"
path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
<external-files-path
name="images"
path="files/Pictures/OriPicture" />
<root-path
name="images"
path="" />
<root-path
name="images"
path="" />
</paths>
2. 在 AndroidMainfest.xml 中的 application 節點下做如下配置:
<!--FileProvider共享檔案、快取-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.cxs.yukumenu.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
其中這裡的 android:authorities 屬性值要和程式碼中 FileProvider.getUriForFile()的第二個引數值保持一致。
注意
- 程式碼已全部給出,貼上即可使用
- 本次測試真機:華為5.1.1,一加8.0.0
總結
- 機型適配是個細活,但能鍛鍊排查問題和解決問題能力。
- 本文的程式碼有很多是可以抽取的,如果是在專案中使用,那麼建議提取出特定功能的程式碼。
相關文章
- Android拍照,相簿選擇圖片以及Android6.0許可權管理Android
- 極其方便的系統原生圖片選取庫,已適配Android 7.0Android
- android實現拍照、相簿選圖、裁剪功能,相容7.0以及小米Android
- Android app 線上更新那點事兒(適配Android6.0、7.0、8.0)AndroidAPP
- Android 6.0、7.0、8.0、9.0適配Android
- Android本地圖片上傳(拍照+相簿)Android地圖
- Android6.0~9.0適配Android
- Android上傳圖片之呼叫系統拍照和從相簿選擇圖片Android
- 直播平臺搭建,Android手機拍照和手機相簿選取圖片的工具Android
- 藉助系統自帶圖片裁剪整合圖片選擇以及7.0適配
- android 圖片適配Android
- Swift相簿選擇和拍照功能Swift
- 仿微信圖片選取、相機拍照—PhotoPicker(已整合GalleryView)View
- Android 從本地選取圖片或者拍照填充ImageViewAndroidView
- Android 圓形頭像 相簿和拍照裁剪選取Android
- Android 呼叫系統相機拍照 . 選取本地相簿Android
- Android7.0的適配Android
- Android6.0許可權適配及相容庫的實現Android
- 專案需求討論 - WebView下拍照及圖片選擇功能WebView
- Android 拍照、選擇圖片並裁剪Android
- Android7.0機型適配Android
- Android6.0許可權的動態適配Android
- Android中呼叫攝像頭拍照儲存,並在相簿中選擇圖片顯示Android
- 「Android6.0許可權適配| 掘金技術徵文 」Android
- 微信小程式開發之從相簿獲取圖片 使用相機拍照 本地圖片上傳微信小程式地圖
- Android 呼叫相簿 拍照 實現系統控制元件縮放 切割圖片Android控制元件
- Android將圖片儲存到相簿並及時看到Android
- 擁抱SVG:苦惱於圖片適配 in Android?SVGAndroid
- Android呼叫相簿、相機(相容6.0、7.0、8.0)所需新增的許可權Android
- Android圖示適配Android
- 相簿圖片多選、反選 swift實現Swift
- Android生成圖片並放入相簿Android
- Android拍照與相機適配問題彙總Android
- 安卓6.0以上從相簿選擇圖片,圖片壓縮及動態許可權安卓
- Flutter圖片解析度適配Flutter
- Android 8.0 自適應圖示Android
- Android7.0行為變更:適配File ProviderAndroidIDE
- 適配Android4.4~Android11,呼叫系統相機,系統相簿,系統圖片裁剪,轉換檔案(對圖片進行上傳等操作)Android