Android权限使用PermissionX

一、前言:

在开始介绍PermissionX的具体用法之前,我们先来讨论一下它的实现原理。

其实之前并不是没有人尝试过对运行时权限处理进行封装,我之前在做直播公开课的时候也向大家演示过一种运行时权限API的封装过程。

但是,想要对运行时权限的API进行封装并不是一件容易的事,因为这个操作是有特定的上下文依赖的,一般需要在Activity中接收onRequestPermissionsResult()方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。

为此,也衍生出了一系列特殊的封装方案,比如将运行时权限的操作封装到BaseActivity中,或者提供一个透明的Activity来处理运行时权限等。

不过上述两种方案都不够轻量,因为改变Activity的继承结构这可是大事情,而提供一个透明的Activty则需要在AndroidManifest.xml中进行额外的声明。

现在,业内普遍比较认可使用另外一种小技巧来进行实现。是什么小技巧呢?回想一下,之前所有申请运行时权限的操作都是在Activity中进行的,事实上,Android在Fragment中也提供了一份相同的API,使得我们在Fragment中也能申请运行时权限。

但不同的是,Fragment并不像Activity那样必须有界面,我们完全可以向Activity中添加一个隐藏的Fragment,然后在这个隐藏的Fragment中对运行时权限的API进行封装。这是一种非常轻量级的做法,不用担心隐藏Fragment会对Activity的性能造成什么影响。

这就是PermissionX的实现原理了,书中其实也已经介绍过了这部分内容。但是,在其实现原理的基础之上,后期我又增加了很多新功能,让PermissionX变得更加强大和好用,下面我们就来学习一下PermissionX的具体用法。

二、基本用法

1、引入jar包

要使用PermissionX之前,首先需要将其引入到项目当中,如下所示:

    //权限
    implementation 'com.permissionx.guolindev:permissionx:1.4.0'

我在写本篇文章时PermissionX的最新版本是1.4.0,想要查看它的当前最新版本,请访问PermissionX的主页:https://github.com/guolindev/PermissionX

PermissionX的目的是为了让运行时权限处理尽可能的容易,因此怎么让API变得简单好用就是我优先要考虑的问题。

比如同样实现拨打电话的功能,使用PermissionX只需要这样写:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        makeCallBtn.setOnClickListener {
            PermissionX.init(this)
                .permissions(Manifest.permission.CALL_PHONE)
                .request { allGranted, grantedList, deniedList ->
                    if (allGranted) {
                        call()
                    } else {
                        Toast.makeText(this, "您拒绝了拨打电话权限", Toast.LENGTH_SHORT).show()
                    }
                }
        }
    }
    ...
}

是的,PermissionX的基本用法就这么简单。首先调用init()方法来进行初始化,并在初始化的时候传入一个FragmentActivity参数。由于AppCompatActivity是FragmentActivity的子类,所以只要你的Activity是继承自AppCompatActivity的,那么直接传入this就可以了。

接下来调用permissions()方法传入你要申请的权限名,这里传入CALL_PHONE权限。你也可以在permissions()方法中传入任意多个权限名,中间用逗号隔开即可。

最后调用request()方法来执行权限申请,并在Lambda表达式中处理申请结果。可以看到,Lambda表达式中有3个参数:allGranted表示是否所有申请的权限都已被授权,grantedList用于记录所有已被授权的权限,deniedList用于记录所有被拒绝的权限。

因为我们只申请了一个CALL_PHONE权限,因此这里直接判断:如果allGranted为true,那么就调用call()方法,否则弹出一个Toast提示。

运行结果如下:


20200517221351551.gif

2、核心用法

然而我们目前还只是处理了最普通的场景,刚才提到的,假如用户拒绝了某个权限,在下次申请之前,我们最好弹出一个对话框来向用户解释申请这个权限的原因,这个又该怎么实现呢?

别担心,PermissionX对这些情况进行了充分的考虑。

onExplainRequestReason()方法可以用于监听那些被用户拒绝,而又可以再次去申请的权限。从方法名上也可以看出来了,应该在这个方法中解释申请这些权限的原因。

而我们只需要将onExplainRequestReason()方法串接到request()方法之前即可,如下所示:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

这种情况下,所有被用户拒绝的权限会优先进入onExplainRequestReason()方法进行处理,拒绝的权限都记录在deniedList参数当中。接下来,我们只需要在这个方法中调用showRequestReasonDialog()方法,即可弹出解释权限申请原因的对话框,如下所示:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
        showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

showRequestReasonDialog()方法接受4个参数:第一个参数是要重新申请的权限列表,这里直接将deniedList参数传入。第二个参数则是要向用户解释的原因,我只是随便写了一句话,这个参数描述的越详细越好。第三个参数是对话框上确定按钮的文字,点击该按钮后将会重新执行权限申请操作。第四个参数是一个可选参数,如果不传的话相当于用户必须同意申请的这些权限,否则对话框无法关闭,而如果传入的话,对话框上会有一个取消按钮,点击取消后不会重新进行权限申请,而是会把当前的申请结果回调到request()方法当中。

另外始终要记得将所有申请的权限都在AndroidManifest.xml中进行声明:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissionx.app">

    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    ...
</manifest>

重新运行一下程序,效果如下图所示:


222222.gif

当前版本解释权限申请原因对话框的样式还无法自定义,1.3.0版本当中已支持了自定义权限提醒对话框样式的功能,详情请参阅 PermissionX重磅更新,支持自定义权限提醒对话框 。

当然,我们也可以指定要对哪些权限重新申请,比如上述申请的3个权限中,我认为CAMERA权限是必不可少的,而其他两个权限则可有可无,那么在重新申请的时候也可以只申请CAMERA权限:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION)
    .onExplainRequestReason { deniedList ->
        val filteredList = deniedList.filter {
            it == Manifest.permission.CAMERA
        }
        showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

这样当再次申请权限的时候就只会申请CAMERA权限,剩下的两个权限最终会被传入到request()方法的deniedList参数当中。

解决了向用户解释权限申请原因的问题,接下来还有一个头疼的问题要解决:如果用户不理会我们的解释,仍然执意拒绝权限申请,并且还选择了拒绝且不再询问的选项,这该怎么办?通常这种情况下,程序层面已经无法再次做出权限申请,唯一能做的就是提示用户到应用程序设置当中手动打开权限。

那么PermissionX是如何处理这种情况的呢?我相信绝对会给你带来惊喜。PermissionX中还提供了一个onForwardToSettings()方法,专门用于监听那些被用户永久拒绝的权限。另外从方法名上就可以看出,我们可以在这里提醒用户手动去应用程序设置当中打开权限。代码如下所示:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
        showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .onForwardToSettings { deniedList ->
        showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

可以看到,这里又串接了一个onForwardToSettings()方法,所有被用户选择了拒绝且不再询问的权限都会进行到这个方法中处理,拒绝的权限都记录在deniedList参数当中。

接下来,你并不需要自己弹出一个Toast或是对话框来提醒用户手动去应用程序设置当中打开权限,而是直接调用showForwardToSettingsDialog()方法即可。类似地,showForwardToSettingsDialog()方法也接收4个参数,每个参数的作用和刚才的showRequestReasonDialog()方法完全一致,我这里就不再重复解释了。

showForwardToSettingsDialog()方法将会弹出一个对话框,当用户点击对话框上的我已明白按钮时,将会自动跳转到当前应用程序的设置界面,从而不需要用户自己慢慢进入设置当中寻找当前应用了。另外,当用户从设置中返回时,PermissionX将会自动重新请求相应的权限,并将最终的授权结果回调到request()方法当中。效果如下图所示:


333333.gif

同样,1.3.0版本也支持了自定义这个对话框样式的功能,详情请参阅 PermissionX重磅更新,支持自定义权限提醒对话框

3、更多用法

PermissionX最主要的功能大概就是这些,不过我在使用一些App的时候发现,有些App喜欢在第一次请求权限之前就先弹出一个对话框向用户解释自己需要哪些权限,然后才会进行权限申请。这种做法是比较提倡的,因为用户同意授权的概率会更高。

那么PermissionX中要如何实现这样的功能呢?

其实非常简单,PermissionX还提供了一个explainReasonBeforeRequest()方法,只需要将它也串接到request()方法之前就可以了,代码如下所示:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
        showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
    }
    .onForwardToSettings { deniedList ->
        showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

这样,当每次请求权限时,会优先进入onExplainRequestReason()方法,弹出解释权限申请原因的对话框,用户点击我已明白按钮之后才会执行权限申请。效果如下图所示:

444444.gif

不过,你在使用explainReasonBeforeRequest()方法时,其实还有一些关键的点需要注意。

第一,单独使用explainReasonBeforeRequest()方法是无效的,必须配合onExplainRequestReason()方法一起使用才能起作用。这个很好理解,因为没有配置onExplainRequestReason()方法,我们怎么向用户解释权限申请原因呢?

第二,在使用explainReasonBeforeRequest()方法时,如果onExplainRequestReason()方法中编写了权限过滤的逻辑,最终的运行结果可能和你期望的会不一致。这一点可能会稍微有点难理解,我用一个具体的示例来解释一下。

观察如下代码:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
        val filteredList = deniedList.filter {
            it == Manifest.permission.CAMERA
        }
        showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    ...

这里在onExplainRequestReason()方法中编写了刚才用到的权限过滤逻辑,当有多个权限被拒绝时,我们只重新申请CAMERA权限。

在没有加入explainReasonBeforeRequest()方法时,一切都可以按照我们所预期的那样正常运行。但如果加上了explainReasonBeforeRequest()方法,在执行权限请求之前会先进入onExplainRequestReason()方法,而这里将除了CAMERA之外的其他权限都过滤掉了,因此实际上PermissionX只会请求CAMERA这一个权限,剩下的权限将完全不会尝试去请求,而是直接作为被拒绝的权限回调到最终的request()方法当中。

效果如下图所示:


55555.gif

针对于这种情况,PermissionX在onExplainRequestReason()方法中提供了一个额外的beforeRequest参数,用于标识当前上下文是在权限请求之前还是之后,借助这个参数在onExplainRequestReason()方法中执行不同的逻辑,即可很好地解决这个问题,示例代码如下:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList, beforeRequest ->
        if (beforeRequest) {
            showRequestReasonDialog(deniedList, "为了保证程序正常工作,请您同意以下权限申请", "我已明白")
        } else {
            val filteredList = deniedList.filter {
                it == Manifest.permission.CAMERA
            }
            showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
        }
    }
    ...

可以看到,当beforeRequest为true时,说明此时还未执行权限申请,那么我们将完整的deniedList传入showRequestReasonDialog()方法当中。

而当beforeRequest为false时,说明某些权限被用户拒绝了,此时我们只重新申请CAMERA权限,因为它是必不可少的,其他权限则可有可无。


66666.gif

参考:https://blog.csdn.net/guolin_blog/article/details/106181780

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容