禁止 Android 的状态栏(Status bar)下拉,是一个常见的定制需求,通常是通过修改系统源码实现。
如果不具备修改系统源码的条件,有没有纯应用层的替代方案呢?
有!但有局限。
这个方案参考了 Stack Overflow 上的一个问答:How to disable status bar click and pull down in Android?
思路就是:通过 WindowManager 在状态栏上添加一个相同宽高尺寸的透明遮盖层,并拦截掉相关触摸事件。
private fun preventStatusBarExpansion(context: Context) {
val manager = context.applicationContext
.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val activity = context as Activity
val localLayoutParams = WindowManager.LayoutParams()
// 在Android 8.0及以上的系统中已经无效了~
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
localLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
localLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
}
localLayoutParams.gravity = Gravity.TOP
localLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
// this is to enable the notification to recieve touch events
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
// Draws over status bar
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
localLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT
val resId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")
var result = 0
if (resId > 0) {
result = activity.resources.getDimensionPixelSize(resId)
}
localLayoutParams.height = result
localLayoutParams.format = PixelFormat.TRANSPARENT
val view = CustomViewGroup(context)
manager.addView(view, localLayoutParams)
}
class CustomViewGroup(context: Context) : ViewGroup(context) {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
// 拦截掉触摸事件
return true
}
}
使用时需要注意对添加 window 权限的判断。
SYSTEM_ALERT_WINDOW
权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
manifest>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
// 打开系统设置页面引导用户授权
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
} else {
preventStatusBarExpansion(this)
}
} else {
preventStatusBarExpansion(this)
}
}
如果应用没有在其他应用之上绘制内容的权限,直接调用了 preventStatusBarExpansion()
,运行将抛出 permission denied for window type 的异常
Caused by: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@c70056a -- permission denied for window type 20xx
at android.view.ViewRootImpl.setView(ViewRootImpl.java:703)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
如果是全屏的应用,类似视频应用的全屏观看页面那种,可以配合设置全屏的相关Flag SYSTEM_UI_FLAG_XX
来实现:
override fun setContentView(view: View?) {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
super.setContentView(view)
}
一个小缺陷就是:仍然可以从屏幕边缘划出状态栏。不过划出后的状态栏是不能操作的,过一会儿也会自动隐藏,无伤大雅。
Android 8.0 Behavior Changes - Common window types for alert windows
在Android 8.0的变更描述中已经提到,使用了 SYSTEM_ALERT_WINDOW
权限的应用不能再使用下面这些type:
而是必须用 TYPE_APPLICATION_OVERLAY
这个新的type来代替。如果在 8.0 以上系统中继续使用上面的类型,还是会抛出 permission denied for window type 的异常。
但经过测试,将 type 设置成 TYPE_APPLICATION_OVERLAY 之后,就没有效果了,状态栏任然可以下拉~
在定制系统中,如果有完整的系统源码,可以通过修改系统源码中的 framework
部分,然后重新编译、刷入系统镜像来实现,具体目录在:
android-aosp/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/
网上有很多指导修改的博客了,感兴趣的小伙伴可以自行研究。