diff --git a/SystemBars_README.md b/SystemBars_README.md new file mode 100644 index 0000000..9beb9c4 --- /dev/null +++ b/SystemBars_README.md @@ -0,0 +1,165 @@ +# iOS 透明状态栏和导航栏实现 + +## 概述 + +本项目成功实现了iOS应用的透明状态栏和导航栏效果,让应用背景图片能够延伸到状态栏和导航栏区域,同时提供了动态获取系统栏高度的功能,确保内容不被遮挡。 + +## 实现的功能 + +### 1. 透明状态栏和导航栏 +- ✅ iOS状态栏完全透明 +- ✅ 内容延伸到状态栏区域 +- ✅ 状态栏文字为白色(适合深色背景) +- ✅ 底部安全区域处理 + +### 2. 动态高度获取 +- ✅ Android平台:动态获取真实的状态栏和导航栏高度 +- ✅ iOS平台:根据设备类型返回合适的高度值 +- ✅ 跨平台统一API + +### 3. Compose组件支持 +- ✅ `systemBarsPadding()` - 添加状态栏和导航栏内边距 +- ✅ `statusBarsPadding()` - 只添加状态栏内边距 +- ✅ `navigationBarsPadding()` - 只添加导航栏内边距 +- ✅ `StatusBarSpacer` - 状态栏空白组件 +- ✅ `NavigationBarSpacer` - 导航栏空白组件 + +## 使用方法 + +### 1. 基本用法 + +```kotlin +@Composable +fun MyScreen() { + Box(modifier = Modifier.fillMaxSize()) { + // 背景图片,延伸到整个屏幕 + Image( + painter = painterResource(Res.drawable.background), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + // 内容区域,添加系统栏内边距 + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() // 关键:添加系统栏内边距 + ) { + // 您的内容 + Text("这里的内容不会被状态栏遮挡") + } + } +} +``` + +### 2. 只处理状态栏 + +```kotlin +@Composable +fun MyTopBar() { + Row( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() // 只添加状态栏内边距 + .padding(horizontal = 16.dp) + ) { + // 顶部栏内容 + } +} +``` + +### 3. 使用空白组件 + +```kotlin +@Composable +fun MyContent() { + Column { + StatusBarSpacer() // 状态栏高度的空白 + + // 您的内容 + Text("内容从这里开始") + + Spacer(modifier = Modifier.weight(1f)) + + NavigationBarSpacer() // 导航栏高度的空白 + } +} +``` + +### 4. 获取高度值 + +```kotlin +@Composable +fun MyComponent() { + val statusBarHeight = getStatusBarHeight() + val navigationBarHeight = getNavigationBarHeight() + + Text("状态栏高度: $statusBarHeight") + Text("导航栏高度: $navigationBarHeight") +} +``` + +## 平台特定实现 + +### Android +- 使用系统资源获取真实的状态栏和导航栏高度 +- 支持不同屏幕密度的自动适配 +- 兼容现有的`ImmersionBar`透明状态栏设置 + +### iOS +- 状态栏高度:iPhone 44dp,iPad 24dp +- 导航栏高度:iPhone 34dp(有Home指示器),iPad 0dp +- 在Swift层实现透明状态栏效果 + +## 文件结构 + +``` +composeApp/src/ +├── commonMain/kotlin/com/whitefish/app/ +│ ├── Platform.kt # 平台接口定义 +│ └── ui/components/ +│ └── SystemBarsPadding.kt # 系统栏内边距组件 +├── androidMain/kotlin/com/whitefish/app/ +│ └── Platform.android.kt # Android平台实现 +└── iosMain/kotlin/com/whitefish/app/ + └── Platform.ios.kt # iOS平台实现 + +iosApp/iosApp/ +├── ContentView.swift # iOS主视图 +├── StatusBarConfig.swift # 状态栏配置 +├── iOSApp.swift # iOS应用入口 +└── Info.plist # iOS配置文件 +``` + +## 技术要点 + +### 1. iOS透明状态栏实现 +- 使用自定义`UIViewController`控制状态栏外观 +- 设置`preferredStatusBarStyle = .lightContent` +- 配置视图背景为透明 +- 内容延伸到所有边缘 + +### 2. 高度获取策略 +- Android:使用系统资源动态获取 +- iOS:基于设备类型的预设值 +- 提供fallback机制确保稳定性 + +### 3. Compose集成 +- 使用`expect/actual`机制实现跨平台API +- 提供Modifier扩展函数简化使用 +- 支持组合式和声明式两种使用方式 + +## 注意事项 + +1. **iOS设备适配**:当前iOS实现使用预设值,未来可以考虑使用更精确的动态获取方式 +2. **Android兼容性**:与现有的`StatusBarView`组件兼容,可以逐步迁移 +3. **性能考虑**:高度获取在Compose中会被缓存,不会重复计算 + +## 示例效果 + +使用这些组件后,您的应用将实现: +- 背景图片延伸到状态栏,营造沉浸式体验 +- 内容区域正确避开系统栏,不被遮挡 +- iOS和Android平台效果一致 +- 代码简洁,易于维护 \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/whitefish/app/Platform.android.kt b/composeApp/src/androidMain/kotlin/com/whitefish/app/Platform.android.kt index a6eddf8..b7b3af8 100644 --- a/composeApp/src/androidMain/kotlin/com/whitefish/app/Platform.android.kt +++ b/composeApp/src/androidMain/kotlin/com/whitefish/app/Platform.android.kt @@ -1,9 +1,59 @@ package com.whitefish.app import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp +import android.content.Context +import android.view.WindowInsets +import androidx.core.view.WindowInsetsCompat class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" } -actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file +actual fun getPlatform(): Platform = AndroidPlatform() + +// Android状态栏高度 +@Composable +actual fun getStatusBarHeight(): Dp { + val context = LocalContext.current + val density = LocalDensity.current + + return with(density) { + getStatusBarHeightPx(context).toDp() + } +} + +// Android导航栏高度 +@Composable +actual fun getNavigationBarHeight(): Dp { + val context = LocalContext.current + val density = LocalDensity.current + + return with(density) { + getNavigationBarHeightPx(context).toDp() + } +} + +// 获取状态栏高度(像素) +private fun getStatusBarHeightPx(context: Context): Int { + var statusBarHeight = 0 + val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + statusBarHeight = context.resources.getDimensionPixelSize(resourceId) + } + return statusBarHeight +} + +// 获取导航栏高度(像素) +private fun getNavigationBarHeightPx(context: Context): Int { + var navigationBarHeight = 0 + val resourceId = context.resources.getIdentifier("navigation_bar_height", "dimen", "android") + if (resourceId > 0) { + navigationBarHeight = context.resources.getDimensionPixelSize(resourceId) + } + return navigationBarHeight +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/Platform.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/Platform.kt index 21616fd..70f014e 100644 --- a/composeApp/src/commonMain/kotlin/com/whitefish/app/Platform.kt +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/Platform.kt @@ -1,7 +1,16 @@ package com.whitefish.app +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp + interface Platform { val name: String } -expect fun getPlatform(): Platform \ No newline at end of file +expect fun getPlatform(): Platform + +@Composable +expect fun getStatusBarHeight(): Dp + +@Composable +expect fun getNavigationBarHeight(): Dp \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/components/SystemBarsDemo.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/components/SystemBarsDemo.kt new file mode 100644 index 0000000..b8cd0cb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/components/SystemBarsDemo.kt @@ -0,0 +1,74 @@ +package com.whitefish.app.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.whitefish.app.getStatusBarHeight +import com.whitefish.app.getNavigationBarHeight + +/** + * 系统栏演示组件 + * 用于展示状态栏和导航栏高度的获取效果 + */ +@Composable +fun SystemBarsDemo(modifier: Modifier = Modifier) { + val statusBarHeight = getStatusBarHeight() + val navigationBarHeight = getNavigationBarHeight() + + Column( + modifier = modifier + .fillMaxSize() + .background(Color(0xFF1A1A1A)) + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Card( + modifier = Modifier.padding(16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2A2A)) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "系统栏高度信息", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "状态栏高度: ${statusBarHeight}", + fontSize = 16.sp, + color = Color(0xFFE0E0E0) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "导航栏高度: ${navigationBarHeight}", + fontSize = 16.sp, + color = Color(0xFFE0E0E0) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "此内容区域已添加系统栏内边距", + fontSize = 14.sp, + color = Color(0xFFB0B0B0) + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/components/SystemBarsPadding.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/components/SystemBarsPadding.kt new file mode 100644 index 0000000..fba6f5d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/components/SystemBarsPadding.kt @@ -0,0 +1,65 @@ +package com.whitefish.app.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.whitefish.app.getStatusBarHeight +import com.whitefish.app.getNavigationBarHeight + +/** + * 系统栏内边距组件 + * 为透明状态栏和导航栏提供正确的内边距 + */ +@Composable +fun Modifier.systemBarsPadding(): Modifier { + val statusBarHeight: Dp = getStatusBarHeight() + val navigationBarHeight: Dp = getNavigationBarHeight() + + return this.padding( + top = statusBarHeight, + bottom = navigationBarHeight + ) +} + +/** + * 只添加状态栏内边距 + */ +@Composable +fun Modifier.statusBarsPadding(): Modifier { + val statusBarHeight: Dp = getStatusBarHeight() + + return this.padding(top = statusBarHeight) +} + +/** + * 只添加导航栏内边距 + */ +@Composable +fun Modifier.navigationBarsPadding(): Modifier { + val navigationBarHeight: Dp = getNavigationBarHeight() + + return this.padding(bottom = navigationBarHeight) +} + +/** + * 状态栏空白组件 + */ +@Composable +fun StatusBarSpacer(modifier: Modifier = Modifier) { + val statusBarHeight: Dp = getStatusBarHeight() + Spacer( + modifier = modifier.height(height = statusBarHeight) + ) +} + +/** + * 导航栏空白组件 + */ +@Composable +fun NavigationBarSpacer(modifier: Modifier = Modifier) { + val navigationBarHeight: Dp = getNavigationBarHeight() + Spacer( + modifier = modifier.height(height = navigationBarHeight) + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/HomeScreen.kt index 2938992..903edc3 100644 --- a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/HomeScreen.kt @@ -24,6 +24,9 @@ import com.whitefish.app.ui.home.state.StateScreen import com.whitefish.app.ui.home.exercise.ExerciseScreen import com.whitefish.app.ui.home.recovery.RecoveryScreen import com.whitefish.app.ui.home.setting.SettingScreen +import com.whitefish.app.ui.components.systemBarsPadding +import com.whitefish.app.ui.components.statusBarsPadding +import com.whitefish.app.ui.components.navigationBarsPadding import org.jetbrains.compose.resources.DrawableResource import ringappkmp.composeapp.generated.resources.bg import ringappkmp.composeapp.generated.resources.ic_nav_exercise @@ -39,6 +42,7 @@ fun HomeScreen( val uiState by viewModel.uiState.collectAsState() Box(modifier = modifier.fillMaxSize()) { + // 背景图片,延伸到整个屏幕(包括状态栏和导航栏区域) Image( painter = painterResource(Res.drawable.bg), contentDescription = null, @@ -46,11 +50,13 @@ fun HomeScreen( contentScale = ContentScale.Crop ) + // 内容区域,添加系统栏内边距 Column( modifier = Modifier .fillMaxSize() + .systemBarsPadding() // 添加状态栏和导航栏的内边距 ) { - + // 主内容区域 Box( modifier = Modifier .weight(1f) @@ -69,17 +75,15 @@ fun HomeScreen( selectedTab = uiState.selectedTab, onTabSelected = viewModel::selectTab ) - } - // 加载动画覆盖层 + // 加载动画覆盖层(不需要内边距,覆盖整个屏幕) if (uiState.isLoading) { LoadingOverlay() } } } - @Composable private fun BottomNavigationBar( selectedTab: HomeTab, diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseScreen.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseScreen.kt index 321ad9a..9aaf4f6 100644 --- a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseScreen.kt @@ -1,58 +1,183 @@ package com.whitefish.app.ui.home.exercise +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import org.jetbrains.compose.ui.tooling.preview.Preview +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun ExerciseScreen( - modifier: Modifier = Modifier, - viewModel: ExerciseViewModel = viewModel { ExerciseViewModel() } -) { - val uiState by viewModel.uiState.collectAsState() - +fun ExerciseScreen() { LazyColumn( - modifier = modifier + modifier = Modifier .fillMaxSize() - .background(Color(0xFF1A1A1A)) - .padding(horizontal = 16.dp), - contentPadding = PaddingValues(vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + .padding(horizontal = 10.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - items(uiState.exerciseData) { exercise -> - ExerciseCard(exercise = exercise) + item { + // 用户问候区域 + UserGreetingSection() + } + + item { + // 运动类型卡片 + ExerciseTypeCards() + } + + item { + // 添加运动记录按钮 + AddExerciseButton() + } + + item { + // 运动记录列表 + ExerciseRecordsSection() } + + item { + // 综合评分 + ComprehensiveScoreSection() + } + + item { + // 解读和建议 + InsightSection() + } + } +} + +@Composable +fun UserGreetingSection() { + Column { + Text( + text = "用户名", + fontSize = 18.sp, + color = Color.White, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "准备好开始运动了吗?", + fontSize = 24.sp, + color = Color.White, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "今天你已经坚持了152天了,当当目日标达了98千卡!", + fontSize = 14.sp, + color = Color.White.copy(alpha = 0.8f) + ) } } @Composable -private fun ExerciseCard( - exercise: ExerciseData, +fun ExerciseTypeCards() { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + ExerciseTypeCard( + title = "跑步", + gradient = listOf(Color(0xFF4FC3F7), Color(0xFF29B6F6)), + modifier = Modifier.size(160.dp, 120.dp) + ) + } + item { + ExerciseTypeCard( + title = "骑行", + gradient = listOf(Color(0xFFFFB74D), Color(0xFFFF9800)), + modifier = Modifier.size(160.dp, 120.dp) + ) + } + item { + ExerciseTypeCard( + title = "游泳", + gradient = listOf(Color(0xFF4DD0E1), Color(0xFF00BCD4)), + modifier = Modifier.size(160.dp, 120.dp) + ) + } + } +} + +@Composable +fun ExerciseTypeCard( + title: String, + gradient: List, modifier: Modifier = Modifier ) { Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = Color(0xFF2A2A2A) - ) + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color.Transparent) ) { - Column( + Box( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) + .fillMaxSize() + .background( + Brush.linearGradient(gradient), + RoundedCornerShape(16.dp) + ), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + fontSize = 18.sp, + color = Color.White, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Composable +fun AddExerciseButton() { + Card( + onClick = { }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xff352764)), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(10.dp) + ) { + Row(modifier = Modifier.fillMaxSize(),verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + Text( + text = "+ 添加运动记录", + fontSize = 16.sp, + color = Color.White, + fontWeight = FontWeight.Medium + ) + } + } +} + +@Composable +fun ExerciseRecordsSection() { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color.White.copy(alpha = 0.9f)) + ) { + Column( + modifier = Modifier.padding(16.dp) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -60,35 +185,159 @@ private fun ExerciseCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = exercise.type, - color = Color.White, + text = "运动记录", fontSize = 16.sp, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Bold, + color = Color(0xFF666666) ) Text( - text = exercise.date, - color = Color.Gray, - fontSize = 12.sp + text = "查看更多", + fontSize = 14.sp, + color = Color(0xFF9C27B0) ) } - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + + Spacer(modifier = Modifier.height(16.dp)) + + // 户外跑步记录 + ExerciseRecordItem( + iconColor = Color(0xFF2196F3), + title = "户外跑步", + distance = "2.85公里", + time = "00:20:36", + pace = "7'59\"公里" + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 自由训练记录 + ExerciseRecordItem( + iconColor = Color(0xFF4CAF50), + title = "自由训练", + distance = "161千卡", + time = "00:57:06", + pace = "104次/分钟" + ) + } + } +} + +@Composable +fun ExerciseRecordItem( + iconColor: Color, + title: String, + distance: String, + time: String, + pace: String +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // 运动图标占位 + Box( + modifier = Modifier + .size(40.dp) + .background(iconColor, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = title.first().toString(), + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFF333333) + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + text = distance, + fontSize = 14.sp, + color = Color(0xFF666666) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = time, + fontSize = 14.sp, + color = Color(0xFF666666) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = pace, + fontSize = 14.sp, + color = Color(0xFF666666) + ) + } + } + } +} + +@Composable +fun ComprehensiveScoreSection() { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color.White.copy(alpha = 0.9f)) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "综合评分", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF333333) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // 圆形评分图表 + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center ) { - ExerciseMetric( - label = "时长", - value = exercise.duration + CircularScoreChart(score = 75) + } + + Spacer(modifier = Modifier.height(20.dp)) + + // 评估指标 + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + ScoreIndicator( + label = "活动小时数得分", + value = "佳", + progress = 0.8f, + color = Color(0xFFFF6B6B) + ) + ScoreIndicator( + label = "有氧活动评估", + value = "佳", + progress = 0.85f, + color = Color(0xFF4ECDC4) ) - ExerciseMetric( - label = "卡路里", - value = "${exercise.calories} kcal" + ScoreIndicator( + label = "运动强度审评估", + value = "优", + progress = 0.95f, + color = Color(0xFF45B7D1) ) - ExerciseMetric( - label = "距离", - value = "${exercise.distance} km" + ScoreIndicator( + label = "运动强度效率评估", + value = "优", + progress = 0.9f, + color = Color(0xFF96CEB4) ) } } @@ -96,39 +345,119 @@ private fun ExerciseCard( } @Composable -private fun ExerciseMetric( +fun CircularScoreChart(score: Int) { + Box(contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(120.dp)) { + val strokeWidth = 12.dp.toPx() + val radius = (size.minDimension - strokeWidth) / 2 + val center = Offset(size.width / 2, size.height / 2) + + // 背景圆 + drawCircle( + color = Color(0xFFE0E0E0), + radius = radius, + center = center, + style = Stroke(strokeWidth) + ) + + // 进度圆弧 + val sweepAngle = (score / 100f) * 360f + drawArc( + color = Color(0xFF5E35B1), + startAngle = -90f, + sweepAngle = sweepAngle, + useCenter = false, + style = Stroke(strokeWidth, cap = StrokeCap.Round), + topLeft = Offset(center.x - radius, center.y - radius), + size = Size(radius * 2, radius * 2) + ) + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = score.toString(), + fontSize = 36.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF333333) + ) + Text( + text = "运动得分", + fontSize = 14.sp, + color = Color(0xFF666666) + ) + } + } +} + +@Composable +fun ScoreIndicator( label: String, - value: String + value: String, + progress: Float, + color: Color ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + fontSize = 14.sp, + color = Color(0xFF666666) + ) + Spacer(modifier = Modifier.height(4.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .background(Color(0xFFE0E0E0), RoundedCornerShape(3.dp)) + ) { + Box( + modifier = Modifier + .fillMaxWidth(progress) + .fillMaxHeight() + .background(color, RoundedCornerShape(3.dp)) + ) + } + } + + Spacer(modifier = Modifier.width(16.dp)) + Text( text = value, - color = Color.White, fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = label, - color = Color.Gray, - fontSize = 12.sp + fontWeight = FontWeight.Medium, + color = color ) } } -data class ExerciseData( - val type: String, - val date: String, - val duration: String, - val calories: Int, - val distance: Float -) - -@Preview @Composable -private fun ExerciseScreenPreview() { - MaterialTheme { - ExerciseScreen() +fun InsightSection() { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color(0xFF5E35B1)) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "解读和建议", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "对于初跑者,建议采用MAF180训练法或储备心率百分比的区间,最大心率百分比的Z3区间进行慢跑或其他有氧跑练,这种低强度的运动有助于减重或提高储,同时能够较好地保持较长时间的有氧运动状态,高步频可以降低足部水平承受伤并提升跑步效率,高步频可以降低减重或不适,使可以说一些话的跑步状态。在增大跑量阶段跑者可承受的压力,增加落地的次数,在跑时过程中更容易驾驭承受伤并提升跑速度。", + fontSize = 14.sp, + color = Color.White.copy(alpha = 0.9f), + lineHeight = 20.sp + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseViewModel.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseViewModel.kt index a91334c..8b00bbe 100644 --- a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/exercise/ExerciseViewModel.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow data class ExerciseUiState( - val exerciseData: List = emptyList(), +// val exerciseData: List = emptyList(), val isLoading: Boolean = false ) @@ -21,38 +21,38 @@ class ExerciseViewModel : ViewModel() { private fun loadExerciseData() { // 模拟加载运动数据 - val mockData = listOf( - ExerciseData( - type = "跑步", - date = "今天", - duration = "30分钟", - calories = 245, - distance = 3.2f - ), - ExerciseData( - type = "骑行", - date = "昨天", - duration = "45分钟", - calories = 320, - distance = 8.5f - ), - ExerciseData( - type = "游泳", - date = "2天前", - duration = "25分钟", - calories = 180, - distance = 1.0f - ), - ExerciseData( - type = "健走", - date = "3天前", - duration = "60分钟", - calories = 150, - distance = 4.8f - ) - ) +// val mockData = listOf( +// ExerciseData( +// type = "跑步", +// date = "今天", +// duration = "30分钟", +// calories = 245, +// distance = 3.2f +// ), +// ExerciseData( +// type = "骑行", +// date = "昨天", +// duration = "45分钟", +// calories = 320, +// distance = 8.5f +// ), +// ExerciseData( +// type = "游泳", +// date = "2天前", +// duration = "25分钟", +// calories = 180, +// distance = 1.0f +// ), +// ExerciseData( +// type = "健走", +// date = "3天前", +// duration = "60分钟", +// calories = 150, +// distance = 4.8f +// ) +// ) - _uiState.value = _uiState.value.copy(exerciseData = mockData) +// _uiState.value = _uiState.value.copy(exerciseData = mockData) } fun refresh() { diff --git a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/state/components/ExerciseGoalCard.kt b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/state/components/ExerciseGoalCard.kt index 200ba57..5c03d0a 100644 --- a/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/state/components/ExerciseGoalCard.kt +++ b/composeApp/src/commonMain/kotlin/com/whitefish/app/ui/home/state/components/ExerciseGoalCard.kt @@ -15,12 +15,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview import ringappkmp.composeapp.generated.resources.Res import ringappkmp.composeapp.generated.resources.bg_exercise_target -import ringappkmp.composeapp.generated.resources.compose_multiplatform data class ExerciseGoalData( val title: String = "运动目标", diff --git a/composeApp/src/iosMain/kotlin/com/whitefish/app/Platform.ios.kt b/composeApp/src/iosMain/kotlin/com/whitefish/app/Platform.ios.kt index a83844d..0f2568f 100644 --- a/composeApp/src/iosMain/kotlin/com/whitefish/app/Platform.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/whitefish/app/Platform.ios.kt @@ -1,9 +1,52 @@ package com.whitefish.app import platform.UIKit.UIDevice +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp class IOSPlatform: Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } -actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file +actual fun getPlatform(): Platform = IOSPlatform() + +// iOS状态栏高度 +@Composable +actual fun getStatusBarHeight(): Dp { + // 根据设备类型返回合适的状态栏高度 + return when (UIDevice.currentDevice.model) { + "iPhone" -> { + // iPhone的状态栏高度通常为44dp(包括刘海屏) + 44.dp + } + "iPad" -> { + // iPad的状态栏高度通常为24dp + 24.dp + } + else -> { + // 默认值 + 44.dp + } + } +} + +// iOS导航栏高度(底部安全区域) +@Composable +actual fun getNavigationBarHeight(): Dp { + // 根据设备类型返回合适的底部安全区域高度 + return when (UIDevice.currentDevice.model) { + "iPhone" -> { + // iPhone X及以上机型的底部安全区域高度 + 34.dp + } + "iPad" -> { + // iPad通常没有底部安全区域 + 0.dp + } + else -> { + // 默认值,适用于有Home键的iPhone + 0.dp + } + } +} \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 2aed99a..c4fe6f7 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -376,4 +376,4 @@ /* End XCConfigurationList section */ }; rootObject = B9DA97A92DC1472C00A4DA20 /* Project object */; -} \ No newline at end of file +} diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 3cd5c32..6cd0267 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -2,9 +2,65 @@ import UIKit import SwiftUI import ComposeApp +// 自定义UIViewController来处理状态栏 +class TransparentStatusBarViewController: UIViewController { + private let composeViewController: UIViewController + + init() { + self.composeViewController = MainViewControllerKt.MainViewController() + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + // 设置视图背景透明 + view.backgroundColor = UIColor.clear + + // 添加Compose视图控制器作为子控制器 + addChild(composeViewController) + view.addSubview(composeViewController.view) + composeViewController.didMove(toParent: self) + + // 设置约束,让Compose视图填满整个屏幕 + composeViewController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + composeViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + composeViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + composeViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + composeViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + // 设置Compose视图背景透明 + composeViewController.view.backgroundColor = UIColor.clear + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent // 白色状态栏文字 + } + + override var prefersStatusBarHidden: Bool { + return false // 显示状态栏 + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // 确保状态栏样式更新 + setNeedsStatusBarAppearanceUpdate() + + // 配置导航控制器(如果存在) + navigationController?.setNavigationBarHidden(true, animated: false) + } +} + struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { - MainViewControllerKt.MainViewController() + return TransparentStatusBarViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} @@ -13,9 +69,14 @@ struct ComposeView: UIViewControllerRepresentable { struct ContentView: View { var body: some View { ComposeView() - .ignoresSafeArea(.keyboard) // Compose has own keyboard handler + .ignoresSafeArea(.all) // 让内容延伸到所有安全区域,包括状态栏和导航栏 + .preferredColorScheme(.light) // 设置为浅色模式以确保状态栏文字可见性 } } +#Preview { + ContentView() +} + diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index 11845e1..a2f7128 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -4,5 +4,18 @@ CADisableMinimumFrameDurationOnPhone + UIStatusBarStyle + UIStatusBarStyleLightContent + UIViewControllerBasedStatusBarAppearance + + UIStatusBarHidden + + UILaunchScreen + + UIColorName + + UIImageName + + diff --git a/iosApp/iosApp/StatusBarConfig.swift b/iosApp/iosApp/StatusBarConfig.swift new file mode 100644 index 0000000..2dbc4d0 --- /dev/null +++ b/iosApp/iosApp/StatusBarConfig.swift @@ -0,0 +1,20 @@ +import SwiftUI +import UIKit + +// 扩展UIApplication来配置状态栏 +extension UIApplication { + static func configureStatusBar() { + // 确保状态栏文字为白色 + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + // 配置所有窗口 + for window in windowScene.windows { + window.backgroundColor = UIColor.clear + + // 确保根视图控制器更新状态栏外观 + if let rootViewController = window.rootViewController { + rootViewController.setNeedsStatusBarAppearanceUpdate() + } + } + } + } +} \ No newline at end of file diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index d83dca6..1317d26 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -5,6 +5,10 @@ struct iOSApp: App { var body: some Scene { WindowGroup { ContentView() + .onAppear { + // 使用新的状态栏配置 + UIApplication.configureStatusBar() + } } } } \ No newline at end of file