Android进阶封装之一个类实现兼容Android 6.0权限、适配Android7.0 拍照!


Android进阶封装之“史无前例”一个类实现兼容Android 6.0权限、适配Android 拍照7.0: 相机与相册上传图片就用我好啦!


一、前言。


  • 本篇博客从基本的AndroidN开始说怎么适配其拍照,其与7.0以下有何区别?

  • 再详细分析如何封装在 avtivity和fragment中的区别? (重点

  • 再详细说明下本封装库如何集成与使用。


博主最近为了适配 AndroidN的拍照,浏览了很多技术文章,有些是需要4到5个类来实现、有些不能在fragment中实现… … ,个人敢想 “可以一个类实现封装全部拍照工作吗?” , 扬起袖子就是干!在辛苦的四个小时,终于把这个封装给弄出来的!


已经放在GitHub上了(强烈推荐Star):https://github.com/xuhongv/TakePhotoAndroidN-master


  • 已经兼容在小米手机出现Attempt to invoke interface method ‘boolean android.database.Cursor.moveToFirst()问题 。(2017/8/19)

  • 已经兼容在fragment出现权限授权不回调的bug。(2017/8/19)

  • 已经兼容在fragmenr出现图片不回调的bug。(2017/8/18)


二、Android7.0和其以下的版本在拍照时候有何区别?


  • 由于从Android7.0(下面统一为AndroidN)开始,直接使用真实的路径的Uri会被认为是不安全的,会抛出一个FileUriExposedException这样的异常。需要使用FileProvider,选择性地将封装过的Uri共享到外部。

  • 出于以上问题,很多事情都意味着要适配,比如你在AndroidN以下,可以跳转到拍照和图库界面,但是在AndroidN就不行了!但是会有error等级的log输出,出现FileUriExposedException这样的异常,原因是Andorid7.0的“私有目录被限制访问”,“StrictMode API 政策”。

  • 谷歌这样做,出自用户隐私的考虑。既然这样,我们就必须要通过FileProvider(Provider的一个子类)共享其URL到外部即可。


问题来了,FileProvider应该如何写?

  • 1.在manifest中添加provider,毕竟Provider也是属于四大组件之一。配置中的authorities按照江湖规矩一般加上包名,${applicationId}是获取当前项目的包名,前提是在module下的gradle.buile文件中defaultConfig{}闭包中要有applicationId属性哦。
      <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_provider_paths"/>
        </provider>
  • 2、在您工程的 res 文件夹根目录下创建一个xml文件夹,里面再新建一个file_provider_paths文件夹,其对应都是在上面的meta-data标签下面的android:resource值的。

    • 代码中path=”“,是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。其file_provider_paths内容如下:
         <?xml version="1.0" encoding="utf-8"?>
           <resources>
             <paths>
             <external-path path="" name="myFile"/>
             </paths>
           </resources>

  • 3、 自此为止,已经把FileProvider的基本的环境搭建好了,别忘了加拍照权限、读取和存储SD卡的权限哦:
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

三、熟悉拍照的整个流程。


  • ①、你先要创建一个URL作为你拍照后得到的图片的URL , 这里我们使用 intent , 制定Action为 MediaStore.ACTION_IMAGE_CAPTURE , 这样就可以跳转到相机界面了 ,别忘了,在intent上把你要传的URL放上去,名字一定要是 :MediaStore.EXTRA_OUTPUT 。最后使用startActivityForResult()跳转,别忘了回调码。

    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
    startActivityForResult(intent, CODE_ORIGINAL_PHOTO_CAMERA)    
    

  • ②、等到跳转相机界面后,我们不需理会用户怎么操作,我们只需关心传回来的Uri数据是否为空,毕竟在拍照后用户可能点击了舍弃,导致拿到相片为空。

    @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
              //相片处理
                    if (resultCode != RESULT_CANCELED) {
                                   switch (requestCode) {
                                   //相册数据,回调码要和上面一致。
                                 case IMAGE_REQUEST_CODE:
    
                                //判断返回的数据Uri是否为空?
                                     if(imgUri!=null){
                                     //doyourthings
                                     }
                                    break;
                                            }
                                 }                                 
    

下面是整个拍照流程图:

Created with Raphaël 2.1.0 使用Intent跳转到手机相机,让用户选择图片。 用户选择相机拍照。可能拍照后取消,或是直接取消拍照? onActivityResult()方法判断拿到Uri数据为空? 自己处理。 yes no

四、熟悉从图库拿图片的整个流程。


  • ①、从本地图库拿图片的原理和相机拍照一样,也是靠Intent跳转到图库界面,指定的Action为Intent.ACTION_PICK,Type为 “image/*” ,别忘了回调码。
               Intent intent = new Intent(Intent.ACTION_PICK);
               intent.setType("image/*");
               startActivityForResult(intent, IMAGE_REQUEST_CODE);
  • ②、与拍照不同的是,onActivityResult()方法成功返回的话。那么传回来的是一个图片文件哦。同样,先判断是否为空?

      @Override
            public void onActivityResult(int requestCode, int resultCode, Intent data) {
                  //相片处理
                        if (resultCode != RESULT_CANCELED) {
                                       switch (requestCode) {
                                       //相册数据,回调码要和上面一致。
                                     case IMAGE_REQUEST_CODE:
                                           //判断返回的数据data.getData()是否为空?
                                            if(data.getData()!=null){
                                            //doyourthings
                                           }
                                        break;
                                                }
                                     }

下面是整个相册选择图片流程图:

Created with Raphaël 2.1.0 使用Intent跳转到手机系统相册界面,让用户选择图片。 用户选择相册图片。可能选择图片后取消? onActivityResult()方法判断拿到data.getData()数据为空? 自己处理。 yes no

五、动态权限?


  • 让不少人烦恼的是,在安卓6.0之后,需要动态授权,那么作为拍照、写入SD卡和读取SD卡,这些“危险权限”,动态授权是必然的。

  • 我这里采用郭神的做法,如果用户拒绝的某些权限的话,会通过接口提示。代码如下:


    /**
     * 申请运行时权限
     * 来自郭神公开课
     */
    private void requestRuntimePermission(String[] permissions, PermissionListener listener) {

        permissionListener = listener;
        List<String> permissionList = new ArrayList<>();
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(mContext, permission) != PackageManager.PERMISSION_GRANTED) {
                permissionList.add(permission);
            }
        }

        //此处兼容了无法在fragment回调监听事件
        if (!permissionList.isEmpty()) {
            if (isActicity) {
                ActivityCompat.requestPermissions((Activity) mContext, permissionList.toArray(new String[permissionList.size()]), 1);
            } else {
                mFragment.requestPermissions(permissionList.toArray(new String[permissionList.size()]), 1);
            }

            if (takeCallBacklistener != null) {
                takeCallBacklistener.failed(1, permissionList);
            }
        } else {
            permissionListener.onGranted();
        }
    }

六、关于裁剪的代码!


  private void statZoom(File srcFile, File output) {

    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(getImageContentUri(mContext, srcFile), "image/*");

    // crop为true是设置在开启的intent中设置显示的view可以剪裁
    intent.putExtra("crop", "true");
    // 是否缩放?如果不缩放,会出现黑边哦
    intent.putExtra("scale", true);

    // aspectX aspectY 是宽高的比例
    intent.putExtra("aspectX", aspectX);
    intent.putExtra("aspectY", aspectY);

    // outputX,outputY 是剪裁图片的宽高
    intent.putExtra("outputX", outputX);
    intent.putExtra("outputY", outputY);
    intent.putExtra("return-data", false);//true:不返回uri,false:返回uri
    intent.putExtra("scaleUpIfNeeded", true);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(output));
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());

    //此处兼容在fragment不会回调图片问题
    if (isActicity) {
        mActivity.startActivityForResult(intent, CODE_TAILOR_PHOTO);
    } else {
        mFragment.startActivityForResult(intent, CODE_TAILOR_PHOTO);
     }
    }

七、封装的主角来了!


6.1 、介绍只需三步的环境集成:

  • 第一步:把 demo下的res的 xml文件夹整个复制到你的工程res文件夹根目录下:

第一步


  • 第二步:在你AndroidManifest.xml下的Application节点下加入以下代码:
          <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_provider_paths"/>
           </provider>

  • 如下所示

这里写图片描述


-第三步 :把demo的就仅仅一个类 TakePictureManager.class 复制过去就可以啦!别忘了在清单文件加相关权限哦!


6.2 、怎么使用?

示例:


TakePictureManager takePictureManager takePictureManager = new TakePictureManager(this);
                //开启裁剪 比例 1:3 宽高 350 350  (默认不裁剪)
                takePictureManager.setTailor(1, 3, 350, 350);
                //拍照方式
                takePictureManager.startTakeWayByCarema();
                //监听回调
                takePictureManager.setTakePictureCallBackListener(new TakePictureManager.takePictureCallBackListener() {
         //成功拿到图片,isTailor 是否裁剪? ,outFile 拿到的文件 ,filePath拿到的URl
        @Override
        public void successful(boolean isTailor, File outFile, Uri filePath) {
                  }
        //失败回调
        @Override
        public void failed(int errorCode, List<String> deniedPermissions) {

                   }
                });

    //把本地的onActivityResult()方法回调绑定到对象
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        takePictureManager.attachToActivityForResult(requestCode, resultCode, data);
    }

    //onRequestPermissionsResult()方法权限回调绑定到对象
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        takePictureManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

使用详细步骤:


  • ①、先new一个TakePictureManager 对象,构造方法只需传this即可(不管你在Activity还是在Fragment)。

  • ②、重写onActivityResult()方法,调用对象的attachToActivityForResult()方法,参数依次是onActivityResult()回调的参数。 实现把拍照或相册回调发数据绑定在对象方法处理。

  • ③、重写onRequestPermissionsResult()方法,调用对象的onRequestPermissionsResult()方法,参数依次是onRequestPermissionsResult()回调的参数。 实现把权限回调绑定在对象方法处理。

  • ④、这时候,你只需调用对象方法,即可轻松调用相机或相册。具体的方法参数说明如下:


方法名参数说明
setTailor(int aspectX, int aspectY, int outputX, int outputY)要裁剪的宽比例、要裁剪的高比例、要裁剪图片的宽、要裁剪图片的高一旦调用,表示要裁剪,默认不裁剪
startTakeWayByCarema()无参数调用相机
startTakeWayByAlbum()无参数调用相册
setTakePictureCallBackListener(takePictureCallBackListener listener)takePictureCallBackListener 回调接口调用相机或相册后的回调

接口方法说明
takePictureCallBackListenersuccessful(boolean isTailor, File outFile, Uri filePath)成功回调! isTailor : 是否已裁剪, outFile :输出的照片文件 ,filePath :输出的照片Uri 。
failed(int errorCode, List deniedPermissions)失败回调!errorCode :0表示相片已移除或不存在! 1表示权限被拒绝,deniedPermissions当权限被拒绝时候,会通过list传回

八、造轮子时候遇到的问题:


  • 在Fragment使用时候,回调的相片数据被依附的activity的onActivityResult()方法拦截了!相信这个问题困扰许多人的问题,在使用他人代码时候,在Activity可以使用,但是在Fragment却失败。原因在于:

    • 在Fragment就存在startActivityForResult()方法,不需要 getActivity().startActivityForResult() , 也就是说不需要调用 依附的Activity的此方法,本身就有此方法。这是我翻阅 Fragment源码发现。截图如下:

这里写图片描述


  • 于是乎我在封装时候,特意这样做:

这里写图片描述




评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半颗心脏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值