现在一般的手机应用都会有上传头像的功能,我在实现这个功能的时候遇到很多问题,这里专门记录一下。

add 2018/5/10 21:05

先列举一下我出现过的问题:

1.运行时权限

2.调用系统相机拍照后crash,或者返回RESULT_CANCEL(0)

3.选择相片后得到的Uri为空或者为Uri后半段为资源ID(%1234567这种)

4.调用系统裁剪后crash

5.小米手机的特别情况

还有许多小问题,大多都是上面问题引起的并发症,就不一一列举了。

先上代码,慢慢讲。

1.布局

只关注头像那一栏就可以了,点击头像后会弹出选择页面。PopupWindow的实现如下:

1.1    新建layout文件pop_item

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="#66000000"><LinearLayoutandroid: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"><Buttonandroid: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="拍照"/><Viewandroid:layout_width="match_parent"android:layout_height="1dp"/><Buttonandroid: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="从相册选择"/><Buttonandroid: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>

三个Button分别对应三个按钮,中间的View是两个按钮之间的线,colorMainGreen是

<color name="colorMainGreen">#40cab3</color>

1.2    可以看到三个按钮分别是上圆角,下圆角,全圆角,在drawable中新建3个xml,绘制Button样式

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>

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>

while_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>

简单解释一下shape的用法,每一组< />中制定一个属性(不知道是不是这么叫,但是是这个意思),在每条属性中可以指定更多的细节。solid指定填充,corners指定圆角,在corners中的radius指定了圆角的半径,stroke用于描边。

1.3    PhotoPopupWindow布局都写好了,现在我们要写自己的PhotoPopupWindow类加载它,同时给他添加点击事件。新建一个package,命名为popup(这样做的目的是使得代码结构清晰),在这个包下新建PhotoPopupWindow类,继承PopupWindow

public class PhotoPopupWindow extends PopupWindow {private static final String TAG = "PhotoPopupWindow";private View mView; // PopupWindow 菜单布局private Context mContext; // 上下文参数private View.OnClickListener mSelectListener; // 相册选取的点击监听器private View.OnClickListener mCaptureListener; // 拍照的点击监听器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);assert inflater != null;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() {@Overridepublic 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);// 单击弹出窗以外处 关闭弹出窗mView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic 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;}});}
}

代码是很主流的写法,没什么特别的。TAG常量会出现在每一个JAVA类中,即使我用不到他。这样写便于区别Log发生的位置。两个OnClickListener需要在实例化的时候实现。

1.4    弹出框写好了,接下来是头像页面的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/backgroundWhite"tools:context=".activity.UserInfoActivity"><android.support.v7.widget.Toolbarandroid:id="@+id/toolbar_userinfo"android:layout_width="match_parent"android:layout_height="?attr/actionBarSize"android:background="?attr/colorPrimary"app:title="用户信息"android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"app:popupTheme="@style/ThemeOverlay.AppCompat.Light"></android.support.v7.widget.Toolbar><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><RelativeLayoutandroid:id="@+id/user_head"android:layout_width="match_parent"android:layout_height="50dp"android:layout_marginTop="1dp"android:background="@drawable/bg_info_rl"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center_vertical"android:layout_marginLeft="15dp"android:textSize="16sp"android:text="头像 "/><de.hdodenhof.circleimageview.CircleImageViewandroid:id="@+id/user_head_iv"android:layout_width="40dp"android:layout_height="40dp"android:layout_alignParentEnd="true"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:layout_marginRight="25dp"app:civ_border_color="#F3F3F3"app:civ_border_width="1dp"/></RelativeLayout><RelativeLayoutandroid:id="@+id/user_nick_name"android:layout_width="match_parent"android:layout_height="50dp"android:layout_marginTop="1dp"android:background="@drawable/bg_info_rl"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center_vertical"android:layout_marginLeft="15dp"android:textSize="16sp"android:text="昵称"/><TextViewandroid:id="@+id/user_nick_name_TV"android:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center_vertical"android:layout_marginRight="10dp"android:textSize="16sp"android:layout_toLeftOf="@id/user_nick_name_IV" /><ImageViewandroid:id="@+id/user_nick_name_IV"android:layout_width="wrap_content"android:layout_height="match_parent"android:src="@drawable/ic_chevron_right_black_24dp"android:layout_alignParentRight="true"android:layout_marginRight="15dp"/></RelativeLayout><RelativeLayoutandroid:id="@+id/user_gender"android:layout_width="match_parent"android:layout_height="60dp"android:layout_marginTop="1dp"android:background="@drawable/bg_info_rl"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center_vertical"android:layout_marginLeft="15dp"android:textSize="16sp"android:text="性别"/><TextViewandroid:id="@+id/user_gender_TV"android:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center_vertical"android:layout_marginRight="10dp"android:textSize="16sp"android:layout_toLeftOf="@id/user_gender_IV" /><ImageViewandroid:id="@+id/user_gender_IV"android:layout_width="wrap_content"android:layout_height="match_parent"android:src="@drawable/ic_chevron_right_black_24dp"android:layout_marginRight="15dp"android:layout_alignParentRight="true"/></RelativeLayout></LinearLayout>
</LinearLayout>
我解释一下有疑问的地方。Toolbar是谷歌推荐的标题栏,比Actionbar具有更好的拓展性。CircleImageView是一个开源库,自行百度GitHub地址,用于产生圆形图片。这一部分的设计可以参考郭霖大神的《第一行代码》或者他的博客,有关Material design的内容(在十一章好像),他讲解的很详细。backgroundWhite是

<color name="backgroundWhite">#EBEBEB</color>

tools:context指定了该layout将在哪个activiy展示,指定了tools:context的layout可以动态预览布局,也就是说我在acticity的onCreate里改变了某个控件,我可以在不run的情况下预览这个改变的效果。tools还有一些别的功能,我懂的也不多,就不介绍了。bg_info_rl是可变背景,这里我只是简单设置了点击和松开时候的颜色,代码这段结束后贴,ic_chevron_right_black_24dp是Material design提供的小图标,样式是“>”,大家可以自行搜索,这是谷歌官方提供的免费素材包,可以在谷歌MD的官网或者GitHub上获取。

bg_info_rl

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_window_focused="false" android:drawable="@drawable/bg_info_rl_normal" /><item android:state_pressed="true" android:drawable="@drawable/bg_info_rl_pressed" />
</selector>

bg_info_rl_normal

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="@color/white" />
</shape>

bg_info_rl_pressed

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="@color/light_grey" />
</shape>

color

<color name="white">#FFFFFF</color>
<color name="light_grey">#EAEAEA</color> <!--浅灰色-->

2.运行时权限的问题

add 2018/5/11 10:18

android 6.0引入了运行时权限,用户不必在安装时授予所有权限,可以在使用到相关功能时再授权。普通权限不需要运行时申请,危险权限则必须,否则程序会crash掉。

头像功能需要以下三个权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.CAMERA" />

读写SD卡,使用相机,他们都是危险权限,接下来新建UserInfoAcitivity,继承AppCompatActivity 继承View.OnClickListener接口。

变量和常量

private static final String TAG = "UserInfoActivity";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_CHANGE_USER_NICK_NAME = 10;private static final String IMAGE_FILE_NAME = "user_head_icon.jpg";PhotoPopupWindow mPhotoPopupWindow;TextView textView_user_nick_name;TextView textView_user_gender;CircleImageView circleImageView_user_head;

初始化布局

@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_userinfo);textView_user_nick_name = findViewById(R.id.user_nick_name_TV);textView_user_gender = findViewById(R.id.user_gender_TV);circleImageView_user_head = findViewById(R.id.user_head_iv);//InfoPrefs自己封装的一个 SharedPreferences 工具类//init()指定文件名,getData(String key)获取key对应的字符串,getIntData(int key)获取key对应的intInfoPrefs.init("user_info");refresh();RelativeLayout relativeLayout_user_nick_name = findViewById(R.id.user_nick_name);relativeLayout_user_nick_name.setOnClickListener(this);RelativeLayout relativeLayout_user_gender = findViewById(R.id.user_gender);relativeLayout_user_gender.setOnClickListener(this);RelativeLayout relativeLayout_user_head = findViewById(R.id.user_head);relativeLayout_user_head.setOnClickListener(this);//初始化 toolbarToolbar toolbar = findViewById(R.id.toolbar_userinfo);setSupportActionBar(toolbar);ActionBar actionBar = getSupportActionBar();if (actionBar != null) {//指定toolbar左上角的返回按钮,这个按钮的id是home(无法更改)actionBar.setDisplayHomeAsUpEnabled(true);//actionBar.setHomeAsUpIndicator();}}
public void refresh(){textView_user_nick_name.setText(InfoPrefs.getData(Constants.UserInfo.NAME));textView_user_gender.setText(InfoPrefs.getData(Constants.UserInfo.GENDER));showHeadImage();//circleImageView_user_head.setImageURI();}

为每一个RelativeLayout都添加了点击事件,这里我们只关注头像的点击事件。出现的工具类我只解释功能,完整代码可以在文末我的GitHub获取,这个项目是一个手机桌面宠物的demo,包括悬浮窗、蓝牙、闹钟等,合作写的,代码的风格不太一致,见谅。

@Overridepublic void onClick(View v) {switch(v.getId()){case R.id.user_head://创建存放头像的文件夹PictureUtil.mkdirMyPetRootDirectory();mPhotoPopupWindow = new PhotoPopupWindow(UserInfoActivity.this, new View.OnClickListener() {@Overridepublic void onClick(View v) {// 文件权限申请if (ContextCompat.checkSelfPermission(UserInfoActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {// 权限还没有授予,进行申请ActivityCompat.requestPermissions(UserInfoActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200); // 申请的 requestCode 为 200} else {// 如果权限已经申请过,直接进行图片选择mPhotoPopupWindow.dismiss();Intent intent = new Intent(Intent.ACTION_PICK);intent.setType("image/*");// 判断系统中是否有处理该 Intent 的 Activityif (intent.resolveActivity(getPackageManager()) != null) {startActivityForResult(intent, REQUEST_IMAGE_GET);} else {Toast.makeText(UserInfoActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();}}}}, new View.OnClickListener(){@Overridepublic void onClick (View v){// 拍照及文件权限申请if (ContextCompat.checkSelfPermission(UserInfoActivity.this,Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED|| ContextCompat.checkSelfPermission(UserInfoActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {// 权限还没有授予,进行申请ActivityCompat.requestPermissions(UserInfoActivity.this,new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申请的 requestCode 为 300} else {// 权限已经申请,直接拍照mPhotoPopupWindow.dismiss();imageCapture();}}});View rootView = LayoutInflater.from(UserInfoActivity.this).inflate(R.layout.activity_userinfo, null);mPhotoPopupWindow.showAtLocation(rootView,Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);break;case R.id.user_nick_name:ChangeInfoBean bean = new ChangeInfoBean();bean.setTitle("修改昵称");bean.setInfo(InfoPrefs.getData(Constants.UserInfo.NAME));Intent intent = new Intent(UserInfoActivity.this,ChangeInfoActivity.class);intent.putExtra("data", bean);startActivityForResult(intent,REQUEST_CHANGE_USER_NICK_NAME);break;case R.id.user_gender:new ItemsAlertDialogUtil(UserInfoActivity.this).setItems(Constants.GENDER_ITEMS).setListener(new ItemsAlertDialogUtil.OnSelectFinishedListener() {@Overridepublic void SelectFinished(int which) {InfoPrefs.setData(Constants.UserInfo.GENDER,Constants.GENDER_ITEMS[which]);textView_user_gender.setText(InfoPrefs.getData(Constants.UserInfo.GENDER));}}).showDialog();break;default:}}

运行时权限的逻辑很简单,先判断是否已经授权过,如果已经授权,则直接进行操作,否则请求授权,根据请求结果处理。请求结果在onRequestPermissonsResult里处理。有一点要提一下,运行时权限申请是按照组来处理的,也就是说同属一个组的权限的请求是一样的,而用户只要授权一个,同组的权限也会同时被授权。SD卡的读写全是同属STORAGE组,所以我在申请SD卡读写权限的时候只申请读权限或者写权限就可以了。

@Overridepublic 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/*");// 判断系统中是否有处理该 Intent 的 Activityif (intent.resolveActivity(getPackageManager()) != null) {startActivityForResult(intent, REQUEST_IMAGE_GET);} else {Toast.makeText(UserInfoActivity.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;}//super.onRequestPermissionsResult(requestCode, permissions, grantResults);}

到这里,运行时权限就已经处理好了。

3.拍照和选图

add 2018/5/11 11:30

从这里开始就可能会NullPointerException,SecurityException等问题。

3.1    拍照

private void imageCapture() {Intent intent;Uri pictureUri;//getMyPetRootDirectory()得到的是Environment.getExternalStorageDirectory() + File.separator+"MyPet"//也就是我之前创建的存放头像的文件夹(目录)File pictureFile = new File(PictureUtil.getMyPetRootDirectory(), IMAGE_FILE_NAME);// 判断当前系统if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//这一句非常重要intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//""中的内容是随意的,但最好用package名.provider名的形式,清晰明了pictureUri = FileProvider.getUriForFile(this,"com.example.mypet.fileprovider", pictureFile);} else {intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);pictureUri = Uri.fromFile(pictureFile);}// 去拍照,拍照的结果存到pictureUri对应的路径中intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);Log.e(TAG,"before take photo"+pictureUri.toString());startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);}

从android7.0(SDK>=24)开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出一个FileUri什么什么Exception(记不清了),必须使用FileProvider封装过的Uri,感兴趣可以自己看下,7.0y以上得到的uri是content开头,以下以file开头。那么FileProvider怎么使用呢?

首先,在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="MyPet/" name="MyPetRoot" /><external-path name="sdcard_root" path="."/>
</paths>

paths指定要用到的目录,external-path是SD卡根目录,所以我的第一条的路径是SD卡根目录下新建的MyPet文件夹,取名为MyPetRoot,这个虚拟目录名可以随意取,最终的Uri会是这样 content://com.example.mypet.fileprovider/MyPetRoot/......。第二条是将SD卡共享,这是用于相册选图。

接下来,在AndroidManifest.xml中注册这个provider

<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="com.example.mypet.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/provider_paths" /></provider>

有几点要注意:authorities要与之前使用FileProvider时""中的内容相同(实际上的逻辑是使用FileProvider时要与声明的authorites相同)。exported必须要并且只能设为false,否则会报错,其实也很好理解,我们将sd卡共享了,如果再设置成可以被外界访问,那么权限就没有用了。grantUriPermissions要设置为true,这样才能让其他程序(系统相机等)临时使用这个provider。最后在<meta-data />指定使用的resource,也就是我们刚才写的xml。

现在我们回到imageCapture,还有两句没有解释。addflags的作用是给Intent添加一个标记(我不知道应该怎么叫,看我后面的解释),这里我们添加的是Intent.FLAG_GRANT_READ_URI_PERMISSION,他的作用是临时授权Intent启动的Activity使用我们Rrovider封装的Uri。最后一行启动拍照Activity,请求码是REQUEST_IMAGE_CAPTURE,这个值自己设置,用于区分回调结果来自哪个请求。这样,我们就完成了拍照请求,会跳转到系统拍照界面,接下来就要处理拍照得到的照片了。

重写onActicityResult方法,我直接把所有请求先全贴了,拍照后的回调是case REQUEST_IMAGE_CAPTURE下的内容,逻辑很简单,拍照后需要裁剪,裁剪需要用到我们拍照得到的图片的Uri。

@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {// 回调成功if (resultCode == RESULT_OK) {switch (requestCode) {// 切割case REQUEST_SMALL_IMAGE_CUTTING:Log.e(TAG,"before show");File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");Uri cropUri = Uri.fromFile(cropFile);setPicToView(cropUri);break;// 相册选取case REQUEST_IMAGE_GET:Uri uri= PictureUtil.getImageUri(this,data);startPhotoZoom(uri);break;// 拍照case REQUEST_IMAGE_CAPTURE:File pictureFile = new File(PictureUtil.getMyPetRootDirectory(), IMAGE_FILE_NAME);Uri pictureUri;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {pictureUri = FileProvider.getUriForFile(this,"com.example.mypet.fileprovider", pictureFile);Log.e(TAG,"picURI="+pictureUri.toString());} else {pictureUri = Uri.fromFile(pictureFile);}startPhotoZoom(pictureUri);break;// 获取changeinfo销毁 后 回传的数据case REQUEST_CHANGE_USER_NICK_NAME:String returnData = data.getStringExtra("data_return");InfoPrefs.setData(Constants.UserInfo.NAME,returnData);textView_user_nick_name.setText(InfoPrefs.getData(Constants.UserInfo.NAME));break;default:}}else{Log.e(TAG,"result = "+resultCode+",request = "+requestCode);}}

到这里,拍照的功能已经实现了,如果不想裁剪,可以直接将  sd卡根目录/MyPet/user_head_icon.jpg这张图片显示,接下来讲一下相册选取的实现。

3.2    相册选取

其实相册选取的代码我在上面已经完全贴过了,权限申请的时候已经调用了相册,OnActivityResult中处理回调,这里我要说一下我踩的一个大坑

Uri uri= PictureUtil.getImageUri(this,data);

具体的各种问题我已经记不清了,没办法进行梳理,我只能笼统的说一下了(本来想说的很多,结果解决后把坑玩的差不多了,嘤嘤嘤,以后要一边踩坑一边写)。总体上是因为android4.4开始相册中返回的图片不再是图片的真是Uri了,而是封装过的。我测试的时候返回的Uri五花八门,有的是可以正常处理的,有的需要进行解析,其中以小米最乱(不得不说MIUI对开发者各种不友好,为了用户体验各种不遵守规则)。最终在各种博客(可能是我用法的原因,有不少博客的方法我用了还是会报错)的洗礼下终于解决了,我把他封装在了PictureUtil里面,过程是    乱七八糟的uri ->真实路径 ->统一的uri 。

部分 50% 来自《第一行代码》,部分 25% 来自他人博客,部分 25% 是我自己写的

public class PictureUtil {private static final String TAG = "PictureUtil";private static final String MyPetRootDirectory = Environment.getExternalStorageDirectory() + File.separator+"MyPet";public static String getMyPetRootDirectory(){return MyPetRootDirectory;}public static Uri getImageUri(Context context,Intent data){String imagePath = null;Uri uri = data.getData();if(Build.VERSION.SDK_INT >= 19){if(DocumentsContract.isDocumentUri(context,uri)){String docId = DocumentsContract.getDocumentId(uri);if("com.android.providers.media.documents".equals(uri.getAuthority())){String id = docId.split(":")[1];String selection = MediaStore.Images.Media._ID+"="+id;imagePath = getImagePath(context,MediaStore.Images.Media.EXTERNAL_CONTENT_URI,selection);}else if("com.android.providers.downloads.documents".equals(uri.getAuthority())){Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(docId));imagePath = getImagePath(context,contentUri,null);}}else if("content".equalsIgnoreCase(uri.getScheme())){imagePath = getImagePath(context,uri,null);}else if("file".equalsIgnoreCase(uri.getScheme())){imagePath = uri.getPath();}}else{uri= data.getData();imagePath = getImagePath(context,uri,null);}File file = new File(imagePath);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {uri = FileProvider.getUriForFile(context,"com.example.mypet.fileprovider", file);} else {uri = Uri.fromFile(file);}return uri;}private static String getImagePath(Context context,Uri uri, String selection) {String path = null;Cursor cursor = context.getContentResolver().query(uri,null,selection,null,null);if(cursor != null){if(cursor.moveToFirst()){path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));}cursor.close();}return path;}public static void mkdirMyPetRootDirectory(){boolean isSdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);// 判断sdcard是否存在if (isSdCardExist) {File MyPetRoot = new File(getMyPetRootDirectory());if (!MyPetRoot.exists()) {try {MyPetRoot.mkdir();Log.d(TAG, "mkdir success");} catch (Exception e) {Log.e(TAG, "exception->" + e.toString());}}}}
}

到这里,有关图片获取的内容就都结束了

4.图片裁剪

前面传入的Uri处理好了,一般裁剪不会出什么问题,只有一个可能出现OOM(out of memory)问题,先上代码

private void startPhotoZoom(Uri uri) {Log.d(TAG,"Uri = "+uri.toString());//保存裁剪后的图片File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");try{if(cropFile.exists()){cropFile.delete();Log.e(TAG,"delete");}}catch(Exception e){e.printStackTrace();}Uri cropUri;cropUri = Uri.fromFile(cropFile);Intent intent = new Intent("com.android.camera.action.CROP");if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//添加这一句表示对目标应用临时授权该Uri所代表的文件intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);}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", false);Log.e(TAG,"cropUri = "+cropUri.toString());intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri);intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());intent.putExtra("noFaceDetection", true); // no face detectionstartActivityForResult(intent, REQUEST_SMALL_IMAGE_CUTTING);}

主要问题在这一句

intent.putExtra("return-data", false);

如果设置成true,那么裁剪得到的图片将会直接以Bitmap的形式缓存到内存中,Bitmap是相当大的,如果手机内存不足,或者手机分配的内存不足会导致OOM问题而闪退。当然,实际上现在的手机300X300一般不会OOM,但为了保险起见和可能需要更大图片的需求,最好将“return-data”置为false。

5.保存并显示图片

回调的代码在上面已经贴过了,这里可能会有一个疑问

case REQUEST_SMALL_IMAGE_CUTTING:Log.e(TAG,"before show");File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");Uri cropUri = Uri.fromFile(cropFile);setPicToView(cropUri);break;

按理说应该分版本获取Uri,否则会抛出异常。实际上并非如此,我之前没有搞明白7.0的Uri保护到底是做什么的,后来看了文档才明白 :

一个应用提供自身文件给其它应用使用时,如果给出一个file://格式的URI的话,应用会抛出FileUriExposedException

也就是说提供给外界时才需要fileprovider,也就是调用相机、相册、裁剪的时候才需要,自己使用是不需要的。谷歌做出这个改动的原因是

谷歌认为目标app可能不具有文件权限,会造成潜在的问题。所以让这一行为快速失败。

好了,基本上所有的坑都踩完了,下面补上其他的代码

public void setPicToView(Uri uri)  {if (uri != null) {Bitmap photo = null;try {photo = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));}catch (FileNotFoundException e){e.printStackTrace();}// 创建 Icon 文件夹if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {//String storage = Environment.getExternalStorageDirectory().getPath();File dirFile = new File(PictureUtil.getMyPetRootDirectory(),  "Icon");if (!dirFile.exists()) {if (!dirFile.mkdirs()) {Log.d(TAG, "in setPicToView->文件夹创建失败");} else {Log.d(TAG, "in setPicToView->文件夹创建成功");}}File file = new File(dirFile, IMAGE_FILE_NAME);InfoPrefs.setData(Constants.UserInfo.HEAD_IMAGE,file.getPath());//Log.d("result",file.getPath());// Log.d("result",file.getAbsolutePath());// 保存图片FileOutputStream outputStream = null;try {outputStream = new FileOutputStream(file);photo.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);outputStream.flush();outputStream.close();} catch (Exception e) {e.printStackTrace();}}// 在视图中显示图片showHeadImage();//circleImageView_user_head.setImageBitmap(InfoPrefs.getData(Constants.UserInfo.GEAD_IMAGE));}}
private void showHeadImage() {boolean isSdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);// 判断sdcard是否存在if (isSdCardExist) {String path = InfoPrefs.getData(Constants.UserInfo.HEAD_IMAGE);// 获取图片路径File file = new File(path);if (file.exists()) {Bitmap bm = BitmapFactory.decodeFile(path);// 将图片显示到ImageView中circleImageView_user_head.setImageBitmap(bm);}else{Log.e(TAG,"no file");circleImageView_user_head.setImageResource(R.drawable.huaji);}} else {Log.e(TAG,"no SD card");circleImageView_user_head.setImageResource(R.drawable.huaji);}}

huaji是一张在没有头像情况下的默认头像

6.总结

最后做一个总结,如果按照以上代码执行的话,会在SD卡根目录下创建一下文件和文件夹

dir:MyPet

file:user_head_icon.jpg

file:crop.jpg

dir:Icon

file:user_head_icon.jpg

MyPet/user_head_icon.jpg是拍照得到的原图,MyPet/crop.jpg是裁剪得到的图片,我们最后显示的是MyPet/Icon/user_head_icon.jpg,所以前两个如果不需要可以在最后删掉,删掉的代码我就不写了。

最后的最后,UserInfoActivity和整个项目的完整代码可在我的GitHub获取:NeedKwok

之后我也可能写一下如何实现一个闹钟

android实现拍照、相册选图、裁剪功能,兼容7.0以及小米相关推荐

  1. Android实现拍照相册图片上传功能

    更改头像功能不像修改信息一样直接提交参数就可以,需要上传图片文件 我就直接贴代码了首先给出布局文件 <ImageViewandroid:id="@+id/iv"android ...

  2. Android实现二维码扫描功能(四)-ZXing识别图片二维码,相册选图

    简介 上一篇 Android实现二维码扫描功能(三)-闪光灯控制介绍了光线较弱情况下开启闪光灯来辅助二维码识别的方法. 本篇我们介绍如何识别相册中的图片(含二维码) 动态演示 使用模拟器录制了动画演示 ...

  3. Android实现二维码扫描功能-ZXing识别图片二维码,相册选图

    文章目录 1.演示 2.权限问题 3.实现步骤 4.工具类 5.图片Uri处理(重要更新) 1.演示 2.权限问题 部分朋友在打开相册时遇到读写权限未授权的问题,我在开发的时候没有遇到,也没有注册读写 ...

  4. 微信公众平台后台编辑器上线图片缩放和封面图裁剪功能

    今日,微信公众平台后台编辑器又上线了两个更新,图片缩放和封面图裁剪功能,微信团队真喜欢深夜放毒,不想让人睡的节奏.[微信公众平台编辑器可以剪裁和替换正文图片了] 两个新增的功能如下 1.图片缩放 插入 ...

  5. android 图片自动裁剪图片,Android实现拍照、选择相册图片并裁剪功能

    通过拍照或相册中获取图片,并进行裁剪操作,然后把图片显示到ImageView上. 当然也可以上传到服务器(项目中绝大部分情况是上传到服务器),参考网上资料及结合项目实际情况, 测试了多款手机暂时没有发 ...

  6. android微信分享大图长图,裁剪微信分享缩略图片(长缩略图取中裁剪)

    有时分享出去的图片如果过长,就会导致缩略图的正方形图片呈现的效果是被压缩的,一般才用居中裁剪的方式,将裁剪后的图作为缩略图即可 本文后面将举微信分享到朋友圈和好友到例子,在分享出去到图片中,若为长图, ...

  7. 利用系统相机相册获取单张图片,兼容7.0

    场景 很多应用都有设置下头像之类的需求,都需要相机相册,但是在6.0的时候加上了运行时权限检测,7.0的时候限制了在应用建共享文件,就是之前相机相册使用的file://URI 不能使用了. 因为在7. ...

  8. Android开发之下载Apk安装的方法兼容Android7.0和8.0及以上

    具体查看代码: 首先在清单文件配置三个权限读写权限和请求安装权限(兼容Android8.0手机)如下: <!--安装apk权限--><uses-permission android: ...

  9. 拍照、从相册选图并对图片进行裁剪

    2013第一篇,大家新年快乐!感谢一直关注我博客的同学们,有你们的支持我才有动力越做越好!有阵子没写博客了,因为前阵子着实比较忙,没时间整理,今天主要实现一个小Demo,我们知道在Instagram或 ...

最新文章

  1. 创建尽可能小的 Docker 容器
  2. 第一章:SpringBoot入门
  3. 为CentOS添加网络yum源
  4. C# 无意间写了一段线程死锁的代码
  5. python可变对象与不可变对象_python 可变对象与不可变对象
  6. 加密Python脚本
  7. Build.VERSION_CODES类
  8. [题解] 2038: [2009国家集训队]小Z的袜子(hose)
  9. 飞天诚信ROCKEY-ARM(标准锁)软件加密狗使用记录
  10. OpenGL基础教程
  11. 射灯安装方法图解_射灯如何安装—射灯的安装方法介绍
  12. 聚焦新生代 戮默科技创造正向价值
  13. 华为小实例|VRRP协议
  14. IOS苹果手机下载DNF韩服手游手机版ios版下载
  15. 计算机学院校园文化标语,智慧校园文明宣传标语
  16. Arduino 机器人权威指南pdf
  17. 按关键字搜索的步骤教学
  18. 微信小程序点击按钮弹出弹窗_微信小程序开发弹出框实现方法
  19. 数学建模篇---2022国赛C题(二)(全程python,完整论文和代码可取!)
  20. 赛后补题:2022CCPC绵阳 A. Ban or Pick, What‘s the Trick

热门文章

  1. 手机必备应用:狐猴浏览器,一站式开启浏览器的所有用法
  2. 计算机组成原理语言方框图,计算机组成原理3---方框图语言
  3. 对创建的screen会话进行恢复时出现:There is no screen to be resumed matching XXX 解决办法
  4. 圆周率怎么计算来的?教你利用欧拉恒等式,生成圆周率万能公式!
  5. 盛天海科技:拼多多团长这样来做
  6. PostgreSQL不等于判断
  7. 一篇文章读懂支付宝9.0改版背后的产品逻辑和战略布局
  8. 高德地图可视化2.0封装(飞线,圆点,热力图)
  9. 【聊技术】在Android中实现自适应文本大小显示
  10. Flutter 中神奇的 AbsorbPointer 组件