一、前言
最近发布APP到应用商店,因为申请权限时没有相关说明被打回,参考了京东、UC等APP,写了一套申请权限框架:RequestPermission
RequestPermission:Android申请权限库,链式调用,权限结果回调,使用起来更优雅。
二、剖析京东、UC申请权限过程
我们先看京东和UC APP申请权限的过程:
| 京东 | UC |
|---|---|
京东、UC申请权限流程如下:
京东、UC申请权限,会把申请权限这个过程托管给一个中介activity,这个中介activity向系统申请权限,再把权限结果返回给最初的activity。 把中介activity的主题设置为透明,activity之间转场动画也去掉,所以看上去和直接使用系统方法申请权限一样,弹出系统弹窗,看起来好像没有新开页面。
有了透明的中介activity,我们可操作的东西就来了:
- 可以在透明的中介activity上半部分,放置一个文本,用来展示为什么要获取当前权限,这样用户基本都会同意。就如上面的京东和UC申请权限一样。
- 由于有了中介activity,我们就可以把申请权限这个过程,封装为request-response的形式,链式调用,结果回调,使用起来更优雅。
三、使用RequestPermission库申请权限
基于上面对京东和UC的分析,写了一套权限申请权限框架:RequestPermission
GitHub地址:github.com/jinxiyang/R…
1、依赖
implementation 'io.github.jinxiyang:requestpermission:0.0.2'
2、简单的使用介绍
a)、不使用中转activity,申请权限
//请求定位权限
PermissionRequester(this)
.addPermissions(PermissionUtils.LOCATION_PERMISSIONS)
.request {
Toast.makeText(this@MainActivity, "申请定位权限:${it.granted()}", Toast.LENGTH_SHORT).show()
}
b)、使用中转activity,申请权限
//请求定位权限
PermissionRequester(this)
.addPermissions(PermissionUtils.LOCATION_PERMISSIONS)
.requestGlobal {
Toast.makeText(this@MainActivity, "申请定位权限:${it.granted()}", Toast.LENGTH_SHORT).show()
}
c)、使用中转activity,申请权限,同时显示权限说明
//统一权限页面:仿京东,页面顶部显示权限提示,背景透明
private fun jdRequestPermission() {
//定义申请权限组时的提示文字,更友好,不会被工信部点名胡乱获取用户隐私。
//京东第一安装,打开首页相机时,会有这样的权限申请页,并在页面背景提示相关信息
//相机权限对应的提示
val cameraExtra = Bundle()
cameraExtra.putString(RequestPermissionActivityJD.KEY_PERMISSION_TITLE, "相机权限使用说明")
cameraExtra.putString(RequestPermissionActivityJD.KEY_PERMISSION_DESC, "京东正在向您获取“相机”权限,同意后," +
"你可以使用扫码服务,巴拉巴拉……")
PermissionRequester(this)
.addPermissionGroup(PermissionUtils.CAMERA_PERMISSIONS.toList(), cameraExtra)
//设置自定义仿京东请求权限页面 RequestPermissionActivityJD,背景透明
.requestGlobal(RequestPermissionActivityJD::class.java) {
val camera = it.granted(PermissionUtils.CAMERA_PERMISSIONS)
Toast.makeText(this@MainActivity, "申请相机权限:$camera", Toast.LENGTH_SHORT).show()
}
}
效果如图:
四、使用RequestPermission的好处
1、链式调用,结果回调listener,请求权限和结果回调代码写在一块儿,高内聚低耦合。不用在activity里重写权限结果回调方法onRequestPermissionsResult()
2、使用了ActivityResultContracts机制,发起registerForActivityResult,所以在结果回调listener时,activity处于可见状态,即:activity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
这时如果显示对话框,DialogFragment.show() 没有这个问题了:java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
五、RequestPermission的技术细节
申请权限的相关代码,没什么难度,就是一些代码的封装,大家都会写,这里不提了。
这里简单介绍一下如何实现链式调用、中介activity的透明主题的配置
1、实现链式调用
activity请求权限时,我们使用一个没有视图的fragment代理发起请求,在这个fragment内包装了以下两个功能:
-
对于不使用中转activity,申请权限时,该fragment包装了请求权限,即:
ActivityResultContracts.RequestMultiplePermissions() -
对于使用中转activity,申请权限时,该fragment包装了与中转activity的通信,即
ActivityResultContracts.StartActivityForResult()
这样对于需要权限的activity就不用重写onRequestPermissionsResult()和
onActivityResult()。
归根结底是对ActivityResultContracts机制的封装,封装成接口回调形式。
有兴趣的可以看看下面两个类的源码:
/**
* 对ActivityResultContracts机制进行封装,配合ActivityResultContractsFragment一起使用
*
*
* 避免这个问题:
* 在onCreate预先定义了Launcher,如果不定义,launcher.launch()时会报错:
* LifecycleOwner xx is attempting to register while current state is RESUMED.
* LifecycleOwners must call register before they are STARTED.
*
*/
object ActivityResultContractsHelper {
private const val FRAGMENT_TAG = "ActivityResultContractsFragment"
//封装了startActivityForResult,结果listener回调
@JvmStatic
fun startActivityForResult(fm: FragmentManager, intent: Intent, onStartActivityForResultListener: OnStartActivityForResultListener) {
val fragment = fm.findFragmentByTag(FRAGMENT_TAG)
if (fragment is ActivityResultContractsFragment) {
fragment.setOnStartActivityForResultListener(onStartActivityForResultListener)
fragment.startActivityForResult(intent)
} else {
val activityResultFragment = ActivityResultContractsFragment.newInstance(intent)
activityResultFragment.setOnStartActivityForResultListener(onStartActivityForResultListener)
fm.beginTransaction()
.add(activityResultFragment, FRAGMENT_TAG)
.commitNowAllowingStateLoss()
}
}
//封装了requestMultiplePermissions,权限结果listener回调
@JvmStatic
fun requestMultiplePermissions(fm: FragmentManager, permissions: Array<String>, onStartActivityForResultListener: OnRequestMultiPermissionListener) {
val fragment = fm.findFragmentByTag(FRAGMENT_TAG)
if (fragment is ActivityResultContractsFragment) {
fragment.setOnRequestMultiPermissionListener(onStartActivityForResultListener)
fragment.requestMultiplePermissions(permissions)
} else {
val activityResultFragment = ActivityResultContractsFragment.newInstance(permissions = permissions)
activityResultFragment.setOnRequestMultiPermissionListener(onStartActivityForResultListener)
fm.beginTransaction()
.add(activityResultFragment, FRAGMENT_TAG)
.commitNowAllowingStateLoss()
}
}
}
下面是没有UI的fragment的源码,封装了ActivityResultContracts机制:
/**
* 没有UI的fragment,用于发起registerForActivityResult
*
* 封装了ActivityResultContracts机制,在onCreate预先定义了Launcher,需要使用时,直接launcher.launch()
*/
class ActivityResultContractsFragment : Fragment() {
private lateinit var mStartActivityForResultLauncher: ActivityResultLauncher<Intent>
private lateinit var mRequestMultiplePermissionsLauncher: ActivityResultLauncher<Array<String>>
private val mStartActivityForResultLiveData = MutableLiveData<Intent>()
private val mRequestMultiplePermissionsLiveData = MutableLiveData<Array<String>>()
private var mOnStartActivityForResultListener: OnStartActivityForResultListener? = null
private var mOnRequestMultiPermissionListener: OnRequestMultiPermissionListener? = null
private val activityOptionsCompat: ActivityOptionsCompat by lazy {
//页面转场无动画
ActivityOptionsCompat.makeCustomAnimation(requireContext(), 0, 0)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//在onCreate预先定义了Launcher,如果不定义,launcher.launch()时会报错:
//LifecycleOwner xx is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
val startActivityForResult = StartActivityForResult()
mStartActivityForResultLauncher = registerForActivityResult(startActivityForResult) {
mOnStartActivityForResultListener?.onActivityResult(it.resultCode, it.data)
}
val requestMultiplePermissions = ActivityResultContracts.RequestMultiplePermissions()
mRequestMultiplePermissionsLauncher = registerForActivityResult(requestMultiplePermissions) {
mOnRequestMultiPermissionListener?.onRequestMultiPermission(it)
}
val args = arguments
val intent = args?.getParcelable<Intent>("intent")
if (intent != null) {
args.putParcelable("intent", null)
startActivityForResult(intent)
}
mStartActivityForResultLiveData.observe(this) {
mStartActivityForResultLauncher.launch(it, activityOptionsCompat)
}
val permissions: Array<String>? = args?.getStringArray("permissions")
if (permissions != null) {
args.putStringArray("permissions", null)
requestMultiplePermissions(permissions)
}
mRequestMultiplePermissionsLiveData.observe(this) {
mRequestMultiplePermissionsLauncher.launch(it)
}
}
fun startActivityForResult(intent: Intent) {
mStartActivityForResultLiveData.postValue(intent)
}
fun requestMultiplePermissions(permissions: Array<String>) {
mRequestMultiplePermissionsLiveData.postValue(permissions)
}
fun setOnStartActivityForResultListener(onStartActivityForResultListener: OnStartActivityForResultListener?) {
this.mOnStartActivityForResultListener = onStartActivityForResultListener
}
fun setOnRequestMultiPermissionListener(onRequestMultiPermissionListener: OnRequestMultiPermissionListener?) {
this.mOnRequestMultiPermissionListener = onRequestMultiPermissionListener
}
companion object {
fun newInstance(intent: Intent? = null, permissions: Array<String>? = null): ActivityResultContractsFragment {
val args = Bundle()
args.putParcelable("intent", intent)
args.putStringArray("permissions", permissions)
val fragment = ActivityResultContractsFragment()
fragment.arguments = args
return fragment
}
}
}
2、中介activity的透明主题的配置
在清单文件给activity设置透明主题时,在android 8.0/8.1 (即v26/27),则会报这个错误:
java.lang.RuntimeException: Unable to start activity ComponentInfo java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
意思是:透明的activity不允许在清单文件设置旋转方向 android:screenOrientation
我们有两种解决方案:
a)、第一种(推荐):在AndroidManifest.xml不为activity设置旋转方向这个属性:android:screenOrientation
AndroidManifest.xml配置透明主题:
<activity
android:name=".GlobalRequestPermissionActivity"
android:exported="false"
android:configChanges="orientation|screenSize"
android:theme="@style/RequestTransparentTheme.TransparentTheme"
/>
style.xml透明主题:
<style name="RequestTransparentTheme.TransparentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
b)、第二种:android 8.0/8.1 (即v26/27)时,主题设置为不透明背景,其他系统版本设置透明主题:
AndroidManifest.xml配置主题:
<activity
android:name=".GlobalRequestPermissionActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/RequestTransparentTheme.OrientationTransparentTheme"
/>
RequestTransparentTheme.OrientationTransparentTheme根据系统版本,在android 8.0/8.1 (即v26/27)时,主题设置为不透明背景,其他系统版本设置透明主题
values文件夹下的style.xml(系统小于android 8.0),透明主题:
<style name="RequestTransparentTheme.OrientationTransparentTheme" parent="RequestTransparentTheme.TransparentTheme"/>
values-v26文件夹下的style.xml(系统android 8.0/8.1),不透明主题:
<style name="RequestTransparentTheme.OrientationTransparentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">@android:color/white</item>
<item name="android:windowBackground">@android:color/white</item>
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowDisablePreview">true</item>
</style>
values-v28文件夹下的style.xml(系统大于android 8.1),透明主题:
<!-- Android 9.0及以上 透明activity主题 -->
<style name="RequestTransparentTheme.OrientationTransparentTheme" parent="RequestTransparentTheme.TransparentTheme"/>
PS.不需要在新建values-v27(系统android 8.1)