Kotlin开发Android App - 工程开发
Layout架构
在Jetpack Compose中,Scaffold(脚手架) 是一个遵循Material Design规范的顶级容器。它为页面提供了一个标准的结构化布局,让你能够非常简单地把常见的 UI 组件(如标题栏、底部导航、悬浮按钮)“嵌”在正确的位置。
你可以把它想象成一个房子的框架,它已经预留好了放电视、沙发和灯的位置,你只需要把家具(组件)填进去。
Scaffold的设计非常直观,它通过多个Slot(槽位)来接收不同的Composable函数:
topBar: 顶部状态栏或标题栏。
bottomBar: 底部导航栏。
floatingActionButton (FAB): 悬浮操作按钮。
snackbarHost: 用于显示简短提示(Snackbar)的容器。
content: 页面的主体内容(必填)。
val navController = rememberNavController()
Scaffold(
// 1. 顶部标题栏
topBar = {
TopAppBar(
title = { Text("助手") },
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Blue)
)
},
// 2. 底部导航栏
bottomBar = {
BottomAppBar {
// 这里通常放置NavigationBarItem
}
},
// 3. 悬浮按钮
floatingActionButton = {
FloatingActionButton(onClick = { /* 执行操作 */ }) {
Icon(Icons.Default.Add, contentDescription = "增加")
}
},
// 4. 内容区域
content = { innerPadding ->
// innerPadding非常关键!它包含了topBar和bottomBar占用的高度
Box(modifier = Modifier.padding(innerPadding)) {
// 你的路由NavHost放在这里
AppNavigation(navController)
}
}
)页面配置
启动页面
在Android开发中,做一个带Logo的启动页,现在的标准做法是使 Android 12 (API 31) 引入的SplashScreen API。
传统的创建一个Activity停两秒再跳转的做法已经过时了,因为那会导致“启动页之前的白屏/黑屏”现象。使用官方 API 可以实现从系统桌面图标到 App 内容的平滑过渡。
添加依赖
在 app/build.gradle 中添加SplashScreen库,以兼容旧版本Android:
dependencies {
implementation "androidx.core:core-splashscreen:1.0.1"
}配置启动页
在 res/values/themes.xml 中定义启动页的主题。你需要准备一个Logo图片(建议是矢量图 res/drawable/ic_logo.xml)。
<resources>
<style name="Theme.AppSplash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">#FFFFFF</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_logo</item>
<item name="postSplashScreenTheme">@style/Theme.YourAppTheme</item>
</style>
</resources>如果你希望 Logo 下方显示应用名称,可以使用windowSplashScreenBrandingImage属性(仅限 Android 12+)。
修改清单文件 (Manifest)
<activity
android:name=".MainActivity"
android:theme="@style/Theme.AppSplash"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>在代码中安装
在 MainActivity 的 onCreate 方法中,在 setContentView 之前调用 installSplashScreen()。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 1. 安装启动页
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
// 2. (可选) 如果你的数字人项目需要初始化网络或 SDK
// 你可以让启动页多停留一会,直到初始化完成
/*
splashScreen.setKeepOnScreenCondition {
!viewModel.isReady // 当这个条件为 true 时,启动页会一直停着
}
*/
setContent {
// 你的 Compose 内容
HomeScreen()
}
}
}页面路由
在Jetpack Compose中,为了高效管理页面及其UI状态(如状态栏、导航栏的显示隐藏),最佳实践是定义一个路由配置文件。
通过定义一个Sealed Class或Data Class来承载这些元数据,我们可以实现 UI 随路由自动切换,而不需要在每个页面里手动写控制逻辑。
定义路由配置模型
首先,我们创建一个类来包含你提到的所有配置项:
import androidx.annotation.DrawableRes
/**
* 路由配置类
* @param route 路由路径(ID)
* @param showTopBar 是否显示顶栏
* @param showBottomBar 是否显示底栏
* @param showStatusBar 是否显示手机状态栏
* @param icon 如果有底栏,对应的图标资源
*/
sealed class Screen(
val route: String,
val showTopBar: Boolean = false,
val showBottomBar: Boolean = false,
val showStatusBar: Boolean = true,
@DrawableRes val icon: Int? = null
) {
object Splash : Screen("splash", showStatusBar = false) // 启动页全屏
object Ad : Screen("ad", showStatusBar = false) // 广告页全屏
object Home : Screen("home", showBottomBar = true, icon = android.R.drawable.ic_menu_today)
object Settings : Screen("settings", showTopBar = true, showBottomBar = true, icon = android.R.drawable.ic_menu_manage)
}编写全局 Scaffold 架构
在MainActivity中,我们利用Scaffold的特性,根据当前路由自动决定 UI 元素的可见性。
@Composable
fun MainAppContainer() {
val navController = rememberNavController()
// 监听当前路由状态
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
// 匹配当前路由对应的配置对象
val currentScreen = remember(currentRoute) {
when (currentRoute) {
Screen.Splash.route -> Screen.Splash
Screen.Ad.route -> Screen.Ad
Screen.Home.route -> Screen.Home
Screen.Settings.route -> Screen.Settings
else -> Screen.Home
}
}
// --- 动态控制系统状态栏 (StatusBar) ---
val view = LocalView.current
val window = (LocalContext.current as Activity).window
LaunchedEffect(currentScreen.showStatusBar) {
val insetsController = WindowCompat.getInsetsController(window, view)
if (currentScreen.showStatusBar) {
insetsController.show(WindowInsetsCompat.Type.systemBars())
} else {
insetsController.hide(WindowInsetsCompat.Type.systemBars())
}
}
Scaffold(
topBar = {
if (currentScreen.showTopBar) {
// 自定义你的 TopAppBar
CenterAlignedTopAppBar(title = { Text(currentScreen.route) })
}
},
bottomBar = {
if (currentScreen.showBottomBar) {
BottomNavigationBar(navController)
}
}
) { innerPadding ->
// 路由映射表
NavHost(
navController = navController,
startDestination = Screen.Splash.route,
modifier = Modifier.padding(innerPadding)
) {
composable(Screen.Splash.route) { SplashScreen(navController) }
composable(Screen.Ad.route) { AdScreen(navController) }
composable(Screen.Home.route) { HomeScreen(navController) }
composable(Screen.Settings.route) { SettingsScreen(navController) }
}
}
}实现底栏组件
@Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(Screen.Home, Screen.Settings)
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
NavigationBarItem(
icon = { screen.icon?.let { Icon(painterResource(it), contentDescription = null) } },
label = { Text(screen.route) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// 避免重复堆叠
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
)
}
}
}沉浸式页面
默认情况下,Android应用会被限制在状态栏下方和导航栏上方。如果你在做一个数字人或全屏播放器,这会导致屏幕顶端和底端出现难看的黑条。
enableEdgeToEdge()是实现“沉浸式”体验的核心方法。它的作用是让应用的内容延伸到系统状态栏和导航栏的正下方,而不是被这些系统栏限制在中间。
配置方式
在MainActivity的onCreate中,必须在setContent之前调用它。
override fun onCreate(savedInstanceState: Bundle?) {
// 1. 开启边到边显示
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)
)
super.onCreate(savedInstanceState)
setContent {
// 你的路由和Scaffold架构
MainAppContainer()
}
}配合Scaffold处理内容避让
开了enableEdgeToEdge后,你的UI组件(比如按钮)可能会被状态栏图标或底部手势条遮挡。这时候Scaffold提供的innerPadding就派上用场了。
在Scaffold内部,系统会自动计算这些边距(WindowInsets):
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = { /* 你的底栏 */ }
) { innerPadding ->
// innerPadding 包含了状态栏和导航栏的高度
Box(modifier = Modifier.padding(innerPadding)) {
// 这里的业务内容不会被系统栏遮挡
HomeScreen()
}
}项目结构
com.excitai.digitalhuman/
├── MainActivity.kt # 只有 enableEdgeToEdge 和 NavHost
├── BootReceiver.kt # 监听开机广播的组件
├── MainApplication.kt # App 入口,初始化全局配置
├── ui/
│ ├── theme/
│ │ ├── Theme.kt # 颜色、字体配置
│ │ └── Color.kt
│ ├── navigation/
│ │ ├── Screen.kt # Screen配置类
│ │ └── NavGraph.kt # 路由导航宿主
│ ├── components/
│ │ └── VideoPlayerView.kt # 封装的Player组件
│ └── screens/
│ ├── splash/
│ │ └── SplashScreen.kt
│ ├── ad/
│ │ └── AdScreen.kt
│ └── home/
│ └── HomeScreen.kt # 页面
├── viewmodel/
│ ├── HomeViewModel.kt # 页面数据层
│ └── AdViewModel.kt # 页面数据层
├── data/
│ ├── sdk/
│ │ ├── AiKitManager.kt # 语音SDK包装类
│ │ └── SparkChainHelper.kt # 交互类
│ ├── repository/
│ │ └── ChatRepository.kt # 统一处理对话数据流
│ └── model/
│ └── MessageModel.kt # 消息实体类
└── util/
├── Constants.kt # 存放 AppID、秘钥等常量
└── Extension.kt # 存放 Context.toast() 等扩展函数