一、概述
相信大家都用過 Android 應用中更換頭像的功能,在這個功能中,用戶可以拍照或者選擇相冊圖片,然后裁剪出頭像所需要的圖案。
那么你們有沒有考慮過這個功能怎么實現(xiàn)的呢?今天就讓我們一步步搞定這個功能,先看運行效果,這里選擇了相冊圖片并設置頭像。

二、PopUpWindow 設計及彈出效果
1. 布局
優(yōu)雅簡潔的用戶界面是吸引用戶的開端,那先讓我們設計一個漂亮的 PopUpWindow,如下所示:

這個 PopUpWindow 里共有3個按鈕,分別為“拍照”,“從相冊選擇”,以及“取消”。上面兩個按鈕連接在了一起,下方的“取消”與它們分開,那么這里就需要3種按鈕樣式:“拍照”按鈕只有上方是圓角,“從相冊選擇”按鈕只有下方是圓角,“取消”按鈕四個角都是圓角。
在 drawable 文件夾中新建下方3個 shape 文件。
1. white_btn
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white"/>
<corners android:radius="10dp"/>
<stroke android:width="0dp" android:color="@android:color/white" />
</shape>
2. white_btn_top
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners android:topLeftRadius="10dp"
android:topRightRadius="10dp"
android:bottomRightRadius="0dp"
android:bottomLeftRadius="0dp"/>
<stroke android:width="0dp" android:color="@android:color/white" />
</shape>
3. white_btn_bottom
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners android:topLeftRadius="0dp"
android:topRightRadius="0dp"
android:bottomRightRadius="10dp"
android:bottomLeftRadius="10dp"/>
<stroke android:width="0dp" android:color="@android:color/white" />
</shape>
有了這3個按鈕樣式,就可以寫出 PopUpWindow 的布局了。
在 colors.xml 中添加字體顏色 <color name="colorMainGreen">#40cab3</color>
在 layout 中新建 pop_item.xml 布局,因為 PopUpWindow 彈出時,屏幕的背景會變灰,因此需要將布局的背景顏色設置為半透明灰色,顏色代碼 #66000000
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#66000000">
<LinearLayout
android:id="@+id/ll_pop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:orientation="vertical"
android:layout_alignParentBottom="true">
<Button
android:id="@+id/icon_btn_camera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/white_btn_top"
android:textColor="@color/colorMainGreen"
android:text="拍照"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/background_gray"/>
<Button
android:id="@+id/icon_btn_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/white_btn_bottom"
android:textColor="@color/colorMainGreen"
android:text="從相冊選擇"/>
<Button
android:id="@+id/icon_btn_cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:background="@drawable/white_btn"
android:textColor="@color/colorMainGreen"
android:text="取消"/>
</LinearLayout>
</RelativeLayout>
2. 動畫效果
這里 PopUpWindow 的出現(xiàn)和消失使用淡入淡出的動畫效果。
在 res 中新建 anim 文件夾,在其中新建兩個動畫效果。
popup_show.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
</set>
popup_gone.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="200"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
</set>
在 styles.xml 中設置 PopUpWindow 整體的動畫效果。
<style name="popwindow_anim_style">
<item name="android:windowEnterAnimation">@anim/popup_show</item>
<item name="android:windowExitAnimation">@anim/popup_gone</item>
</style>
3. PopupWindow 類
有了布局和動畫效果,接下來就可以寫 PopUpWindow 的工具類了,這個工具類負責接收外部的點擊監(jiān)聽器,并設置點擊彈窗外關(guān)閉彈窗。
新建 PhotoPopupWindow 類,它的構(gòu)造函數(shù)需要傳入 “拍照” 和 “相冊” 兩個按鈕的點擊監(jiān)聽,具體代碼如下:
public class PhotoPopupWindow extends PopupWindow {
private View mView; // PopupWindow 菜單布局
private Context mContext; // 上下文參數(shù)
private View.OnClickListener mSelectListener; // 相冊選取的點擊監(jiān)聽器
private View.OnClickListener mCaptureListener; // 拍照的點擊監(jiān)聽器
public PhotoPopupWindow(Activity context, View.OnClickListener selectListener, View.OnClickListener captureListener) {
super(context);
this.mContext = context;
this.mSelectListener = selectListener;
this.mCaptureListener = captureListener;
Init();
}
/**
* 設置布局以及點擊事件
*/
private void Init() {
LayoutInflater inflater = (LayoutInflater) mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = inflater.inflate(R.layout.pop_item, null);
Button btn_camera = (Button) mView.findViewById(R.id.icon_btn_camera);
Button btn_select = (Button) mView.findViewById(R.id.icon_btn_select);
Button btn_cancel = (Button) mView.findViewById(R.id.icon_btn_cancel);
btn_select.setOnClickListener(mSelectListener);
btn_camera.setOnClickListener(mCaptureListener);
btn_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
// 導入布局
this.setContentView(mView);
// 設置動畫效果
this.setAnimationStyle(R.style.popwindow_anim_style);
this.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
this.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
// 設置可觸
this.setFocusable(true);
ColorDrawable dw = new ColorDrawable(0x0000000);
this.setBackgroundDrawable(dw);
// 單擊彈出窗以外處 關(guān)閉彈出窗
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int height = mView.findViewById(R.id.ll_pop).getTop();
int y = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_UP) {
if (y < height) {
dismiss();
}
}
return true;
}
});
}
}
三、在 MainActivity 中設置頭像
1. activity_main 布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.chen.lister.testchangeicon.MainActivity">
<LinearLayout
android:id="@+id/main_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/main_icon"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="20dp"
android:src="@mipmap/ic_launcher"/>
<Button
android:id="@+id/main_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="更換頭像"/>
</LinearLayout>
</LinearLayout>
2. 彈出 PopUpWindow
回顧之前的 PopUpWindow 工具類,它的構(gòu)造方法需要上下文以及兩個點擊事件的監(jiān)聽器,新建 PopUpWindow 之后就可以讓它顯示在屏幕下方中間。
main_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPhotoPopupWindow = new PhotoPopupWindow(MainActivity.this, new View.OnClickListener() {
@Override
public void onClick(View v) {
// 進入相冊選擇
}
}, new View.OnClickListener() {
@Override
public void onClick(View v) {
// 拍照
}
});
View rootView = LayoutInflater.from(MainActivity.this).inflate(R.layout.activity_main, null);
mPhotoPopupWindow.showAtLocation(rootView,
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
}
});
3. 拍照或選擇圖片并切割
在 MainActivity 中添加如下常量:
private static final int REQUEST_IMAGE_GET = 0;
private static final int REQUEST_IMAGE_CAPTURE = 1;
private static final int REQUEST_SMALL_IMAGE_CUTTING = 2;
private static final int REQUEST_BIG_IMAGE_CUTTING = 3;
private static final String IMAGE_FILE_NAME = "icon.jpg";
先看在相冊中選擇圖片,點擊進入相冊選擇圖片的按鈕后,系統(tǒng)應該使用 startActivityForResult() 調(diào)用選擇圖片的 intent 并返回一個結(jié)果。
mPhotoPopupWindow.dismiss();
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 判斷系統(tǒng)中是否有處理該 Intent 的 Activity
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_IMAGE_GET);
} else {
Toast.makeText(MainActivity.this, "未找到圖片查看器", Toast.LENGTH_SHORT).show();
}
返回的結(jié)果在 onActivityResult() 中處理,先通過 data.getData() 獲取選擇到的圖片的 Uri,再通過 startSmallPhotoZoom() 對該圖片進行裁剪。
/**
* 處理回調(diào)結(jié)果
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 回調(diào)成功
if (resultCode == RESULT_OK) {
switch (requestCode) {
// 小圖切割
case REQUEST_SMALL_IMAGE_CUTTING:
if (data != null) {
setPicToView(data);
}
break;
// 相冊選取
case REQUEST_IMAGE_GET:
try {
startSmallPhotoZoom(data.getData());
} catch (NullPointerException e) {
e.printStackTrace();
}
break;
//......
}
}
}
startSmallPhotoZoom() 方法如下,它會啟動系統(tǒng)的裁剪界面進行裁剪并返回結(jié)果。它啟動的 intent 中 "return-data" 為 true,意味著它裁剪完圖片會直接將圖片作為 bitmap 在內(nèi)存中返回。
如果你夠細心,你就會發(fā)現(xiàn)它返回的結(jié)果也在上面 onActivityResult() 中調(diào)用 setPicToView() 方法處理了。
/**
* 小圖模式切割圖片
* 此方式直接返回截圖后的 bitmap,由于內(nèi)存的限制,返回的圖片會比較小
*/
public void startSmallPhotoZoom(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1); // 裁剪框比例
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 300); // 輸出圖片大小
intent.putExtra("outputY", 300);
intent.putExtra("scale", true);
intent.putExtra("return-data", true);
startActivityForResult(intent, REQUEST_SMALL_IMAGE_CUTTING);
}
setPicToView() 方法如下,它將裁剪后的圖片保存到指定文件夾并設置到 ImageView 中。
/**
* 小圖模式中,保存圖片后,設置到視圖中
*/
private void setPicToView(Intent data) {
Bundle extras = data.getExtras();
if (extras != null) {
Bitmap photo = extras.getParcelable("data"); // 直接獲得內(nèi)存中保存的 bitmap
// 創(chuàng)建 smallIcon 文件夾
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String storage = Environment.getExternalStorageDirectory().getPath();
File dirFile = new File(storage + "/smallIcon");
if (!dirFile.exists()) {
if (!dirFile.mkdirs()) {
Log.e("TAG", "文件夾創(chuàng)建失敗");
} else {
Log.e("TAG", "文件夾創(chuàng)建成功");
}
}
File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
// 保存圖片
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
photo.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 在視圖中顯示圖片
main_icon.setImageBitmap(photo);
}
}
解決了相冊選圖,再來看拍照。
點擊拍照的按鈕,即調(diào)用系統(tǒng)的拍照功能。
mPhotoPopupWindow.dismiss();
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME)));
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
修改 onActivityResult() 函數(shù),增加拍照返回的處理,最后同樣調(diào)用 startSmallPhotoZoom() 函數(shù)進行裁剪。
/**
* 處理回調(diào)結(jié)果
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 回調(diào)成功
if (resultCode == RESULT_OK) {
switch (requestCode) {
// 小圖切割
case REQUEST_SMALL_IMAGE_CUTTING:
if (data != null) {
setPicToView(data);
}
break;
// 相冊選取
case REQUEST_IMAGE_GET:
try {
startSmallPhotoZoom(data.getData());
} catch (NullPointerException e) {
e.printStackTrace();
}
break;
// 拍照
case REQUEST_IMAGE_CAPTURE:
File temp = new File(Environment.getExternalStorageDirectory() + "/" + IMAGE_FILE_NAME);
startSmallPhotoZoom(Uri.fromFile(temp));
break;
}
}
}
四、大圖片裁剪
設置完頭像,再看之前保存的圖片,你會發(fā)現(xiàn)它們都很模糊,那如果想裁剪出清晰的圖片,該怎么做呢?
還記得裁剪圖片 Intent 中的這兩個參數(shù)嗎,它們就代表了輸出圖片的大小。
intent.putExtra("outputX", 300); // 輸出圖片大小
intent.putExtra("outputY", 300);
那么想提高圖片的質(zhì)量,是不是把這兩個值加大就可以了呢?
在回答這個問題之前,讓我們先來了解一下裁剪后的圖片是怎么返回的。
假設現(xiàn)在有一張圖片尺寸為 3200*2400px。也許你覺得返回這張圖沒什么問題,大不了耗1-2M的內(nèi)存。不錯,這個尺寸的圖片確實只有1.8M左右的大小。但是你想不到的是,這個尺寸對應的 Bitmap 會耗光你應用程序的所有內(nèi)存。Android出于安全性考慮,只會給你一個寒磣的縮略圖。
Android 中,默認 Bitmap 為 32 位,也就是說,一個像素點占用 4 個字節(jié),那么之前我們說的圖片需要占用多大的內(nèi)存呢?3200*2400*4 bytes = 30M。
整整30M!即使你想為一張只會存在幾秒鐘的圖片消耗這么大的內(nèi)存,Android 也不會答應的。
所以如果我們想提高裁剪圖片的質(zhì)量,可不是只加大輸出的圖片像素大小就可以的。那我們還應該做什么呢?先來看看裁剪圖片的 Intent 可附帶的參數(shù),看看它們?yōu)槲覀兲峁┝耸裁葱畔ⅰ?/p>
| 附帶參數(shù) | 數(shù)據(jù)類型 | 描述 |
|---|---|---|
| crop | String | 發(fā)送裁剪信號 |
| aspectX | int | X方向上的比例 |
| aspectY | int | Y方向上的比例 |
| outputX | int | 裁剪區(qū)的寬 |
| outputY | int | 裁剪區(qū)的高 |
| scale | boolean | 是否保留比例 |
| return-data | boolean | 是否將數(shù)據(jù)保留在Bitmap中返回 |
| data | Parcelable | 相應的Bitmap數(shù)據(jù) |
| circleCrop | String | 圓形裁剪區(qū)域? |
| MediaStore.EXTRA_OUTPUT ("output") | Uri | 將URI指向相應的file:///... |
在小圖返回模式中,我們將 return-data 設置為了“true”,因此會在內(nèi)存中直接返回一個 Bitmap,由于內(nèi)存的原因,它將會是一個模糊的縮略圖。
如果將 return-data 設置為“false”,那么在 onActivityResult() 的 Intent 數(shù)據(jù)中你將不會接收到任何 Bitmap,相反,我們需要將 MediaStore.EXTRA_OUTPUT 關(guān)聯(lián)到一個 Uri,此 Uri 是用來存放 Bitmap 的,那么裁剪后的圖片就會保存到 sd 卡中。
具體代碼如下:
/**
* 大圖模式切割圖片
* 直接創(chuàng)建一個文件將切割后的圖片寫入
*/
public void startBigPhotoZoom(Uri uri) {
// 創(chuàng)建大圖文件夾
Uri imageUri = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String storage = Environment.getExternalStorageDirectory().getPath();
File dirFile = new File(storage + "/bigIcon");
if (!dirFile.exists()) {
if (!dirFile.mkdirs()) {
Log.e("TAG", "文件夾創(chuàng)建失敗");
} else {
Log.e("TAG", "文件夾創(chuàng)建成功");
}
}
File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
imageUri = Uri.fromFile(file);
}
// 開始切割
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1); // 裁剪框比例
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 600); // 輸出圖片大小
intent.putExtra("outputY", 600);
intent.putExtra("scale", true);
intent.putExtra("return-data", false); // 不直接返回數(shù)據(jù)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一個文件
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
}
之后根據(jù)需求將圖片設置到 ImageView 中或者上傳服務器即可,這里不再贅述。
五、Android 新版本適配
上面的程序在 Android5.X 及以下可以正常運行,但是在 Android6.0 和 Android7.0 下運行時會崩潰。這是因為 Android6.0 需要程序動態(tài)申請權(quán)限,而 Android7.0 對 Uri 添加了保護。
1. 動態(tài)權(quán)限
在 Android6.0 之后,拍照和讀取本地文件都需要在運行時動態(tài)申請權(quán)限。
申請之前需要檢查用戶之前是否已經(jīng)同意該權(quán)限。如果已經(jīng)同意,則直接進行下一步操作。如果沒有,則進行申請,成功后在回調(diào)方法 onRequestPermissionsResult() 中進行后續(xù)處理。
修改頭像按鈕的點擊事件
main_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPhotoPopupWindow = new PhotoPopupWindow(MainActivity.this, new View.OnClickListener() {
@Override
public void onClick(View v) {
// 拍照及文件權(quán)限申請
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 權(quán)限還沒有授予,進行申請
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申請的 requestCode 為 300
} else {
// 權(quán)限已經(jīng)申請,直接拍照
mPhotoPopupWindow.dismiss();
imageCapture();
}
}
}, new View.OnClickListener() {
@Override
public void onClick(View v) {
// 文件權(quán)限申請
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 權(quán)限還沒有授予,進行申請
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200); // 申請的 requestCode 為 200
} else {
// 如果權(quán)限已經(jīng)申請過,直接進行圖片選擇
mPhotoPopupWindow.dismiss();
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 判斷系統(tǒng)中是否有處理該 Intent 的 Activity
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_IMAGE_GET);
} else {
Toast.makeText(MainActivity.this, "未找到圖片查看器", Toast.LENGTH_SHORT).show();
}
}
}
});
View rootView = LayoutInflater.from(MainActivity.this)
.inflate(R.layout.activity_main, null);
mPhotoPopupWindow.showAtLocation(rootView,
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
}
});
權(quán)限申請的回調(diào)
/**
* 處理權(quán)限回調(diào)結(jié)果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 200:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
mPhotoPopupWindow.dismiss();
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 判斷系統(tǒng)中是否有處理該 Intent 的 Activity
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_IMAGE_GET);
} else {
Toast.makeText(MainActivity.this, "未找到圖片查看器", Toast.LENGTH_SHORT).show();
}
} else {
mPhotoPopupWindow.dismiss();
}
break;
case 300:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
mPhotoPopupWindow.dismiss();
imageCapture();
} else {
mPhotoPopupWindow.dismiss();
}
break;
}
}
2. Uri 保護
Android7.0 的官方文檔是這么說的:
Passing file://URIs outside the package domain may leave the receiver with an unaccessible path. Therefore, attempts to pass a file:// URI trigger a FileUriExposedException. The recommended way to share the content of a private file is using the FileProvider.
什么意思呢?就是說,file:// 這樣的 Uri 不能附著在 Intent 上,否則會引發(fā) FileUriExposedException,官方建議使用 FileProvider 改變 Uri 的傳遞方式。
在這個應用中,我們在調(diào)用相機并把拍攝的照片保存到手機本地時,如果傳入 file:// 這樣的 Uri 就會造成應用崩潰,因此需要使用 FileProvider,步驟如下。
注:選擇圖片不會崩潰,因為選擇圖片后傳入的 Uri 本身就是 Content Uri。
1. 在 res 下新建 xml 文件夾,其中新建 provider_paths.xml,代碼如下
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 注意包名 -->
<external-path path="Android/data/com.chen.lister.testchangeicon/" name="files_root" />
</paths>
2. 在 manifest 中進行聲明
<!-- 注意包名 -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.chen.lister.testchangeicon.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
3. 將拍照行為封裝成 imageCapture() 方法
/**
* 判斷系統(tǒng)及拍照
*/
private void imageCapture() {
Intent intent;
Uri pictureUri;
File pictureFile = new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME);
// 判斷當前系統(tǒng)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
pictureUri = FileProvider.getUriForFile(this,
"com.chen.lister.testchangeicon.fileProvider", pictureFile);
} else {
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
pictureUri = Uri.fromFile(pictureFile);
}
// 去拍照
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
4. 圖片裁剪
因為裁剪圖片時也會用到 Intent,所以也要對 Uri 進行處理,我們也可以使用上面的方法進行處理。具體如下:
// 開始切割
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(FileProvider.getUriForFile(this,
"com.chen.lister.testchangeicon.fileProvider", file), "image/*");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// ......
intent.putExtra("return-data", false); // 不直接返回數(shù)據(jù)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一個文件
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
這樣程序就可以在 Android7.0 下正常運行。
2020.05.06補充:
當前程序在Android10下讀取文件再decode為Bitmap時為null,報錯為open failed: EACCES (Permission denied)。這是因為Android10下新增分區(qū)儲存功能,在外部存儲設備中為每個應用提供了一個“隔離存儲沙盒”,我們選擇將其停用,在AndroidManifest的application節(jié)點中添加android:requestLegacyExternalStorage="true"即可。