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

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

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"?>
<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"
            />
        <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>

        三个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() {
            @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);
        // 单击弹出窗以外处 关闭弹出窗
        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;
            }
        });
    }
}

        代码是很主流的写法,没什么特别的。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.Toolbar
        android: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>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >
        <RelativeLayout
            android:id="@+id/user_head"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginTop="1dp"
            android:background="@drawable/bg_info_rl">
            <TextView
                android: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.CircleImageView
                android: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>

        <RelativeLayout
            android:id="@+id/user_nick_name"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginTop="1dp"
            android:background="@drawable/bg_info_rl">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginLeft="15dp"
                android:textSize="16sp"
                android:text="昵称"/>
            <TextView
                android: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" />
            <ImageView
                android: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>

        <RelativeLayout
            android:id="@+id/user_gender"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_marginTop="1dp"
            android:background="@drawable/bg_info_rl">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginLeft="15dp"
                android:textSize="16sp"
                android:text="性别"/>
            <TextView
                android: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" />
            <ImageView
                android: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;
初始化布局
@Override
    protected 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对应的int
        InfoPrefs.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);
        //初始化 toolbar
        Toolbar 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,包括悬浮窗、蓝牙、闹钟等,合作写的,代码的风格不太一致,见谅。

@Override
    public void onClick(View v) {
        switch(v.getId()){
            case R.id.user_head:
                //创建存放头像的文件夹
                PictureUtil.mkdirMyPetRootDirectory();
                mPhotoPopupWindow = new PhotoPopupWindow(UserInfoActivity.this, new View.OnClickListener() {
                    @Override
                    public 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 的 Activity
                            if (intent.resolveActivity(getPackageManager()) != null) {
                                startActivityForResult(intent, REQUEST_IMAGE_GET);
                            } else {
                                Toast.makeText(UserInfoActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                            }
                        }
                    }
                }, new View.OnClickListener()
                {
                    @Override
                    public 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() {
                            @Override
                            public 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卡读写权限的时候只申请读权限或者写权限就可以了。

@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/*");
                    // 判断系统中是否有处理该 Intent 的 Activity
                    if (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

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.mypet.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android: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。

@Override
    protected 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 detection
        startActivityForResult(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

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




发布了6 篇原创文章 · 获赞 12 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览