Android | 权限申请与前置说明弹窗同时展示的优雅方案

引言:权限申请的痛点

在 Android 应用开发中,权限申请是必不可少的环节。如果直接申请权限弹窗,显得有点突兀,用户不了解为何需要此权限,所以通常在申请权限之前会有个说明弹窗,当用户同意之后再去弹系统权限的弹窗。然后这种方案也有缺点,就是每次申请权限都需要2个弹窗:说明弹窗+系统权限弹窗,不过现在主流App的方案都是将这两个弹窗合二为一了,说明弹窗和系统权限弹窗同时展示,比如: 本文就实现一下这种效果。

权限申请流程优化

优化后流程:

复制代码
用户点击功能 → 展示自定义说明 → 自动触发系统弹窗 → 处理结果

效果图

示例代码

以相机权限为例,在Fragment中申请,核心代码如下:

kotlin 复制代码
/**
 * 权限申请时,自定义顶部TIPS
 */
class PermissionRequestFragment : BaseFragment() {
    private val btnPermission: Button by id(R.id.btn_permission_request)

    private val requestCameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            handleCameraPermissionResult(isGranted)
        }

    override fun getLayoutId(): Int {
        return R.layout.layout_permission_request
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        btnPermission.setOnClickListener { requestPermission() }
    }

    private fun handleCameraPermissionResult(isGranted: Boolean) {
        removeTopTipsView(requireActivity())
        PermissionUtils.setCameraPermissionRequested(requireActivity(), true)

        log("isGranted:$isGranted")
        if (isGranted) {
            //权限授予成功,打开相机
            showToast("权限授予成功,打开相机")
        } else {
            //权限被拒绝,检查是否永久拒绝
            val shouldShowRationale = shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)

            if (!shouldShowRationale) {
                //用户选择了"不再询问",属于永久拒绝
                PermissionUtils.setCameraPermissionRequested(requireActivity(), false)
                showGoToSettingsDialog()
            } else {
                //临时拒绝
                showToast("相机权限被拒绝,无法使用拍照功能")
            }
        }
    }

    private fun showGoToSettingsDialog() {
        AlertDialog.Builder(requireActivity())
            .setTitle("相机权限被永久拒绝")
            .setMessage("相机权限已被永久拒绝,请到应用设置中手动开启权限。")
            .setPositiveButton("去设置") { _, _ ->
                openAppSettings()
            }
            .setNegativeButton("取消", null)
            .setCancelable(false)
            .show()
    }

    /**
     * 打开应用设置页面
     */
    private fun openAppSettings() {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
        intent.data = "package:${requireActivity().packageName}".toUri()
        startActivity(intent)
    }

    private fun requestPermission() {
        if (hasCameraPermission()) {
            showToast("已经有对应权限了")
            return
        }
        //展示tips
        if (shouldShowPermissionTips()) {
            addTopTipsView(requireActivity())
        }
        requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
    }

    /**
     * 判断是否应该显示权限提示 tips
     * 返回 true 的情况:用户第一次请求权限,或者之前只是临时拒绝
     * 返回 false 的情况:
     * 1. 已经有权限
     * 2. 用户永久拒绝了权限(选择了"不再询问")
     * 3. 其他不应该展示的情况
     */
    private fun shouldShowPermissionTips(): Boolean {
        //如果已经有权限,不需要提示
        if (hasCameraPermission()) {
            return false
        }
        //判断用户之前是否请求过权限
        val hasBeenRequestedBefore = PermissionUtils.hasCameraPermissionBeenRequested(requireActivity())
        if (!hasBeenRequestedBefore) {
            return true
        }

        // 检查是否需要显示权限说明
        // 注意:shouldShowRequestPermissionRationale() 在以下情况返回 false:
        //   - 第一次请求权限
        //   - 用户永久拒绝(选择了"不再询问")
        //   - 用户已经授予权限
        // 只有在用户临时拒绝时返回 true
        val shouldShowRationale = shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)

        // 如果shouldShowRationale为true,说明用户之前临时拒绝过,现在再次请求,这种情况下可以展示提示
        return shouldShowRationale
    }


    private fun hasCameraPermission(): Boolean {
        return ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
    }

    /**
     * 顶部展示权限说明Tips
     */
    private fun addTopTipsView(activity: FragmentActivity) {
        runCatching {
            val decorView = activity.window.decorView
            (decorView as? ViewGroup)?.let { viewGroup ->
                //检查是否已经存在tips,通过Tag查找
                val existingView = viewGroup.findViewWithTag<TextView>(CAMERA_PERMISSION_TIPS_TAG)
                if (existingView != null) return@let

                val textView = TextView(activity)
                textView.run {
                    tag = CAMERA_PERMISSION_TIPS_TAG
                    setBackgroundResource(R.drawable.shape_black_bg)
                    setTextColor(Color.WHITE)
                    typeface = Typeface.DEFAULT_BOLD
                    text = "请相机权限:"为了给您提供'拍照搜题'服务,需要申请使用您的相机权限。我们承诺仅用于此功能,保障您的隐私安全。""
                    textSize = 16f
                    setLineSpacing(2.dp2px().toFloat(), 1f)
                    val dpLength = 12.dp2px()
                    setPadding(dpLength, 18.dp2px(), dpLength, 18.dp2px())
                    val layoutParams = FrameLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                    layoutParams.leftMargin = dpLength
                    layoutParams.rightMargin = dpLength
                    layoutParams.topMargin = dpLength * 3
                    decorView.addView(textView, layoutParams)
                }
            }
        }
    }

    /**
     * 删除Tips
     */
    private fun removeTopTipsView(activity: FragmentActivity) {
        runCatching {
            val decorView = activity.window.decorView
            (decorView as? ViewGroup)?.let { viewGroup ->
                //通过Tag精确查找并移除
                val tipsView = viewGroup.findViewWithTag<TextView>(CAMERA_PERMISSION_TIPS_TAG)
                if (tipsView != null) {
                    viewGroup.removeView(tipsView)
                }
            }
        }
    }

    companion object {
        private const val CAMERA_PERMISSION_TIPS_TAG = "camera_permission_tips_tag"
    }

}

object PermissionUtils {
    private const val PREFS_NAME = "app_permissions_prefs"
    private const val KEY_CAMERA_REQUESTED_ONCE = "camera_requested_once"

    private fun getPrefs(context: Context): SharedPreferences {
        return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
    }

    // 标记相机权限已经被请求过
    fun setCameraPermissionRequested(context: Context, status: Boolean) {
        getPrefs(context).edit { putBoolean(KEY_CAMERA_REQUESTED_ONCE, status) }
    }

    //检查相机权限是否被请求过(用于区分"第一次"和"永久拒绝")
    fun hasCameraPermissionBeenRequested(context: Context): Boolean {
        return getPrefs(context).getBoolean(KEY_CAMERA_REQUESTED_ONCE, false)
    }
}

上述代码中的解释已经很详细了,主要是在弹系统弹窗的同时需要展示我们自己的说明弹窗,通过addTopTipsView()展示说明弹窗,其方法内部是通过activity.window.decorView找到页面的根布局,通过addView将说明弹窗展示出来;当权限申请结束时通过removeTopTipsView()删除说明弹窗即可。

相关推荐
xiangpanf13 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx16 小时前
安卓线程相关
android
消失的旧时光-194317 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon17 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon18 小时前
VSYNC 信号完整流程2
android
dalancon18 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138419 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android19 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才20 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶20 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle