ui-component
这是针对 UI (用户界面) 相关组件的一个依赖。
配置依赖
你可以使用如下方式将此模块添加到你的项目中。
Version Catalog (推荐)
在你的项目 gradle/libs.versions.toml 中添加依赖。
[versions]
betterandroid-ui-component = "<version>"
[libraries]
betterandroid-ui-component = { module = "com.highcapable.betterandroid:ui-component", version.ref = "betterandroid-ui-component" }
在你的项目 build.gradle.kts 中配置依赖。
implementation(libs.betterandroid.ui.component)
请将 <version> 修改为此文档顶部显示的版本。
传统方式
在你的项目 build.gradle.kts 中配置依赖。
implementation("com.highcapable.betterandroid:ui-component:<version>")
请将 <version> 修改为此文档顶部显示的版本。
功能介绍
你可以 点击这里 查看 KDoc。
在找适配器 (Adapter) 吗?
适配器 (Adapter) 相关功能已被分离为一个独立的模块 ui-component-adapter,后期将单独进行更新。
在找边衬区 (Insets) 吗?
边衬区 (Insets) 相关功能已被迁移至 ui-extension → 边衬区 (Insets) 扩展,后期将跟随这个模块进行更新。
Activity
本节内容
带有视图绑定的 Activity (继承于 AppCompatActivity)。
基础视图组件 Activity (继承于 AppCompatActivity)。
基础组件 Activity (继承于 ComponentActivity)。
可用于 Jetpack Compose 项目。
小提示
下方的预置组件都实现了 IBackPressedController、 ISystemBarsController 接口。
你可以在下方的 系统事件 和 系统栏 (状态栏、导航栏等) 中找到详细的使用方法。
在使用 ViewBinding 的情况下,你可以使用 AppBindingActivity 来快速创建一个带有视图绑定的 Activity。
在 AppBindingActivity 中,你可以直接使用 binding 属性获取视图绑定对象而无需手动调用 setContentView 方法。
示例如下
class MainActivity : AppBindingActivity<ActivityMainBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.mainText.text = "Hello World!"
}
}
小提示
如果你需要在视图装载前进行一些自定义操作,在一般情况下,你可能会这样做。
示例如下
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
doSomething()
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.main_text).text = "Hello World!"
}
private fun doSomething() {
// Your code here.
}
}
在使用 AppBindingActivity 的情况下,你需要这样做。
示例如下
class MainActivity : AppBindingActivity<ActivityMainBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.mainText.text = "Hello World!"
}
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
doSomething()
// 你可以返回经过处理后的 LayoutInflater,这个实例将用于初始化布局
return super.onPrepareContentView(savedInstanceState)
}
private fun doSomething() {
// Your code here.
}
}
你也可以使用 AppViewsActivity 来创建一个基本 Activity,使用 findViewById 方法来获取 View。
示例如下
class MainActivity : AppViewsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.main_text).text = "Hello World!"
}
}
如果你的项目是一个 Jetpack Compose 项目,你可以使用 AppComponentActivity 来创建一个基本 Activity。
示例如下
class MainActivity : AppComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello World!")
}
}
}
小提示
有关 Jetpack Compose 的相关扩展你可以参考 compose-extension、compose-multiplatform。
BetterAndroid 同样为 Activity 提供了相关扩展,你可以参考 ui-extension → Activity 扩展。
Fragment
小提示
下方的预置组件都实现了 IBackPressedController、 ISystemBarsController 接口。
你可以在下方的 系统事件 和 系统栏 (状态栏、导航栏等) 中找到详细的使用方法。
在使用 ViewBinding 的情况下,你可以使用 AppBindingFragment 来快速创建一个带有视图绑定的 Fragment。
在 AppBindingFragment 中,你可以直接使用 binding 属性获取视图绑定对象而无需手动重写 onCreateView 方法。
你不需要考虑 Fragment 的生命周期对 binding 的影响,BetterAndroid 已经为你处理了这些问题。
示例如下
class MainFragment : AppBindingFragment<FragmentMainBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.mainText.text = "Hello World!"
}
}
你也可以使用 AppViewsFragment 来创建一个基本 Fragment。
同样地,你无需重写 onCreateView 方法,直接将需要绑定的布局资源 ID 填入构造方法即可。
示例如下
class MainFragment : AppViewsFragment(R.layout.fragment_main) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<TextView>(R.id.main_text).text = "Hello World!"
}
}
小提示
BetterAndroid 同样为 Fragment 提供了相关扩展,你可以参考 ui-extension → Fragment 扩展。
系统事件
在 androidx 的依赖 androidx.activity:activity 中已经为开发者提供了一个 OnBackPressedDispatcher。
但是出于对官方贸然作废重写 onBackPressed 方法的不满,BetterAndroid 对 OnBackPressedDispatcher 相关功能进行了封装, 支持了更适用于 Kotlin 写法的返回事件回调功能,同时添加了忽略全部回调事件直接释放返回事件的功能,使其变得更加灵活好用。
AppBindingActivity、AppViewsActivity、AppComponentActivity、AppBindingFragment、AppViewsFragment 已经默认实现了 IBackPressedController 接口,你可以直接使用 backPressed 获取 BackPressedController。
但是你依然可以在 Activity 中手动创建一个 BackPressedController。
示例如下
class YourActivity : AppCompatActivity() {
// 创建一个懒加载对象
val backPressed by lazy { BackPressedController.from(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 在这里调用 backPressed 实现相关功能
backPressed
}
override fun onDestroy() {
super.onDestroy()
// 销毁 backPressed,这会移除所有回调事件
// 可选,防止内存泄漏
backPressed.destroy()
}
}
下面是 BackPressedController 的基本用法。
示例如下
// 添加一个返回回调
val callback = backPressed.addCallback {
// 在回调内忽略当前回调并触发返回操作
// 例如你可以在此处弹出一个对话框询问用户是否退出且此时选择了 “是”
// 传入的对象需要为创建此回调的 backPressed
trigger(backPressed)
// 或者在触发后同时移除自身
trigger(backPressed, removed = true)
// 直接移除 (不推荐,你应该使用 backPressed.removeCallback)
remove()
}
// 你也可以手动创建一个回调
// 注意:请确保引入 com.highcapable.betterandroid.ui.component.backpress.callback
// 包名下的 OnBackPressedCallback,而不是 androidx.activity.OnBackPressedCallback
val callback = OnBackPressedCallback {
// Your code here.
}
// 然后添加到 backPressed
backPressed.addCallback(callback)
// 移除一个已知的回调
backPressed.removeCallback(callback)
// 触发系统的返回操作
backPressed.trigger()
// 你可以设置 ignored 为 true 来忽略所有已添加的回调直接返回
backPressed.trigger(ignored = true)
// 判断当前是否存在已启用的回调
val hasEnabledCallbacks = backPressed.hasEnabledCallbacks
// 销毁,这会移除所有回调事件
backPressed.destroy()
注意
在使用 BackPressedController 后,当前的 OnBackPressedDispatcher 已被其自动接管, 你不应该继续使用 onBackPressedDispatcher.addCallback(...),这会造成存在未知的 (野生的) 回调导致无法干净地移除它们。
通知
想要在 Android 中创建并发送一条通知并不容易,其中最大的问题就在于系统通知的创建复杂、管理混乱且 API 难以简单地兼容旧版本。
尤其是当开发者看到了 NotificationCompat 以及 NotificationChannelCompat 这两个类时,更是会感到无从下手。
于是 BetterAndroid 对系统通知相关 API 进行了整体性的封装,基本上覆盖了系统通知中能够用到的所有功能和调用。
所以你不需要再考虑类似通知渠道这样 Android 8 及以下系统的兼容性问题,BetterAndroid 已经为你处理了这些问题。
在 Kotlin 中你能够更加方便地创建一条系统通知。
示例如下
// 假设这就是你当前的 Context
val context: Context
// 创建需要推送的通知对象
val notification = context.createNotification(
// 创建并设置通知渠道
// 在 Android 8 及以上系统中必须存在一个通知渠道
// 在低于 Android 8 的系统中,此功能会被自动兼容化处理
channel = NotificationChannel("my_channel_id") {
// 设置通知渠道名称 (这会显示在系统的通知设置中)
name = "My Channel"
// 设置通知渠道描述 (这会显示在系统的通知设置中)
description = "My channel description."
// 其余用法与 NotificationChannelCompat.Builder 保持一致
}
) {
// 设置通知小图标 (这将会显示在状态栏和通知栏中)
// 通知小图标必须为单色图标 (建议为矢量图)
smallIconResId = R.drawable.ic_my_notification
// 设置通知标题
contentTitle = "My Notification"
// 设置通知内容
contentText = "Hello World!"
// 其余用法与 NotificationCompat.Builder 保持一致
}
// 使用默认通知 ID 推送通知
notification.post()
// 使用自定义通知 ID 推送通知
val notifyId = 1
notification.post(notifyId)
// 取消当前通知 (这会从系统通知栏中清除这条通知)
notification.cancel()
// 判断当前通知是否已经被取消
val isCanceled = notification.isCanceled
注意
在 Android 13 及以上系统中,你需要为通知定义并添加运行时权限。
当未正确定义此权限时,调用 post 方法时将自动要求你添加权限到 AndroidManifest.xml 中。
你可以使用以下方式在通知渠道中为通知设置优先级。
示例如下
// 假设这就是你当前的 Context
val context: Context
// 创建需要推送的通知对象
val notification = context.createNotification(
// 创建并设置通知渠道
// 在低于 Android 8 的系统中,此功能会被自动兼容化处理
// 优先级决定了通知的重要性,这会影响通知的显示方式
// BetterAndroid 将 NotificationManager 中的优先级静态变量
// 封装到了 NotificationImportance 中,你可以更方便地设置通知的优先级
// 这里我们设置了 NotificationImportance.HIGH (高优先级),
// 这将会在系统通知栏中以横幅的形式显示通知并伴随响铃提醒
channel = NotificationChannel("my_channel_id", importance = NotificationImportance.HIGH) {
name = "My Channel"
description = "My channel description."
}
) {
smallIconResId = R.drawable.ic_my_notification
contentTitle = "My Notification"
contentText = "Hello World!"
}
// 使用默认通知 ID 推送通知
notification.post()
当遇到多组通知时,你可以使用以下方式创建一组通知渠道。
示例如下
// 假设这就是你当前的 Context
val context: Context
// 创建一个通知渠道组
// 在低于 Android 8 的系统中,此功能将无作用
val channelGroup = NotificationChannelGroup("my_channel_group_id") {
// 设置通知渠道组名称 (这会显示在系统的通知设置中)
name = "My Channel Group"
// 设置通知渠道组描述 (这会显示在系统的通知设置中)
description = "My channel group description."
}
// 创建第一个通知渠道并指定通知渠道组
val channel1 = NotificationChannel("my_channel_id_1", channelGroup) {
name = "My Channel 1"
description = "My channel description."
}
// 创建第二个通知渠道并指定通知渠道组
val channel2 = NotificationChannel("my_channel_id_2", channelGroup) {
name = "My Channel 2"
description = "My channel description."
}
// 使用 channel1 创建第一条通知并推送
context.createNotification(channel1) {
smallIconResId = R.drawable.ic_my_notification
contentTitle = "My Notification 1"
contentText = "Hello World!"
}.post(1)
// 使用 channel2 创建第二条通知并推送
context.createNotification(channel2) {
smallIconResId = R.drawable.ic_my_notification
contentTitle = "My Notification 2"
contentText = "Hello World!"
}.post(2)
上述内容将创建一个通知渠道组并在其中添加两个通知渠道。
通知推送后,系统将会自动为这两个通知渠道创建一个组分类。
注意
通知渠道中的设置仅会在首次创建这个通知渠道时生效,如果通知渠道的设置被用户修改过,那么这些设置将不会再被覆盖。
你无法修改已经创建的通知渠道设置,但是你可以重新为其分配一个新的通知渠道 ID,这样将会创建一个新的通知渠道。
上方的示例中,通知对象是被自动化管理的,如果你希望手动创建一个通知对象而并不依赖于 context.createNotification 方法,请参考以下示例。
示例如下
// 假设这就是你当前的 Context
val context: Context
// 创建需要推送的通知对象
val notification = Notification(
// 设置 Context
context = context,
// 创建并设置通知渠道
channel = NotificationChannel("my_channel_id") {
name = "My Channel"
description = "My channel description."
}
) {
smallIconResId = R.drawable.ic_my_notification
contentTitle = "My Notification"
contentText = "Hello World!"
}
// 将当前通知作为推送对象
val poster = notification.asPoster()
// 使用默认通知 ID 推送通知
poster.post()
// 使用自定义通知 ID 推送通知
val notifyId = 1
poster.post(notifyId)
// 取消当前通知 (这会从系统通知栏中清除这条通知)
poster.cancel()
// 判断当前通知是否已经被取消
val isCanceled = poster.isCanceled
小提示
通过 Notification、NotificationChannel、NotificationChannelGroup 创建的对象是对 NotificationCompat、NotificationChannelCompat、NotificationChannelGroupCompat 的一个包装,你可以使用 instance 来得到其中的实际对象以进行一些你自己的操作。
你还可以通过 Context.notificationManager 来获取到 NotificationManagerCompat 对象以进行一些你自己的操作。
系统栏 (状态栏、导航栏等)
本节内容
系统栏控制器。
系统栏控制器接口。
系统栏的样式。
系统栏的类型。
系统栏的行为。
Android 开发的严重适配问题就在于终端设备没有统一开发规范的混乱性。
为了给用户带来更好的体验,状态栏、导航栏何时应该显示、隐藏,状态栏、导航栏的颜色、背景等等,这些都是开发者在开发过程中需要考虑的问题。
所以 BetterAndroid 对接并封装了 androidx 所提供的系统栏适配方案,并将其集成到了 SystemBarsController 中,现在,你可以非常方便地来调用它去轻松实现操作系统栏的一系列解决方案。
SystemBarsController 最低支持到 Android 5.0,并解决了部分厂商定制系统中的兼容性问题。
AppBindingActivity、AppViewsActivity、AppComponentActivity、AppBindingFragment、AppViewsFragment 已经默认实现了 ISystemBarsController 接口,你可以直接使用 systemBars 获取 SystemBarsController。
但是你依然可以在 Activity 中使用 Activity.getWindow 对象手动创建一个 SystemBarsController。
示例如下
class YourActivity : AppCompatActivity() {
// 创建一个懒加载对象
val systemBars by lazy { SystemBarsController.from(window) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 创建你的 binding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 使用当前根布局初始化 systemBars
systemBars.init(binding.root)
}
override fun onDestroy() {
super.onDestroy()
// 销毁 systemBars,这会还原初始化之前的状态
// 可选,防止内存泄漏
systemBars.destroy()
}
}
注意
在使用 init 方法时,推荐并建议传入你自己的根布局,否则将默认使用 android.R.id.content 作为根布局。
你应该避免使用它作为根布局,这是不可控的,你应该做到在 Activity 中能够随时维护一个自己的根布局。
如果你并未使用 ViewBinding,AppViewsActivity、AppComponentActivity 已经默认为你重写了 setContentView 方法, 它会在你使用这个方法的时候自动装载你的根布局到 SystemBarsController 中。
你也可以手动重写 setContentView 方法来实现这个功能。
示例如下
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
// 第一位子布局即你的根布局
val rootView = findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
// 使用当前根布局初始化 systemBars
systemBars.init(rootView)
}
下面是 SystemBarsController 的详细用法介绍。
初始化 SystemBarsController 及处理根布局的 Window Insets padding。
示例如下
// 假设这就是你当前的根布局
val rootView: ViewGroup
// 初始化 SystemBarsController
// 你的根布局必须已经被设置到了一个父布局中,否则将会抛出异常
systemBars.init(rootView)
// 你可以自定义处理根布局的 Window Insets
systemBars.init(rootView, edgeToEdgeInsets = { systemBars })
// 如果你不希望 SystemBarsController 自动为你处理根布局的 Window Insets,
// 你可以直接设置 edgeToEdgeInsets 为 null
systemBars.init(rootView, edgeToEdgeInsets = null)
注意
SystemBarsController 初始化时会自动设置 Window.setDecorFitsSystemWindows(false) (在异形屏设备上会同时设置 layoutInDisplayCutoutMode 为 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES), 你只要在 init 中设置了 edgeToEdgeInsets (默认设置), 那么你的根布局将会拥有一个 safeDrawingIgnoringIme 控制的 Window Insets padding,这也是为什么你应该做到在 Activity 中能够随时维护一个自己的根布局。
如果你在 init 中将 edgeToEdgeInsets 设为了 null,那么你的根布局将会完全扩展到全屏。
以上效果等同于 androidx.activity:activity 中提供的 enableEdgeToEdge。
在不做出任何操作的情况下,你的布局就会被系统栏或系统的危险区域 (例如异形屏的挖空处) 遮挡,这会影响用户体验。
如果你想自己维护并管理当前根布局的 padding,你必须确保你的界面元素能够正确适应 Window Insets 提供的间距,你可以前往 ui-extension 的 边衬区 (Insets) 了解更多关于 Window Insets 的内容。
你不再需要使用 enableEdgeToEdge,SystemBarsController 初始化后默认将持有此效果,你应该使用 edgeToEdgeInsets 来控制根布局的 Window Insets padding。
小提示
在 Jetpack Compose 中,你可以使用 AppComponentActivity 来获得一个设置了 edgeToEdgeInsets = null 初始化的 SystemBarsController, 然后使用 Jetpack Compose 的方式去设置 Window Insets,BetterAndroid 同样为其提供了扩展支持,更多功能你可以参考 compose-multiplatform。
设置系统栏的行为。
这决定了显示或隐藏系统栏时由系统控制的行为。
示例如下
systemBars.behavior = SystemBarBehavior.SHOW_TRANSIENT_BARS_BY_SWIPE
以下是 SystemBarBehavior 中提供的全部行为,标有 * 的为默认行为。
| 行为 | 描述 |
|---|---|
DEFAULT | 由系统控制的默认行为 |
*SHOW_TRANSIENT_BARS_BY_SWIPE | 在全屏时可由手势滑动弹出并显示为半透明的系统栏,并在一段时间后继续隐藏 |
显示、隐藏系统栏。
示例如下
// 进入沉浸模式 (全屏模式)
// 同时隐藏状态栏和导航栏
systemBars.hide(SystemBars.ALL)
// 单独控制状态栏和导航栏
systemBars.hide(SystemBars.STATUS_BARS)
systemBars.hide(SystemBars.NAVIGATION_BARS)
// 退出沉浸模式 (全屏模式)
// 同时显示状态栏和导航栏
systemBars.show(SystemBars.ALL)
// 单独控制状态栏和导航栏
systemBars.show(SystemBars.STATUS_BARS)
systemBars.show(SystemBars.NAVIGATION_BARS)
小提示
如果你需要控制输入法 (IME) 的显示与隐藏,你可以参考 ui-extension → View 扩展。
设置系统栏的样式。
你可以自定义状态栏、导航栏的外观。
注意
在 Android 6.0 以下系统中,状态栏的内容不支持反色,如果你设置了亮色则会自动处理为半透明遮罩,但是对于 MIUI、Flyme 自行添加了反色功能的系统将使用其私有方案实现反色效果。
在 Android 8 以下系统中,导航栏的内容不支持反色,处理方式同上。
示例如下
// 设置状态栏的样式
// 注意:请确保引入 com.highcapable.betterandroid.ui.component.systembar.style
// 包名下的 SystemBarStyle,而不是 androidx.activity.SystemBarStyle
systemBars.statusBarStyle = SystemBarStyle(
// 设置背景颜色
color = Color.WHITE,
// 设置内容颜色
darkContent = true
)
// 设置导航栏的样式
systemBars.navigationBarStyle = SystemBarStyle(
// 设置背景颜色
color = Color.WHITE,
// 设置内容颜色
darkContent = true
)
// 你可以一次性设置状态栏和导航栏的样式
systemBars.setStyle(
statusBar = SystemBarStyle(
color = Color.WHITE,
darkContent = true
),
navigationBar = SystemBarStyle(
color = Color.WHITE,
darkContent = true
)
)
// 你也可以同时设置状态栏和导航栏的样式
systemBars.setStyle(
style = SystemBarStyle(
color = Color.WHITE,
darkContent = true
)
)
以下是 SystemBarStyle 中提供的预置样式,标有 * 的为默认样式。
| 样式 | 描述 |
|---|---|
Auto | 系统深色模式下为纯黑背景 + 浅色内容颜色,浅色模式下为纯白背景 + 深色内容颜色 |
*AutoTransparent | 系统深色模式下为浅色内容颜色,浅色模式下为深色内容颜色,背景透明 |
Light | 纯白背景 + 深色内容颜色 |
LightScrim | 半透明纯白背景 + 深色内容颜色 |
LightTransparent | 透明背景 + 深色内容颜色 |
Dark | 纯黑背景 + 浅色内容颜色 |
DarkScrim | 半透明纯黑背景 + 浅色内容颜色 |
DarkTransparent | 透明背景 + 浅色内容颜色 |
小提示
在应用程序首次冷启动时,系统栏的颜色将跟随你在 styles.xml 中设置的属性而决定。
为了能在冷启动时带来更好的用户体验,你可以参考以下示例。
示例如下
<style name="Theme.MyApp.Demo" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 设置状态栏颜色 -->
<item name="android:statusBarColor">@color/colorPrimary</item>
<!-- 设置导航栏颜色 -->
<item name="android:navigationBarColor">@color/colorPrimary</item>
<!-- 设置状态栏内容颜色 -->
<item name="android:windowLightStatusBar">true</item>
<!-- 设置导航栏内容颜色 -->
<item name="android:windowLightNavigationBar">true</item>
</style>
销毁 SystemBarsController。
这会还原初始化之前的状态,包括初始化之前的状态栏、导航栏颜色等。
示例如下
// 销毁 SystemBarsController,这会还原初始化之前的状态
systemBars.destroy()
// 你可以随时使用 isDestroyed 判断当前 SystemBarsController 是否已被销毁
val isDestroyed = systemBars.isDestroyed
注意
在使用 SystemBarsController 后,当前根布局 rootView 的 WindowInsetsController 已被其自动接管, 请不要手动设置 WindowInsetsController 中的 isAppearanceLightStatusBars、isAppearanceLightNavigationBars 等参数, 这可能会导致 statusBarStyle、navigationBarStyle、setStyle 等功能的实际效果显示异常。
