更换头像是个对一群人毫无存在感的功能,但是对另外一群人炒鸡常用的功能。
6.0相机需要申请动态权限,7.0认为直接使用本地真实路径Uri不安全会抛出异常。
针对这两个比较大的改变,在下站在了一堆巨人的肩膀上蹦了几下,由其是郭神。
文中几处关键代码出自其出版的《第一行代码》第二版的8.3章节。
本文结构
- 最终效果展示
- 关键代码说明
- 主要代码逻辑
- github项目地址
- 补充
最终效果展示
备注:MIUI系统优化了剪切功能,允许放大缩小,即使设置了固定的目标值。
关键代码说明
相机相关
//尝试打开相机 无权限则申请权限
private void tryOpenCamera() {
//判断sdk大于6.0则先请求动态访问相机的权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if ((ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.READ_CONTACTS) !=
PackageManager.PERMISSION_GRANTED)) {
//进入到这里代表没有权限,下面申请权限
//用户点击同意或拒绝后会进入onRequestPermissionsResult
ProfilePicFragment.this.requestPermissions(
new String[]{Manifest.permission.CAMERA},
REQUEST_PERMISSION_CAMERA_CODE);
} else {
//sdk大于6.0,但已有权限
openCamera();
}
} else {
//sdk小于6.0 注意:Manifest中应已配置了相机的使用权限
openCamera();
}
}
//打开相机
private void openCamera() {
initFile();//定义用户存储拍摄图片的文件
// 启动相机程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);//拍摄成功后将跳转至onActivityResult
}
imageUri是用于储存照片的文件路径,在initFile()中设置其值。
//创建文件并获取其路径的Uri
private void initFile() {
// 创建File对象,用于存储拍照后生成的图片
File outputImage = new File(path + profilePicFileName);//使用应用关联缓存目录存放图片
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT < 24) {
//7.0之前,调用Uri的FromFile()方法将File对象转换为Uri对象,
这个Uri对象标识着user_profile_picture.jpg这张图片的本地真实路径
imageUri = Uri.fromFile(outputImage);
} else {
//7.0之后,直接使用本地真实路径的Uri被认为是不安全的,会抛出FileUriExposed异常。
//因此使用FileProvider的getUriForFile方法将Uri转换成一个封装过的Uri对象
//FileProvider是一个特殊的内容提供器,我们需要在Manifest中对其进行定义
//(具体请查询Manifest中<provider>..<provider/>)
imageUri = FileProvider.getUriForFile(getActivity(),
BuildConfig.APPLICATION_ID+".fileprovider", outputImage);
}
}
使用FileProvider需要在Manifest中配置以下代码,并添加res/xml/file_paths.xml文件
<!--android:authorities值应该与上一段代码中的BuildConfig.APPLICATION_ID+".fileprovider"一致-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
file_paths.xml文件
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="profilePic" path="" />
</paths>
<!--path属性表示共享的具体路径,设置为空表示将整个SD卡共享-->
相册相关
//尝试打开相册
private void tryOpenAlbum() {
//请求访问SD卡的动态权限,该权限在4.4之后被认为是危险权限
if (ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
//进入此处代表没有访问SD卡的权限 下面申请权限
//用户点击同意或拒绝后会进入onRequestPermissionsResult
ProfilePicFragment.this.requestPermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_PERMISSION_SD_CODE);
} else {
openAlbum();
}
}
主要代码逻辑
/**
* 完成图片选择,剪裁后展示的功能
* 代码按照用户操作思路安排方法的顺序
*/
public class ProfilePicFragment extends Fragment implements View.OnClickListener {
...
变量定义
...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...
//获取应用关联缓存目录
//6.0之后,读写SD卡被列为危险权限,如果将图片放在SD卡的任何其他位置,都需要运行时权限,
// 而使用应用关联目录可以跳过这一步
path = getActivity().getExternalCacheDir();
...
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
from_album.setOnClickListener(this);
from_camera.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.from_album:
getPicFromAlum();//从相册获取头像图片
break;
case R.id.from_camera:
getPicFromCamera();//使用相机拍摄图片
break;
default:
break;
}
}
/**
* 正常从相册获取图片的逻辑
* 尝试打开相册->有权限则打开相册->用户选择了图片->跳转至剪裁页面->剪裁成功将图片保存并展示
*/
private void getPicFromAlum() {
tryOpenAlbum();//尝试打开相册
}
private void tryOpenAlbum() {
....
openAlbum()
}
//打开相册
private void openAlbum() {
...
}
/**
* 正常从相机获取图片的逻辑
* 尝试打开相机->有权限则打开相机->用户拍摄了图片->跳转至剪裁页面->剪裁成功将图片保存并展示
*/
private void getPicFromCamera() {
tryOpenCamera();//尝试打开相机
}
private void tryOpenCamera() {
...
openCamera();
...
}
//打开相机
private void openCamera() {
initFile();//定义用户存储拍摄图片的文件
// 启动相机程序
...
}
//创建文件并获取其路径的Uri
private void initFile() {
...
imageUri = ...;
...
}
/**
* 权限获取结果响应
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
...
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d("Result", "onActivityResult: "+requestCode);
switch (requestCode) {
case TAKE_PHOTO:
//由相机跳转而来
cropPhoto(imageUri);// 裁剪图片
...
case CHOOSE_PHOTO:
//由相册跳转而来
cropPhoto(uri);// 裁剪图片
...
case CROP_PHOTO:
//由剪裁Activity跳转而来
...
}
}
/**
* 调用系统的裁剪功能
*/
public void cropPhoto(Uri uri) {
..
}
//保存图片
private void setPicToView(Bitmap mBitmap) {
...
}
github项目地址
https://github.com/snowowolf/profilepicturedemo
补充
- 关于7.0之后应用间共享文件的FileProvider,hongyang大神有一篇新鲜出炉的文章
http://mp.weixin.qq.com/s/0BFFoyJdrzkfk6k66tHtyA - 启动时没有加载已经上次保存的图片
可以参考以下代码
//对本地图片压缩后再显示
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 只获取图片的大小信息,而不是将整张图片载入在内存中,避免内存溢出
BitmapFactory.decodeFile(path + imgFileName, options);
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 2; // 默认像素压缩比例,压缩为原图的1/2
int minLen = Math.min(height, width); // 原图的最小边长
if (minLen > 100) { // 如果原始图像的最小边长大于100dp(此处单位我认为是dp,而非px)
float ratio = (float) minLen / 100.0f; // 计算像素压缩比例
inSampleSize = (int) ratio;
}
options.inJustDecodeBounds = false; // 计算好压缩比例后,这次可以去加载原图了
options.inSampleSize = inSampleSize; // 设置为刚才计算的压缩比例
Bitmap bt = BitmapFactory.decodeFile(path + imgFileName, options);// 从SD卡中找头像,转换成Bitmap
if (bt != null) {
@SuppressWarnings("deprecation")
Drawable drawable = new BitmapDrawable(bt);// 转换成drawable
profilePicImg.setImageDrawable(drawable);
} else {
/**
* 如果SD里面没有则需要从服务器取头像,取回来的头像再保存在SD中
* 或显示默认图片
*/
}