前言
自从Android6.0以后,运行时权限申请就成为了一大痛点!动态申请吧,说它难吧,它又不难,说它不难吧,申请起来贼复杂;不申请吧,还要给你整闪退。
其实现在有很多权限申请框架了,那么为什么我还在造轮子呢?其实原因很简单,可能是年纪大了,开始喜欢简约风了,能少写一行的我绝对不想多写一个字母!说到底还是觉得现有的很多框架,用起来不是那么顺手,配置相对复杂,所以萌生了再整一个一行代码实现权限申请的想法。
系列
在工作之余,打算将一些常用的逻辑页面,模块,功能点做成library库,这样当有相似需求的时候,可以做到插拔式开发!现在系列中有以下内容
- App内部自动更新-AutoUpdateProject
- 选择城市-CitySelect
- 扫描二维码条形码控件-ScanCode
- 一键打开WebView控件-WebViewUtils
- 简约动态权限申请库-FanPermission
Github地址
具体的实现demo已经放到Github了,效果图什么的也在上面,上面也有写好的demo apk提供下载尝试,如果你觉得有用,记得star哦!哈哈哈,就是这么不要脸~~~
Github地址
正文
实现思路
权限申请无非三板斧,第一:检查是否已授权,第二:发起授权申请,第三:处理授权结果!第一步和第二步是固定的操作,只不过我们可以将繁琐的步骤封装起来。
第三步才是不好处理的核心,正常来说,当我们发起权限申请之后,必须要重写onRequestPermissionsResult方法来对授权结果进行处理,这就导致了无论你怎么封装,还是必须侵入到每个Activity的onRequestPermissionsResult中去,这就意味着不管怎样,你都至少要在两个地方写上对应的代码才能实现授权功能。但是作为调用者来说,我希望的操作只有两个,一,提供我需要授权的权限,二,回调给我授权结果。
如何实现通过回调的方式反馈给调用者授权结果呢?最开始想法是写一个透明宽高各1像素大小的Activity,将权限申请和授权结果的操作都放在里面处理,处理完之后关闭此Activity,对于用户来说其实也是无感知的。但是有一个最大的问题就是,当在这个Activity处理完onRequestPermissionsResult之后,如何才能通过回调的方式告知调用方呢?由于打开新的Activity之后,调用方Activity和处理逻辑的Activity是两个不同的页面,在不借助其他技术的情况下,是不好实现类似接口回调的方式进行通讯的,所以摒弃了这个方案。
后来想到,既然Activity不能胜任这份工作,可以尝试一下它的兄弟Fragment,Fragment中也可以发起授权申请并处理授权结果,最重要的是,Fragment是依附于Activity的,所以我们可以在同一个Activity下进行权限的申请和处理,并且不会污染到宿主Activity的逻辑,美滋滋。
实现过程
-
配置并申请权限
对于调用者来说,他需要做的就是配置他需要申请的权限列表。所以我们需要提供一个入口给他。
FanPermissionUtils.with(MainActivity.this) //添加所有你需要申请的权限 .addPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) .addPermissions(Manifest.permission.ACCESS_FINE_LOCATION) .addPermissions(Manifest.permission.CALL_PHONE) .addPermissions(Manifest.permission.ACCESS_WIFI_STATE) .addPermissions(Manifest.permission.CAMERA) ...
我们通过FanPermissionUtils.with方法构造出一个FanPermissionUtils工具类,使用链式方法让用户配置他需要申请的所有权限。FanPermissionUtils的内部维护了权限列表,存储所有需要授权的权限。
调用者配置好权限之后,调用startCheckPermission开始进行权限申请。
FanPermissionUtils.with(MainActivity.this) //添加所有你需要申请的权限 .addPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) .addPermissions(Manifest.permission.ACCESS_FINE_LOCATION) .addPermissions(Manifest.permission.CALL_PHONE) .addPermissions(Manifest.permission.ACCESS_WIFI_STATE) .addPermissions(Manifest.permission.CAMERA) //开始授权 .startCheckPermission();
此时,我们会构造一个没有页面的FanPermissionFragment来处理授权相关的逻辑,并将构造出的Fragment添加到宿主Activity中。
//构造FanPermissionFragment并将自己添加到宿主Activity中 FanPermissionFragment.newInstance(permissions).start(mContext); /** * 开始请求 */ public void start(Activity activity) { if (activity != null) { mContext = activity; if (Looper.getMainLooper() != Looper.myLooper()) { return; } activity.getFragmentManager().beginTransaction().add(this, activity.getClass().getName()).commit(); } }
一旦FanPermissionFragment被构造,就会对调用方提供的权限列表进行逐一排查,罗列出还未授权的权限列表,并对这些未授权的权限发起二次权限申请。
//记录未授权的权限 List
deniedPermissions = new ArrayList<>(); for (String permission : permissions) { int check = ContextCompat.checkSelfPermission(getActivity(), permission); if (check == PackageManager.PERMISSION_GRANTED) { //授权通过了已经 do nothing } else { deniedPermissions.add(permission); } } if (deniedPermissions.size() != 0) { //有权限没有通过 requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), PERMISSION_REQUEST_CODE); } else { //授权全部通过 } 这样我们就完成了第一步和第二步。
-
对授权结果进行处理
对于调用者来说,最佳的体验就是回调的方式反馈授权结果。先定义一个回调接口。
public interface FanPermissionListener { /* * 授权全部通过 */ void permissionRequestSuccess(); /* * 授权未通过 * @param grantedPermissions 已通过的权限 * @param deniedPermissions 拒绝的权限 * @param forceDeniedPermissions 永久拒绝的权限(也就是用户点击了不再提醒的那些权限) */ void permissionRequestFail(String[] grantedPermissions, String[] deniedPermissions, String[] forceDeniedPermissions); }
调用者在构造FanPermissionUtils的时候,设置回调监听即可。
FanPermissionUtils.with(MainActivity.this) //添加权限申请回调监听 如果申请失败 会返回已申请成功的权限列表,用户拒绝的权限列表和用户点击了不再提醒的永久拒绝的权限列表 .setPermissionsCheckListener(new FanPermissionListener() { @Override public void permissionRequestSuccess() { //所有权限授权成功才会回调这里 } @Override public void permissionRequestFail(String[] grantedPermissions, String[] deniedPermissions, String[] forceDeniedPermissions) { //当有权限没有被授权就会回调这里 } });
我们在FanPermissionUtils中会保存调用者设置的回调接口,然后在FanPermissionFragment被构造的时候将这个回调接口传给FanPermissionFragment。
FanPermissionFragment.newInstance(permissions).setPermissionCheckListener(listener).start(mContext);
然后在FanPermissionFragment中去处理申请授权之后的授权回调操作。将授权回调的结果通过调用者设置的回调监听返回给调用者。来实现侵入性最小的权限申请方案。
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { //记录点击了不再提醒的未授权权限 List
forceDeniedPermissions = new ArrayList<>(); //记录点击了普通的未授权权限 List normalDeniedPermissions = new ArrayList<>(); List grantedPermissions = new ArrayList<>(); for (int i = 0; i < grantResults.length; i++) { int grantResult = grantResults[i]; String permission = permissions[i]; if (grantResult == PackageManager.PERMISSION_GRANTED) { //授权通过 grantedPermissions.add(permission); } else { //授权拒绝 if (!ActivityCompat.shouldShowRequestPermissionRationale(mContext, permission)) { //用户勾选了不再提示 forceDeniedPermissions.add(permission); } else { //用户仅仅点击了拒绝 未勾选不再提示 normalDeniedPermissions.add(permission); } } } if (forceDeniedPermissions.size() == 0 && normalDeniedPermissions.size() == 0) { //全部授权通过 requestPermissionsSuccess(); } else { //授权未通过 for (String permission : this.permissions) { if (grantedPermissions.contains(permission) || normalDeniedPermissions.contains(permission) || forceDeniedPermissions.contains(permission)) { } else { //如果三者都不包含他 包名这个权限不是隐私权限 直接给就完事了 所以要放到已授权的权限列表里面去 grantedPermissions.add(permission); } } requestPermissionsFail(grantedPermissions.toArray(new String[grantedPermissions.size()]), normalDeniedPermissions.toArray(new String[normalDeniedPermissions.size()]), forceDeniedPermissions.toArray(new String[forceDeniedPermissions.size()])); } } } //授权全部通过 private void requestPermissionsSuccess() { if (permissionCheckListener != null) { permissionCheckListener.permissionRequestSuccess(); } mContext.getFragmentManager().beginTransaction().remove(this).commit(); } //授权未通过 private void requestPermissionsFail(String[] grantedPermissions, String[] deniedPermissions, String[] forceDeniedPermissions) { if (permissionCheckListener != null) { permissionCheckListener.permissionRequestFail(grantedPermissions, deniedPermissions, forceDeniedPermissions); } mContext.getFragmentManager().beginTransaction().remove(this).commit(); }
畅想优化
此外还有一种需求,当用户不授权,就不给用。这种情况下我们需要在申请权限的时候,当用户点击拒绝的时候,一直无限申请权限,直到用户点击允许为止,对,就是这么流氓!
细心的同学在上面的代码中已经看到了一些逻辑,对应的就是另外一种一种情况,用户在你连续弹出几次申请权限的弹窗之后,会选择勾选不再提醒,这个时候你再去申请权限的时候,系统会直接回调给你拒绝,并且不会弹出授权弹窗,这个时候我们需要调用ActivityCompat.shouldShowRequestPermissionRationale(mContext, permission)方法来判断下用户是否勾选了不再提醒的按钮,如果用户勾选了,那么我们就需要指引用户去设置页面手动授权,因为我们在app内部已经无能为力了。
if (normalDeniedPermissions.size() != 0) {
//还有普通拒绝的权限可以弹窗
requestPermission();
} else {
//所有没有通过的权限都是用户点击了不再提示的 我擦 这里本来是想把未授权的所有权限的名称列出来展示的 后来想想觉得配置有点麻烦
new AlertDialog.Builder(mContext)
.setTitle(mContext.getString(R.string.permissions_check_warn))
.setMessage(checkConfig == null ? forceDeniedPermissionTips : checkConfig.getForceDeniedPermissionTips())
.setCancelable(false)
.setPositiveButton(mContext.getString(R.string.permissions_check_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
openSettingPage();
}
}).show();
}
牛逼回吹
之前不是说一行代码实现吗?下面就是整个申请权限的功能,的确只有一个结尾的分号,我说一行不算吹牛吧!~~~
FanPermissionUtils.with(MainActivity.this)
//添加所有你需要申请的权限
.addPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.addPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
.addPermissions(Manifest.permission.CALL_PHONE)
.addPermissions(Manifest.permission.ACCESS_WIFI_STATE)
.addPermissions(Manifest.permission.CAMERA)
//添加权限申请回调监听 如果会返回已申请成功的权限列表,用户拒绝的权限列表和用户点击了不再提醒的永久列表
.setPermissionsCheckListener(new FanPermissionListener() {
@Override
public void permissionRequestSuccess() {
//所有权限授权成功才会回调这里
((TextVfindViewById(R.id.tv_result)).setText("授权结果\n\n所有权限都授权成;
Toast.makeText(MainActivity.this, "所有权限都授权Toast.LENGTH_SHORT).show();
@Override
public void permissionRequestFail(String[] grantedPermissions, StrideniedPermissions, String[] forceDeniedPermissions) {
//当有权限没有被授权就会回调这里
StringBuilder result = new StringBuilder("授权结果\n\n授权失败\n\n");
result.append("授权通过的权限:\n");
for (String grantedPermission : grantedPermissions) {
result.append(grantedPermission + "\n");
}
result.append("\n临时拒绝的权限:\n");
for (String deniedPermission : deniedPermissions) {
result.append(deniedPermission + "\n");
}
result.append("\n永久拒绝的权限:\n");
for (String forceDeniedPermission : forceDeniedPermissions) {
result.append(forceDeniedPermission + "\n");
}
((TextView) findViewById(R.id.tv_result)).setText(result);
Toast.makeText(MainActivity.this, "授权失败", Toast.LENGTH_SHORT).show();
}
})
//开始授权
.startCheckPermission();
结语
首页,链接是 https://www.jianshu.com/u/123f97613b86
掘金首页,链接是 https://juejin.im/user/5838d57fac502e006c1708bc
Github首页,链接是 https://github.com/MZCretin
CSDN首页,链接是 http://blog.csdn.net/u010998327
我是Cretin,一个可爱的小男孩。