feat: 新增底部四 Tab 导航骨架与路由表

This commit is contained in:
2026-09-10 10:54:05 +08:00
parent 2fba18bc7e
commit 6d3cb1b418
11 changed files with 282 additions and 8 deletions
@@ -4,15 +4,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.core.designsystem.RingHealthTheme import com.placeholder.ringhealth.core.designsystem.RingHealthTheme
import com.placeholder.ringhealth.core.navigation.RingHealthApp
/** 唯一 Activity,承载 Compose 界面。 */ /** 唯一 Activity,承载 Compose 界面。 */
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -25,10 +22,8 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background, color = MaterialTheme.colorScheme.background,
) { ) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { // 界面骨架(底部导航 + 路由)在 core/navigation/RingHealthApp.kt
// 文案取自 res/values/strings.xml,不在 Kotlin 里硬编码用户可见文字 RingHealthApp()
Text(stringResource(R.string.app_name))
}
} }
} }
} }
@@ -0,0 +1,117 @@
package com.placeholder.ringhealth.core.navigation
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FitnessCenter
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.placeholder.ringhealth.R
import com.placeholder.ringhealth.domain.model.MetricType
import com.placeholder.ringhealth.feature.device.DeviceScreen
import com.placeholder.ringhealth.feature.device.DeviceSettingsPlaceholder
import com.placeholder.ringhealth.feature.detail.MetricDetailPlaceholder
import com.placeholder.ringhealth.feature.home.HomeScreen
import com.placeholder.ringhealth.feature.profile.ProfileScreen
import com.placeholder.ringhealth.feature.workout.WorkoutHistoryPlaceholder
import com.placeholder.ringhealth.feature.workout.WorkoutScreen
/** 底部导航项定义。 */
private data class BottomTab(
val route: String,
val labelRes: Int,
val icon: ImageVector,
)
private val BOTTOM_TABS = listOf(
BottomTab(Routes.HOME, R.string.tab_home, Icons.Filled.Home),
BottomTab(Routes.WORKOUT, R.string.tab_workout, Icons.Filled.FitnessCenter),
BottomTab(Routes.DEVICE, R.string.tab_device, Icons.Filled.Settings),
BottomTab(Routes.PROFILE, R.string.tab_profile, Icons.Filled.Person),
)
/**
* 应用主界面:Scaffold + 底部导航 + NavHost。
*/
@Composable
fun RingHealthApp(navController: NavHostController = rememberNavController()) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
bottomBar = {
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
BOTTOM_TABS.forEach { tab ->
NavigationBarItem(
selected = currentRoute == tab.route,
onClick = { navController.navigateToTab(tab.route) },
icon = { Icon(imageVector = tab.icon, contentDescription = null) },
label = { Text(stringResource(tab.labelRes)) },
)
}
}
},
) { innerPadding ->
NavHost(
navController = navController,
startDestination = Routes.HOME,
modifier = Modifier.padding(innerPadding),
) {
composable(Routes.HOME) {
HomeScreen(onOpenDetail = { metric -> navController.navigate(Routes.detail(metric)) })
}
composable(Routes.WORKOUT) {
WorkoutScreen()
}
composable(Routes.DEVICE) {
DeviceScreen()
}
composable(Routes.PROFILE) {
ProfileScreen()
}
composable(Routes.DEVICE_SETTINGS) {
DeviceSettingsPlaceholder()
}
composable(Routes.WORKOUT_HISTORY) {
WorkoutHistoryPlaceholder()
}
composable(
route = Routes.DETAIL_PATTERN,
arguments = listOf(navArgument(Routes.DETAIL_ARG_METRIC) { type = NavType.StringType }),
) { entry ->
val raw = entry.arguments?.getString(Routes.DETAIL_ARG_METRIC).orEmpty()
val metric = MetricType.entries.firstOrNull { it.name == raw } ?: MetricType.STEP
MetricDetailPlaceholder(metric = metric)
}
}
}
}
/** 底部 Tab 切换:保留状态、避免回退栈堆积。 */
private fun NavHostController.navigateToTab(route: String) {
navigate(route) {
popUpTo(graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
@@ -0,0 +1,23 @@
package com.placeholder.ringhealth.core.navigation
import com.placeholder.ringhealth.domain.model.MetricType
/**
* 全局路由表。字符串集中定义,避免散落在各处。
*/
object Routes {
const val HOME = "home"
const val WORKOUT = "workout"
const val DEVICE = "device"
const val PROFILE = "profile"
const val DEVICE_SETTINGS = "device/settings"
const val WORKOUT_HISTORY = "workout/history"
/** 指标详情路由模板,参数为 MetricType.name。 */
const val DETAIL_PATTERN = "detail/{metric}"
const val DETAIL_ARG_METRIC = "metric"
/** 拼出某个指标的详情路由。 */
fun detail(metric: MetricType): String = "detail/${metric.name}"
}
@@ -0,0 +1,19 @@
package com.placeholder.ringhealth.feature.detail
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
import com.placeholder.ringhealth.domain.model.MetricType
/** 指标详情子页(占位,待 Task 12 实现)。 */
@Composable
fun MetricDetailPlaceholder(metric: MetricType, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.placeholder_metric_detail))
}
}
@@ -0,0 +1,18 @@
package com.placeholder.ringhealth.feature.device
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
/** 设备页(占位,待 Task 10 实现)。 */
@Composable
fun DeviceScreen(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.tab_device))
}
}
@@ -0,0 +1,18 @@
package com.placeholder.ringhealth.feature.device
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
/** 设备设置子页(占位,待 Task 10 实现)。 */
@Composable
fun DeviceSettingsPlaceholder(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.placeholder_device_settings))
}
}
@@ -0,0 +1,22 @@
package com.placeholder.ringhealth.feature.home
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
import com.placeholder.ringhealth.domain.model.MetricType
/** 健康首页(占位,待 Task 8 实现)。 */
@Composable
fun HomeScreen(
onOpenDetail: (MetricType) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.tab_home))
}
}
@@ -0,0 +1,18 @@
package com.placeholder.ringhealth.feature.profile
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
/** 我的页(占位,待 Task 11 实现)。 */
@Composable
fun ProfileScreen(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.tab_profile))
}
}
@@ -0,0 +1,18 @@
package com.placeholder.ringhealth.feature.workout
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
/** 运动历史子页(占位,待 Task 9 实现)。 */
@Composable
fun WorkoutHistoryPlaceholder(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.placeholder_workout_history))
}
}
@@ -0,0 +1,18 @@
package com.placeholder.ringhealth.feature.workout
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.placeholder.ringhealth.R
/** 运动页(占位,待 Task 9 实现)。 */
@Composable
fun WorkoutScreen(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.tab_workout))
}
}
+8
View File
@@ -6,4 +6,12 @@
<string name="stress_normal">正常</string> <string name="stress_normal">正常</string>
<string name="stress_medium">中等</string> <string name="stress_medium">中等</string>
<string name="stress_high">偏高</string> <string name="stress_high">偏高</string>
<string name="tab_home">健康</string>
<string name="tab_workout">运动</string>
<string name="tab_device">设备</string>
<string name="tab_profile">我的</string>
<string name="placeholder_device_settings">设备设置</string>
<string name="placeholder_workout_history">运动历史</string>
<string name="placeholder_metric_detail">指标详情</string>
</resources> </resources>