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>

在代码中安装

MainActivityonCreate 方法中,在 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 ClassData 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()是实现“沉浸式”体验的核心方法。它的作用是让应用的内容延伸到系统状态栏和导航栏的正下方,而不是被这些系统栏限制在中间。

配置方式

MainActivityonCreate中,必须在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() 等扩展函数