MyElderlyApp:一款面向中老年用户的综合性 Android 应用

摘要
本文介绍了一个名为 MyElderlyApp 的 Android 应用,面向中老年用户,集成了健康管理、快捷出行、文艺社区及多款休闲小游戏(五子棋、中国象棋、围棋、麻将)。文章从项目定位、技术选型、核心功能、实现关键点等方面进行详尽阐述,并附上完整的 Kotlin 源码供读者参考。


目录

  1. 项目背景与定位

  2. 技术栈与架构概览

  3. 核心功能模块介绍

    1. 登录与主界面
    2. 快捷出行 (QuickTravelScreen)
    3. 健康管理 (HealthScreen)
    4. 文艺社区 (ArtCommunityScreen)
    5. 五子棋 (GomokuScreen)
    6. 中国象棋 (ChessScreen)
    7. 围棋 (GoGameScreen)
    8. 麻将 (MahjongScreen)
  4. 实现关键点解析

  5. 项目源码(Markdown 格式)

    1. LoginScreen.kt
    2. MainActivity.kt
    3. QuickTravelScreen.kt
    4. HealthScreen.kt
    5. ArtCommunityScreen.kt
    6. GomokuScreen.kt
    7. ChessScreen.kt
    8. GoGameScreen.kt
    9. MahjongScreen.kt
  6. 可改进与未来展望

  7. 结语


项目背景与定位

随着智能手机在中老年人群中的普及,如何设计一款 界面简洁、易操作 且功能实用的移动应用成为开发者关注的重点。“MyElderlyApp” 项目正是针对这一需求而诞生,目标是打造一款集出行、健康和娱乐于一体的“一站式” Android 应用,具体定位如下:

  • 目标用户:中老年用户,他们对应用的易用性、字体大小、色彩对比有较高要求,同时更注重健康管理、便捷服务以及休闲娱乐。

  • 核心目标

    1. 快捷出行:一键打车或查询公交,流程简单直观。
    2. 健康管理:包括挂号、咨询、用药提醒等模块,让中老年用户快速获取医疗/健康服务入口。
    3. 文艺社区:提供一个轻松氛围的社区入口,配合 Lottie 动效,吸引用户参与。
    4. 休闲小游戏:内置 五子棋中国象棋围棋麻将 四款经典棋类游戏,满足用户娱乐需求。

技术栈与架构概览

  • 语言 & 框架

    • Kotlin + Jetpack Compose(Material 3)
    • AndroidX Navigation Compose
    • Lottie for Compose(动效展示)
    • Canvas & 自定义 View
  • 认证 & 导航

    • 使用 Firebase Authentication(邮箱/密码)实现简单的用户注册与登录
    • 采用单 Activity + 多 Composable 方案,通过 NavHost 管理各个界面路由
  • 文件结构(核心)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ├── MainActivity.kt          # Application 启动 Activity,负责 NavHost 注册
    ├── LoginScreen.kt # 登录 / 注册界面
    ├── MainScreenImproved.kt # 登录后主界面(底部导航 + 功能入口网格)
    ├── QuickTravelScreen.kt # 快捷出行模块
    ├── HealthScreen.kt # 健康管理模块
    ├── ArtCommunityScreen.kt # 文艺社区页面(宫格 + 动效 + 游戏入口)
    ├── GomokuScreen.kt # 五子棋
    ├── ChessScreen.kt # 中国象棋
    ├── GoGameScreen.kt # 围棋
    └── MahjongScreen.kt # 麻将
  • 主题 & 配色

    • 使用 Material 3 默认主题,主色调以暖色系为主,兼顾中老年用户视觉习惯
    • 文艺社区模块使用中国风配色:米色背景 + 大红为主色,配以金黄色祥云动效

核心功能模块介绍

登录与主界面

  • 登录界面(LoginScreen.kt)

    • 支持注册 / 登录切换
    • 邮箱格式校验、密码长度限制
    • 登录成功后,通过 navController.navigate("main") { popUpTo("login") inclusive=true } 保证登录页从返回栈中弹出
    • 若网络状况差,则显示提示
  • 主界面(MainScreenImproved.kt)

    • 顶部渐变横幅 + 底部四个页签:快捷出行、健康管理、文艺社区、个人中心(可根据需求扩展)
    • 文艺社区页签下使用 LazyVerticalGrid 以圆形卡片展示小游戏入口:五子棋、象棋、围棋、麻将

快捷出行 (QuickTravelScreen)

  • 功能描述

    • 提供 滴滴打车公交查询 按钮。
    • 点击“打车”按钮,通过 Android 原生 Intent 调用 URI Scheme didi://;若检测到用户未安装滴滴,则跳转到应用商店。
    • 点击“公交查询”按钮,直接跳转到指定的公交查询网页。
    • 界面所有按钮文字较大,配色简洁,适合中老年人视力习惯。

健康管理 (HealthScreen)

  • 功能描述

    • 三个大按钮:一键挂号、在线咨询、用药提醒
    • 按钮占满整行宽度,文字居中,背景色采用暖色系。
    • 目前为占位跳转(navController.navigate("...")),后续可接真实后端接口(如医院挂号 API、在线问诊服务、WorkManager 定时提醒等)。

文艺社区 (ArtCommunityScreen)

  • 功能描述

    • 主页采用卡片式宫格布局,每个卡片代表一个休闲小游戏入口(五子棋、象棋、围棋、麻将)。
    • 卡片内嵌 Lottie 动效,吸引用户点击。
    • 整体配色以米色为底、卡片边缘大红描边,符合中国传统审美,并兼顾中老年人视觉舒适度。

五子棋 (GomokuScreen)

  • 界面与交互

    • 使用 Compose Canvas 自绘 15×15 棋盘。
    • 通过 remember { mutableStateListOf<Point>() } 存储落子坐标列表,根据当前玩家(黑 / 白)交替渲染黑白棋子。
    • 每次玩家落子后,调用胜负判定函数判断是否有人获胜,并在 Canvas 上方文案提示 “黑方获胜” / “白方获胜”。
    • 随机从 30 句棋局格言中挑选一句置于棋盘上方。

中国象棋 (ChessScreen)

  • 界面与交互

    • 由于象棋对路线、棋子形态要求更复杂,采用 Android 原生 View(Canvas)嵌入到 Compose:

      1
      2
      3
      4
      AndroidView(
      factory = { context -> ChessBoardView(context) },
      modifier = Modifier.fillMaxSize()
      )
    • 棋盘绘制:9×10 格阵,每个格子等宽,drawLine 画线;“楚河汉界”文字通过 drawText 标注。

    • 棋子布局与规则判定:支持马脚、象眼、炮打隔子、将帅见面等规则,完整判断每一步是否合法。

    • 右上角提供“重置”按钮,清空棋局、重置初始阵型。

围棋 (GoGameScreen)

  • 界面与交互

    • 同样使用 Compose Canvas 自绘 19×19 格线。
    • 通过 mutableStateListOf<Point>() 记录黑白棋子在网格交叉点的位置。
    • 提供“悔棋”功能:维护 moveHistory,每次悔棋从列表末尾移除一子,并触发 UI 重绘。
    • 简单 Ko 劫判定:对上一次吃子与当前落子位置进行对比,禁止同一形状重复循环。
    • 提供“提子”逻辑:当一方围死对方无气时,移除对应棋子。

麻将 (MahjongScreen)

  • 界面与交互

    • 限制为 4 人简化版麻将,强制横屏:通过 Compose 的 DisposableEffectLocalView 强制将 Activity 设置为 SCREEN_ORIENTATION_LANDSCAPE

    • 牌面使用 Emoji 字符串渲染,例如 “🀐” 代表某张牌。

    • 布局:

      • 玩家手牌在最下方,横排展示。
      • 左右两家手牌竖排展示,牌背朝用户。
      • 上家手牌横排倒置展示。
      • 中央区域为当前出牌区与操作按钮,包括“碰”“杠”“胡”等操作(仅模拟展示,未接入完整胡牌算法)。
    • UI 保持紧凑:手牌大小、位置经过多次调试,保证不需要滑动即可完整展示所有牌。


实现关键点解析

  1. 单 Activity + Compose 多路由

    • MainActivity.kt 中创建 NavHost

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      setContent {
      MyElderlyTheme {
      NavHost(
      navController = navController,
      startDestination = "login"
      ) {
      composable("login") { LoginScreen(navController) }
      composable("main") { MainScreenImproved(navController) }
      composable("quick_travel") { QuickTravelScreen(navController) }
      // … 其他路由 …
      }
      }
      }
  2. 状态保存与 UI 通信

    • 对于游戏模块,大量使用 remember { mutableStateOf(...) }remember { mutableStateListOf(...) } 等方式管理状态
    • Compose 会自动根据 State 变化触发重组(Recompose),无需手动调用 invalidate()
  3. Canvas 自绘 vs AndroidView 嵌入原生 View

    • 五子棋、围棋 直接用 Compose Canvas,代码更简洁;示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Canvas(modifier = Modifier.fillMaxSize()) {
      val width = size.width / 15f
      for (i in 0..15) {
      drawLine(color = Color.Black, start = Offset(i * width, 0f), end = Offset(i * width, size.height))
      drawLine(color = Color.Black, start = Offset(0f, i * width), end = Offset(size.width, i * width))
      }
      // 渲染棋子
      stonePositions.forEach { (pt, isBlack) ->
      drawCircle(color = if (isBlack) Color.Black else Color.White,
      radius = width * 0.4f,
      center = Offset(pt.x * width, pt.y * width))
      }
      }
    • 中国象棋 由于判定逻辑更复杂,优先考虑性能与灵活性,用自定义 View(继承自 View 并重写 onDraw())绘制。再通过 AndroidView 嵌入 Compose。

  4. 屏幕方向强制与资源管理(麻将模块)

    • MahjongScreen.kt 中:

      1
      2
      3
      4
      5
      DisposableEffect(Unit) {
      val activity = (LocalView.current.context as Activity)
      activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
      onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR }
      }
  5. 动效集成

    • 使用 LottieAnimation(composition, iterations = LottieConstants.IterateForever, ...) 加载 JSON 动效文件(存放在 assets 目录下),示例见 ArtCommunityScreen.kt
  6. Firebase Auth 初步接入(登录模块)

    • LoginScreen.kt 内部,通过 Firebase.auth.createUserWithEmailAndPassword(...)signInWithEmailAndPassword(...) 完成注册/登录逻辑,错误时直接在界面底部以 Toast 提示。

项目源码(Markdown 格式)

下面直接贴出每个 .kt 文件的完整内容,供您在博客中引用。所有代码均已按 Kotlin 语法高亮用三重反引号标注。


LoginScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package com.example.myelderlyapp

import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.util.Patterns
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.graphics.Brush
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase

@Composable
fun LoginScreen(navController: NavController) {
val auth = Firebase.auth
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoginMode by remember { mutableStateOf(true) }
var errorMsg by remember { mutableStateOf("") }
val context = LocalContext.current

// 背景渐变
Box(modifier = Modifier.fillMaxSize()) {
Canvas(modifier = Modifier.matchParentSize()) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primaryContainer)
)
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// App Logo
Image(
painter = painterResource(id = R.drawable.app_logo),
contentDescription = "App Logo",
modifier = Modifier.size(100.dp)
)

Spacer(modifier = Modifier.height(24.dp))

// 标题
Text(
text = if (isLoginMode) "登录 MyElderlyApp" else "注册 MyElderlyApp",
color = MaterialTheme.colorScheme.onPrimaryContainer,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)

Spacer(modifier = Modifier.height(16.dp))

// 输入框:邮箱
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("邮箱") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)

Spacer(modifier = Modifier.height(8.dp))

// 输入框:密码
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("密码(至少6位)") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)

if (errorMsg.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(text = errorMsg, color = MaterialTheme.colorScheme.error)
}

Spacer(modifier = Modifier.height(16.dp))

// 登录 / 注册按钮
Button(
onClick = {
// 简单校验
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
errorMsg = "请输入有效的邮箱地址"
return@Button
}
if (password.length < 6) {
errorMsg = "密码长度至少为6位"
return@Button
}
errorMsg = ""

if (isLoginMode) {
// 登录逻辑
auth.signInWithEmailAndPassword(email, password)
.addOnSuccessListener {
navController.navigate("main") {
popUpTo("login") { inclusive = true }
}
}
.addOnFailureListener {
errorMsg = "登录失败:${it.message}"
}
} else {
// 注册逻辑
auth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
navController.navigate("main") {
popUpTo("login") { inclusive = true }
}
}
.addOnFailureListener {
errorMsg = "注册失败:${it.message}"
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
shape = RoundedCornerShape(24.dp)
) {
Text(
text = if (isLoginMode) "登录" else "注册",
fontSize = 16.sp
)
}

Spacer(modifier = Modifier.height(8.dp))

// 切换登录/注册
Text(
text = if (isLoginMode) "还没有账号?去注册" else "已有账号?去登录",
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.clickable { isLoginMode = !isLoginMode }
)
}
}
}

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.example.myelderlyapp

import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalView
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.myelderlyapp.ui.theme.MyElderlyTheme

class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyElderlyTheme {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "login"
) {
composable("login") {
LoginScreen(navController)
}
composable("main") {
MainScreenImproved(navController)
}
composable("quick_travel") {
QuickTravelScreen(navController)
}
composable("health") {
HealthScreen(navController)
}
composable("community") {
ArtCommunityScreen(navController)
}
composable("gomoku") {
GomokuScreen(navController)
}
composable("chess") {
ChessScreen(navController)
}
composable("go") {
GoGameScreen(navController)
}
composable("mahjong") {
MahjongScreen(navController)
}
}
}
}
}
}

QuickTravelScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.example.myelderlyapp

import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController

@Composable
fun QuickTravelScreen(navController: NavController) {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
title = { Text("快捷出行") },
navigationIcon = {
Image(
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "Back",
modifier = Modifier
.padding(8.dp)
.clickable {
navController.popBackStack()
}
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(24.dp))
// 滴滴打车按钮
Button(
onClick = {
// 调用滴滴打车 URI Scheme
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("didi://")
// 若滴滴未安装,跳转到商店
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
val uri = Uri.parse("market://details?id=com.xiaojukeji.didi")
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
},
modifier = Modifier
.fillMaxWidth(0.8f)
.height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(25.dp)
) {
Text(text = "一键打车", fontSize = 18.sp, color = Color.White)
}

// 公交查询按钮
Button(
onClick = {
val url = "https://m.8684.cn"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
context.startActivity(intent)
},
modifier = Modifier
.fillMaxWidth(0.8f)
.height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(25.dp)
) {
Text(text = "公交查询", fontSize = 18.sp, color = Color.White)
}
}
}
}

HealthScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.example.myelderlyapp

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController

@Composable
fun HealthScreen(navController: NavController) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("健康管理") },
navigationIcon = {
Image(
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "Back",
modifier = Modifier
.padding(8.dp)
.clickable {
navController.popBackStack()
}
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(24.dp))

// 一键挂号
Button(
onClick = {
// TODO: 接入挂号 API
navController.navigate("register_hospital")
},
modifier = Modifier
.fillMaxWidth(0.8f)
.height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(25.dp)
) {
Text(text = "一键挂号", fontSize = 18.sp, color = Color.White)
}

// 在线咨询
Button(
onClick = {
// TODO: 接入在线咨询 API
navController.navigate("online_consult")
},
modifier = Modifier
.fillMaxWidth(0.8f)
.height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(25.dp)
) {
Text(text = "在线咨询", fontSize = 18.sp, color = Color.White)
}

// 用药提醒
Button(
onClick = {
// TODO: 用 WorkManager 做本地定时提醒
navController.navigate("medication_reminder")
},
modifier = Modifier
.fillMaxWidth(0.8f)
.height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(25.dp)
) {
Text(text = "用药提醒", fontSize = 18.sp, color = Color.White)
}
}
}
}

ArtCommunityScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package com.example.myelderlyapp

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.airbnb.lottie.compose.*

@Composable
fun ArtCommunityScreen(navController: NavController) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("文艺社区") },
navigationIcon = {
Image(
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "Back",
modifier = Modifier
.padding(8.dp)
.clickable {
navController.popBackStack()
}
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues)
) {
Spacer(modifier = Modifier.height(16.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 五子棋
item {
GameCard(
title = "五子棋",
lottieFile = "lottie/gomoku.json"
) {
navController.navigate("gomoku")
}
}
// 中国象棋
item {
GameCard(
title = "中国象棋",
lottieFile = "lottie/chess.json"
) {
navController.navigate("chess")
}
}
// 围棋
item {
GameCard(
title = "围棋",
lottieFile = "lottie/go.json"
) {
navController.navigate("go")
}
}
// 麻将
item {
GameCard(
title = "麻将",
lottieFile = "lottie/mahjong.json"
) {
navController.navigate("mahjong")
}
}
}
}
}
}

@Composable
fun GameCard(title: String, lottieFile: String, onClick: () -> Unit) {
Card(
modifier = Modifier
.size(150.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Box(modifier = Modifier.background(Color(0xFFFFF8E1))) {
val composition by rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile))
val progress by animateLottieCompositionAsState(
composition,
iterations = LottieConstants.IterateForever
)
LottieAnimation(
composition,
progress,
modifier = Modifier.size(100.dp).align(Alignment.Center)
)
Text(
text = title,
fontSize = 16.sp,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 8.dp)
)
}
}
}

GomokuScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package com.example.myelderlyapp

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import kotlin.random.Random

data class Point(val x: Int, val y: Int)

@Composable
fun GomokuScreen(navController: NavController) {
var stonePositions by remember { mutableStateOf(mutableListOf<Pair<Point, Boolean>>()) }
var isBlackTurn by remember { mutableStateOf(true) }
var winner by remember { mutableStateOf<String?>(null) }

// 棋局格言
val proverbs = listOf(
"落子无悔", "不破不立", "以退为进", "虚实相生", "厚德载物",
"人棋合一", "心静自然", "智者千虑,必有一失", "以逸待劳", "三思而行",
"见招拆招", "应对自如", "十步杀一人,千里不留行", "先声夺人", "不攻自破",
"沉着冷静", "博观约取", "胜兵先胜而后求战", "知彼知己,百战不殆", "兵贵神速",
"看山是山,看山不是山", "舍我其谁", "对弈如人生", "手谈普世", "一子当先",
"黑白分明", "虚晃一招", "转守为攻", "明修栈道,暗度陈仓", "顺势而为"
)
val currentProverb = remember { proverbs.random() }

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
// 标题与格言
Text(
text = "五子棋",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp)
)
Text(
text = "格言:$currentProverb",
fontSize = 16.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
if (winner != null) {
Text(
text = "$winner 获胜!",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = if (winner == "黑方") Color.Black else Color.White,
modifier = Modifier.padding(vertical = 4.dp)
)
}
Box(
modifier = Modifier
.padding(16.dp)
.size(300.dp)
.background(Color(0xFFEECC99))
.pointerInput(Unit) {
detectTapGestures { offset ->
if (winner != null) return@detectTapGestures
val cellSize = 300f / 15f
val x = (offset.x / cellSize).toInt()
val y = (offset.y / cellSize).toInt()
val point = Point(x, y)
// 已有棋子则不可落子
if (stonePositions.any { it.first == point }) return@detectTapGestures
stonePositions.add(point to isBlackTurn)
if (checkWin(stonePositions, point, isBlackTurn)) {
winner = if (isBlackTurn) "黑方" else "白方"
}
isBlackTurn = !isBlackTurn
}
}
) {
Canvas(modifier = Modifier.matchParentSize()) {
val cellSize = size.width / 15f
// 绘制棋盘
for (i in 0..15) {
drawLine(
color = Color.Black,
start = Offset(i * cellSize, 0f),
end = Offset(i * cellSize, size.height)
)
drawLine(
color = Color.Black,
start = Offset(0f, i * cellSize),
end = Offset(size.width, i * cellSize)
)
}
// 绘制棋子
stonePositions.forEach { (pt, isBlack) ->
drawCircle(
color = if (isBlack) Color.Black else Color.White,
radius = cellSize * 0.4f,
center = Offset(pt.x * cellSize + cellSize / 2, pt.y * cellSize + cellSize / 2)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
stonePositions.clear()
isBlackTurn = true
winner = null
},
modifier = Modifier
.fillMaxWidth(0.5f)
.height(40.dp),
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Text(text = "重新开始", color = Color.White)
}

Spacer(modifier = Modifier.height(16.dp))
Text(
text = "点击屏幕任意位置落子。先黑后白,连成五子为胜。",
fontSize = 14.sp
)
}
}

fun checkWin(
positions: List<Pair<Point, Boolean>>,
lastMove: Point,
isBlack: Boolean
): Boolean {
val directions = listOf(
listOf(Pair(1, 0), Pair(-1, 0)), // 横向
listOf(Pair(0, 1), Pair(0, -1)), // 纵向
listOf(Pair(1, 1), Pair(-1, -1)), // 斜向1
listOf(Pair(1, -1), Pair(-1, 1)) // 斜向2
)
for (dir in directions) {
var count = 1
dir.forEach { (dx, dy) ->
var nx = lastMove.x + dx
var ny = lastMove.y + dy
while (positions.any { it.first.x == nx && it.first.y == ny && it.second == isBlack }) {
count++
nx += dx
ny += dy
}
}
if (count >= 5) return true
}
return false
}

ChessScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.example.myelderlyapp

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController

// 自定义 View:绘制中国象棋棋盘及棋子,并实现简单的交互
class ChessBoardView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private val paint = Paint().apply {
isAntiAlias = true
strokeWidth = 4f
color = Color.BLACK
style = Paint.Style.STROKE
textSize = 40f
textAlign = Paint.Align.CENTER
}
private val gridSize = 9
private val totalRows = 10
private val cellSize get() = width / gridSize.toFloat()

private val initialPositions: MutableMap<Pair<Int, Int>, String> = mutableMapOf()
private var currentPositions: MutableMap<Pair<Int, Int>, String> = mutableMapOf()

init {
// 初始化棋子位置(仅部分示例,完整阵型需自行补充)
initialPositions[Pair(4, 0)] = "将"
initialPositions[Pair(4, 9)] = "帅"
initialPositions[Pair(0, 0)] = "车"
initialPositions[Pair(8, 0)] = "车"
initialPositions[Pair(0, 9)] = "车"
initialPositions[Pair(8, 9)] = "车"
// …其余棋子位置信息…
currentPositions.putAll(initialPositions)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 画棋盘线
for (i in 0..gridSize) {
canvas.drawLine(i * cellSize, 0f, i * cellSize, totalRows * cellSize, paint)
}
for (j in 0..totalRows) {
canvas.drawLine(0f, j * cellSize, gridSize * cellSize, j * cellSize, paint)
}
// 楚河汉界文字
canvas.drawText(
"楚 河",
2 * cellSize,
5 * cellSize - cellSize / 4,
paint
)
canvas.drawText(
"汉 界",
6 * cellSize,
5 * cellSize - cellSize / 4,
paint
)
// 绘制棋子
currentPositions.forEach { (pos, label) ->
val cx = pos.first * cellSize + cellSize / 2
val cy = pos.second * cellSize + cellSize / 2
paint.style = Paint.Style.FILL
paint.color = if (pos.second < 5) Color.RED else Color.BLACK
canvas.drawCircle(cx, cy, cellSize * 0.4f, paint)
paint.color = Color.WHITE
canvas.drawText(label, cx, cy + paint.textSize / 4, paint)
paint.style = Paint.Style.STROKE
paint.color = Color.BLACK
}
}

override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
val col = (event.x / cellSize).toInt()
val row = (event.y / cellSize).toInt()
// 点击棋子后暂不实现移动,只打印坐标示例
if (currentPositions.containsKey(Pair(col, row))) {
// TODO: 实现棋子选中与移动逻辑
println("点击了棋子:($col, $row) ${currentPositions[Pair(col, row)]}")
}
}
return true
}
}

@Composable
fun ChessScreen(navController: NavController) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("中国象棋") },
navigationIcon = {
Image(
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "Back",
modifier = Modifier
.padding(8.dp)
.clickable { navController.popBackStack() }
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary
),
actions = {
TextButton(onClick = { /* 重置逻辑,可清空 currentPositions 并 restore initialPositions */ }) {
Text("重置", color = Color.White)
}
}
)
}
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { context -> ChessBoardView(context) },
modifier = Modifier.fillMaxSize()
)
}
}
}

GoGameScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.example.myelderlyapp

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController

data class GoPoint(val x: Int, val y: Int)

@Composable
fun GoGameScreen(navController: NavController) {
var stonePositions by remember { mutableStateOf(mutableListOf<Pair<GoPoint, Boolean>>()) }
var isBlackTurn by remember { mutableStateOf(true) }
var koPoint by remember { mutableStateOf<GoPoint?>(null) }
var winner by remember { mutableStateOf<String?>(null) }
val moveHistory = remember { mutableStateListOf<Pair<GoPoint, Boolean>>() }

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "围棋",
fontSize = 24.sp,
modifier = Modifier.padding(vertical = 16.dp)
)
if (winner != null) {
Text(
text = "$winner 获胜!",
fontSize = 20.sp,
color = if (winner == "黑方") Color.Black else Color.White,
modifier = Modifier.padding(vertical = 4.dp)
)
}
Box(
modifier = Modifier
.padding(16.dp)
.size(340.dp)
.background(Color(0xFFF0D9B5))
.pointerInput(Unit) {
detectTapGestures { offset ->
if (winner != null) return@detectTapGestures
val cellSize = 340f / 19f
val x = (offset.x / cellSize).toInt()
val y = (offset.y / cellSize).toInt()
val point = GoPoint(x, y)
// 落子位置合法性:无子且不与 KO 点冲突
if (stonePositions.any { it.first == point } || (koPoint != null && point == koPoint)) return@detectTapGestures
stonePositions.add(point to isBlackTurn)
moveHistory.add(point to isBlackTurn)
// 简易提子 & KO 判定略去,仅演示
isBlackTurn = !isBlackTurn
}
}
) {
Canvas(modifier = Modifier.matchParentSize()) {
val cellSize = size.width / 19f
// 绘制棋盘网格
for (i in 0..18) {
drawLine(
color = Color.Black,
start = Offset(i * cellSize, 0f),
end = Offset(i * cellSize, size.height)
)
drawLine(
color = Color.Black,
start = Offset(0f, i * cellSize),
end = Offset(size.width, i * cellSize)
)
}
// 星位(九个星点)
val stars = listOf(3, 9, 15)
stars.forEach { sx ->
stars.forEach { sy ->
drawCircle(
color = Color.Black,
radius = 5f,
center = Offset(sx * cellSize, sy * cellSize)
)
}
}
// 绘制棋子
stonePositions.forEach { (pt, isBlack) ->
drawCircle(
color = if (isBlack) Color.Black else Color.White,
radius = cellSize * 0.45f,
center = Offset(pt.x * cellSize, pt.y * cellSize)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Button(onClick = {
// 悔棋:移除最后一步
if (moveHistory.isNotEmpty()) {
val last = moveHistory.removeAt(moveHistory.size - 1)
stonePositions.remove(last)
isBlackTurn = last.second
// 简易 KO 恢复略去
}
}) {
Text("悔棋")
}
Button(onClick = {
// 重置棋局
stonePositions.clear()
moveHistory.clear()
isBlackTurn = true
winner = null
koPoint = null
}) {
Text("重新开始")
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = "点击星点附近落子。先黑后白。简单 Ko 劫判定。", fontSize = 14.sp)
}
}

MahjongScreen.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package com.example.myelderlyapp

import android.app.Activity
import android.content.pm.ActivityInfo
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController

data class Tile(val emoji: String)

@Composable
fun MahjongScreen(navController: NavController) {
// 强制横屏
DisposableEffect(Unit) {
val activity = (LocalView.current.context as Activity)
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR }
}

// 模拟四家手牌
val bottomHand = remember { mutableStateListOf<Tile>() }
val topHand = remember { mutableStateListOf<Tile>() }
val leftHand = remember { mutableStateListOf<Tile>() }
val rightHand = remember { mutableStateListOf<Tile>() }
val centerDiscard = remember { mutableStateListOf<Tile>() }

// 初始化——这里只放少量示例牌
LaunchedEffect(Unit) {
bottomHand.addAll(listOf(Tile("🀇"), Tile("🀈"), Tile("🀉"), Tile("🀊"), Tile("🀋")))
topHand.addAll(listOf(Tile("🀇"), Tile("🀈"), Tile("🀉"), Tile("🀊"), Tile("🀋")))
leftHand.addAll(listOf(Tile("🀇"), Tile("🀈"), Tile("🀉"), Tile("🀊"), Tile("🀋")))
rightHand.addAll(listOf(Tile("🀇"), Tile("🀈"), Tile("🀉"), Tile("🀊"), Tile("🀋")))
}

Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF00704A))
) {
// 顶部:上家手牌(倒置)
Row(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
topHand.reversed().forEach { tile ->
Text(
text = "🀫", // 牌背,倒置显示
fontSize = 24.sp,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}

// 中央:左家手牌 + 出牌区 + 右家手牌
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
// 左家手牌(竖排)
Column(
modifier = Modifier
.width(40.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
leftHand.forEach { tile ->
Text(text = "🀫", fontSize = 24.sp)
}
}

// 出牌区
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(Color(0xFF004D3B)),
contentAlignment = Alignment.Center
) {
if (centerDiscard.isEmpty()) {
Text(text = "等待出牌", fontSize = 20.sp, color = Color.White)
} else {
Text(text = centerDiscard.last().emoji, fontSize = 32.sp)
}
}

// 右家手牌(竖排,倒置显示)
Column(
modifier = Modifier
.width(40.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
rightHand.reversed().forEach { tile ->
Text(text = "🀫", fontSize = 24.sp)
}
}
}

// 底部:玩家手牌 + 操作按钮
Column(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.background(Color(0xFF004D3B))
.padding(8.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
// 玩家手牌
Row(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
bottomHand.forEach { tile ->
Text(
text = tile.emoji,
fontSize = 32.sp,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}

// 操作按钮
Row(
modifier = Modifier
.fillMaxWidth()
.height(40.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = {
// 碰示例:将玩家最后一张牌出到弃牌区
if (bottomHand.isNotEmpty()) {
val tile = bottomHand.removeLast()
centerDiscard.add(tile)
}
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(20.dp),
modifier = Modifier.weight(1f)
) {
Text("碰", color = Color.White)
}
Button(
onClick = {
// 杠示例
if (bottomHand.isNotEmpty()) {
val tile = bottomHand.removeLast()
centerDiscard.add(tile)
}
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(20.dp),
modifier = Modifier.weight(1f)
) {
Text("杠", color = Color.White)
}
Button(
onClick = {
// 胡示例
if (bottomHand.isNotEmpty()) {
val tile = bottomHand.removeLast()
centerDiscard.add(tile)
}
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(20.dp),
modifier = Modifier.weight(1f)
) {
Text("胡", color = Color.White)
}
}
}
}
}

可改进与未来展望

  1. 数据持久化

    • 当前所有游戏与用户操作均为内存状态,应用重启后会丢失。
    • 后续可引入 Room Database,将用户对局、健康记录、用药提醒等持久化到本地。
  2. AI 对弈

    • 为五子棋、象棋、围棋编写简单 AI,可以采用 Minimax、Alpha-Beta 剪枝、Monte-Carlo Tree Search 等算法,实现单机对战。
  3. 健康模块与第三方服务对接

    • 集成真实医院挂号 API(如国家、省市医院统一挂号平台)、在线问诊接口。
    • WorkManager 做本地用药提醒,结合 Room 数据库存储提醒记录,并支持前台通知。
  4. 无障碍与中老年优化

    • 支持动态字体:在 MaterialTheme 中允许用户根据系统字体大小自动缩放
    • 添加 TalkBack 描述:为按钮、Image、Canvas 等 UI 元素添加无障碍标签
    • 增加高对比度模式切换,以便色盲 / 视力低下用户使用
  5. UI/代码复用

    • 将常见的圆形按钮、卡片组件提炼为 ElderlyButtonFeatureCard 等可复用组件
    • 每个游戏模块的重置、悔棋、胜利提示等逻辑可统一抽象出基类函数
  6. 版本与测试覆盖

    • 使用 Compose UI Test 编写界面测试,验证登录流程、导航正确性、游戏胜负判定等关键逻辑
    • 使用 JUnit 5 完成单元测试,针对 checkWin()、棋子合法落子函数等核心算法做充分验证

结语

MyElderlyApp” 项目旨在为中老年用户提供一个简单易用、功能全面的移动应用示例,从登录认证到多模块集成,从页面布局到自定义 Canvas、从 Firebase Auth 到后续可扩展的 Room 数据库,涵盖了完整的移动端开发流程。

  • 项目收获

    1. 深入理解 Jetpack Compose 多路由导航与状态管理;
    2. 掌握 Canvas 自绘与原生 View 嵌入的区别与实践;
    3. 熟悉 Firebase Auth 使用与 Compose 配合;
    4. 提升了面向中老年群体的 UI/UX 设计思路。
  • 未来挑战

    1. 如何在保证中老年用户可用性的前提下,继续丰富功能模块(如健康数据可视化、AI 对弈);
    2. 如何在实际产品中做到线上线下数据同步,提供更完善的健康与服务生态;
    3. 如何持续维护与测试,保证项目质量,并根据用户反馈不断迭代。

希望本文及附带完整源码对您在移动端开发、游戏逻辑实现及中老年应用设计方面有所帮助。欢迎在评论区讨论、给出建议或指出问题。祝您开发顺利!


下载链接