🟢 阶段一:新手村(语言与工具)
目标:能看懂代码,能让程序跑起来。
Kotlin 基础:只需要学
变量、if/else、for/while、函数、类。协程、泛型、高阶函数统统先跳过。Android Studio:学会点绿色三角形运行、学会看底部的 Logcat(日志,用来排查闪退)。
🟢 阶段二:搭积木(Compose UI)
目标:能画出静态页面。
基础组件:
Text(文字)、Button(按钮)、Image(图片)、TextField(输入框)。布局组件:
Column(竖着排)、Row(横着排)、Box(叠着放)。修饰符:
Modifier(大小、背景色、边距 padding)。💡 你现在就在这个阶段。不要纠结原理,就像搭乐高,能把文字和按钮摆在屏幕正确的位置就算赢。
耗时:2~3周
🟢 阶段三:注入灵魂(状态与交互)
目标:点按钮有反应,数据变了界面跟着变。
核心魔法:
remember和mutableStateOf(状态)。事件:
onClick(点击事件)。任务:自己写一个“计数器”(点一下数字+1),再写一个简单的“待办事项清单”(能输入,能显示在列表里)。
耗时:2~3周
🟢 阶段四:多页面与长列表(进阶UI)
目标:能做多页面的App,能滑长列表。
列表:
LazyColumn(纵向滑动列表)、LazyRow。导航:
Navigation Compose(页面跳转,比如从主页跳到详情页)。任务:写一个假的“商城App”,有首页、商品列表、点击商品能跳到详情页(数据先写死)。
耗时:2~3周
🟢 阶段五:连接真实世界(网络与架构)—— 分水岭
目标:从网上拉取真实数据,把代码写规范。
网络请求:
Retrofit或Ktor。图片加载:
Coil(从网上下载图片显示)。架构模式:
ViewModel+StateFlow(把数据和界面分开管理,这一步是新手到大白的关键)。任务:调用一个免费的天气API,把真实的天气数据显示在你的App上。
耗时:1~2个月
🔴 阶段六:以后再说(高级技能)
本地数据库(Room)、依赖注入(Hilt)、推送、上架Google Play。
(现在完全不用看,看了也会忘,等找到工作或者做毕设时再学。)
kotlin
Column、Row、Button、Modifier
不知道要学多久
计算契代码 无逻辑
阶段二
// ========================================================================= // 1. 包声明 // 作用:告诉电脑,这个代码文件属于哪个文件夹结构(你的项目路径)。 // 注意:这是系统自动生成的,不要改。 // ========================================================================= package com.example.brodcast.compents // ========================================================================= // 2. 导入工具箱(Import) // 作用:代码里用到的每一个组件,都放在特定的工具包里。Import 就是去取工具。 // 语法:import 包名.具体类名 或者 import 包名.* (星号代表把整个包全部拿过来) // ========================================================================= // Compose 的基础画图工具(比如排列、大小、背景、圆角等) import androidx.compose.foundation.Canvas // 画布,用来画坐标轴、线、圆点 import androidx.compose.foundation.background // 背景颜色 import androidx.compose.foundation.clickable // 让某个组件变成可点击的 import androidx.compose.foundation.layout.* // ⭐ 通配符:导入所有布局工具(Column, Row, Box, Spacer, padding, fillMaxSize 等) import androidx.compose.foundation.lazy.LazyColumn // 懒加载列表(列表长了可以滑动,不卡顿) import androidx.compose.foundation.lazy.items // 配合 LazyColumn 用的,用来循环生成列表行 import androidx.compose.foundation.shape.CircleShape // 圆形形状 import androidx.compose.foundation.shape.RoundedCornerShape // 圆角形状 import androidx.compose.foundation.text.KeyboardOptions // 键盘配置 import androidx.compose.material3.* // ⭐ 通配符:导入 Material3 组件库(Text, Button, Card, OutlinedTextField, CardDefaults 等) // Compose 运行时的核心工具(状态管理) import androidx.compose.runtime.* // ⭐ 通配符:导入 @Composable 标签、remember、mutableStateOf、LaunchedEffect 等数据灵魂工具 // 一些基础工具(对齐、修饰符、颜色、文字样式、单位等) import androidx.compose.ui.Alignment // 对齐方式(居中、靠左、靠右等) import androidx.compose.ui.Modifier // 万能修饰符(用来改大小、边距、背景等) import androidx.compose.ui.geometry.Offset // 用来表示画布上的一个坐标点 (x, y) import androidx.compose.ui.graphics.Color // 颜色 import androidx.compose.ui.text.font.FontWeight // 字体粗细(加粗) import androidx.compose.ui.text.input.KeyboardType // 键盘类型(数字、字母、小数) import androidx.compose.ui.unit.dp // 尺寸单位(类似厘米,用于大小、边距) import androidx.compose.ui.unit.sp // 字体大小单位 // ========================================================================= // 3. 数据模型 // 作用:用来装每一个坐标点的数据。 // 语法:data class 是 Kotlin 的特有写法。加上 data 后,电脑会自动帮你生成对比、打印等函数。 // 上下关系:这是一个独立的模型,不包含在任何界面里,但界面里的所有操作都是围绕它转的。 // ========================================================================= data class DataPoint(val x: Float, val y: Float) // ========================================================================= // 4. 界面主函数(大管家) // 作用:所有 UI 界面的入口。 // 语法:@Composable 是固定标签,告诉编译器“这是一个画界面的函数”。 // ========================================================================= @Composable fun LeastSquaresChart() { // ==================== 4.1 状态管理(数据大脑) ==================== // 常识:Compose 是“数据驱动”的。数据变了,界面就会自动刷新,不需要你手动去改文字。 // 语法:var 变量名 by remember { mutableStateOf(初始值) } // 作用:记住这个数据。数据改变时,界面会自动重新画一遍。 // 作用:存储所有输入的点。默认给 5 个点,一打开就能看到图表,不用手动输入。 var points by remember { mutableStateOf( listOf( DataPoint(1f, 1.5f), // 1f 代表 1.0,Float 类型的固定写法 DataPoint(2f, 2.8f), DataPoint(3f, 3.2f), DataPoint(4f, 5.1f), DataPoint(5f, 5.9f) ) ) } // 作用:记住 X 和 Y 输入框里面的文字。初始为空字符串。 var inputX by remember { mutableStateOf("") } var inputY by remember { mutableStateOf("") } // 作用:存储计算出来的斜率(a) 和截距(b)。 var slopeA by remember { mutableStateOf(0f) } var interceptB by remember { mutableStateOf(0f) } // 作用:存储最终格式化好的方程文本(比如 "y = 1.234x + 0.567"),直接显示在屏幕上。 var equationText by remember { mutableStateOf("") } // ==================== 4.2 自动计算(后台小房间) ==================== // 作用:监听 points 列表。只要 points 变了(增加、删除、清空),就会自动触发里面的代码,重新计算最小二乘法。 // 语法:LaunchedEffect(要监听的数据) { 逻辑代码 } LaunchedEffect(points) { // 常识:要画一条直线,至少需要 2 个点。如果不够,直接退出计算。 if (points.size < 2) { equationText = "至少需要 2 个点才能拟合" // return@LaunchedEffect 的意思是:退出这个后台小房间,后面的代码不跑了。 // 为什么不能直接写 return?因为这是匿名函数,必须用 @LaunchedEffect 标签告诉电脑我要退出哪里。 return@LaunchedEffect } // 最小二乘法公式需要用的“求和”变量。初始都为 0.0(Double 类型,精度比 Float 高)。 var sumX = 0.0 var sumY = 0.0 var sumXY = 0.0 var sumX2 = 0.0 val n = points.size // 点的总数量 // 作用:用 for 循环把所有点遍历一遍,把数据累加起来。 for (p in points) { sumX += p.x sumY += p.y sumXY += p.x * p.y sumX2 += p.x * p.x } // 作用:计算分母。如果分母为 0,说明所有的 X 坐标一样,无法画出一条斜线。 val denominator = n * sumX2 - sumX * sumX if (denominator == 0.0) { equationText = "X坐标不能完全相同" return@LaunchedEffect } // 作用:最小二乘法核心公式。计算出斜率 a 和截距 b。 val a = (n * sumXY - sumX * sumY) / denominator val b = (sumY - a * sumX) / n // 作用:把计算结果存进状态里。数据一变,界面自动重绘红线和方程文本。 slopeA = a.toFloat() interceptB = b.toFloat() // 作用:格式化输出,保留三位小数。 equationText = "y = ${"%.3f".format(a)}x + ${"%.3f".format(b)}" } // ==================== 4.3 整体界面布局 ==================== // 作用:最外层的 Column,让所有内容从上往下排列。 // 语法:Modifier 是一节一节拼起来的。fillMaxSize 占满屏幕,padding 四周留白,background 设置浅灰底色。 // verticalArrangement = Arrangement.spacedBy(12.dp) 作用:每个子元素之间自动留出 12dp 的空隙。 Column( modifier = Modifier .fillMaxSize() .padding(16.dp) .background(Color(0xFFF7F7F7)), verticalArrangement = Arrangement.spacedBy(12.dp) ) { // 作用:顶部标题文字。 Text( text = "📈 最小二乘法拟合器", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFF333333) ) // --- 输入卡片 --- // 作用:做一个白色的卡片框,把输入区包起来,带阴影。 Card( modifier = Modifier.fillMaxWidth(), // 撑满宽度 colors = CardDefaults.cardColors(containerColor = Color.White), // 卡片背景色 // elevation 作用:卡片阴影大小。数字越大,卡片浮得越高,影子越深。 // 语法:必须通过 CardDefaults.cardElevation(...) 来设置,不能直接写 2.dp。 elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { // 作用:卡片内部的内容,从上往下排。padding 内部留白 12dp。 Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("添加新数据点", fontSize = 14.sp, fontWeight = FontWeight.Medium) // 作用:把输入框和按钮横着排(Row)。 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), // 横向元素之间留 8dp 空隙 // verticalAlignment 作用:让 Row 里面所有元素在垂直方向居中对齐。 // 比如输入框和按钮高度不一样时,加上这个就会对齐到中间,不会一个高一个低。 verticalAlignment = Alignment.CenterVertically ) { // 作用:X 输入框。 // value 是输入框显示的内容,onValueChange 是用户打字时触发的动作。 // singleLine = true 作用:限制只能输入一行。按回车也不换行,高度固定不变。 // keyboardOptions 作用:点击输入框时弹出来的键盘类型。这里是 Decimal,代表带小数点的数字键盘。 // label 是输入框的占位提示文字。 OutlinedTextField( value = inputX, onValueChange = { inputX = it }, // it 代表用户输入的最新文字 label = { Text("X") }, modifier = Modifier.weight(1f), // 平分宽度 singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal) ) // 作用:Y 输入框,和 X 输入框逻辑完全一样。 OutlinedTextField( value = inputY, onValueChange = { inputY = it }, label = { Text("Y") }, modifier = Modifier.weight(1f), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal) ) // 作用:添加按钮。点击时,解析输入框内容并转为 Float,如果合法,就加到 points 列表里,并清空输入框。 Button( onClick = { val x = inputX.toFloatOrNull() val y = inputY.toFloatOrNull() if (x != null && y != null) { points = points + DataPoint(x, y) inputX = "" inputY = "" } }, modifier = Modifier.height(56.dp) // 按钮高度 ) { Text("添加") } } } } // --- 结果显示区 --- // 作用:专门用来展示计算出的方程。 Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color(0xFFE3F2FD)), // 浅蓝色 elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // 阴影为0,紧贴屏幕 ) { // 作用:横向排版,左边显示方程,右边显示清空按钮。 Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), // SpaceBetween 作用:把这两个孩子推到左右两端,中间留白。 horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column { Text("拟合方程:", fontSize = 14.sp, color = Color.DarkGray) // 作用:显示计算出来的方程,加大加粗。 Text(equationText, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1565C0)) } // 作用:清空按钮。点击后把列表置空,触发自动重新计算。 TextButton( onClick = { points = emptyList() }, colors = ButtonDefaults.textButtonColors(contentColor = Color.Red) ) { Text("清空所有") } } } // --- 图表绘制区 --- // 作用:用 Card 包起来,占据屏幕剩余的垂直空间。weight(1f) 就是“把剩下的高度全吃光”。 Card( modifier = Modifier .fillMaxWidth() .weight(1f), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { // 作用:Box 是一个盒子,里面的东西可以叠加在一起。加个 padding 防止图贴边。 Box(modifier = Modifier.fillMaxSize().padding(16.dp)) { // 作用:Canvas 画布。坐标轴、红线和圆点都在这里画出来。 Canvas(modifier = Modifier.fillMaxSize()) { // 如果点不够,直接退出画图。 if (points.isEmpty()) return@Canvas // 作用:找出数据里的最大、最小值,用来计算坐标系的范围。 val minX = points.minOf { it.x } val maxX = points.maxOf { it.x } val minY = points.minOf { it.y } val maxY = points.maxOf { it.y } // 作用:给四周加 10% 的留白,防止数据点贴着画布边缘,显得好看。 val paddingX = (maxX - minX) * 0.1f val paddingY = (maxY - minY) * 0.1f // 作用:防止除零错误。如果所有点的 X 一样,范围强制设为 1f。 val rangeX = if (maxX - minX == 0f) 1f else (maxX - minX) + paddingX * 2 val rangeY = if (maxY - minY == 0f) 1f else (maxY - minY) + paddingY * 2 val startX = minX - paddingX val startY = minY - paddingY // 拿到画布的真实宽高(像素)。 val width = size.width val height = size.height // 作用:坐标映射。 // 数学坐标系是“左下角为原点,Y 向上”。 // 手机屏幕坐标系是“左上角为原点,Y 向下”。 // 所以 Y 轴的计算需要用 height 减去一部分,才能把图翻正过来。 fun mapX(x: Float): Float = (x - startX) / rangeX * width fun mapY(y: Float): Float = height - (y - startY) / rangeY * height // 计算原点 (0,0) 在屏幕上的像素位置。 val zeroX = mapX(0f) val zeroY = mapY(0f) // 作用:画 X 轴(横线)。只有在原点在可视范围内才画。 if (zeroY in 0f..height) { drawLine(Color.Black, start = Offset(0f, zeroY), end = Offset(width, zeroY), strokeWidth = 2f) } // 作用:画 Y 轴(竖线)。只有在原点在可视范围内才画。 if (zeroX in 0f..width) { drawLine(Color.Black, start = Offset(zeroX, 0f), end = Offset(zeroX, height), strokeWidth = 2f) } // 作用:画红色拟合直线。起点和终点延伸到画布边缘。 if (points.size >= 2) { val lineStartX = startX val lineStartY = slopeA * lineStartX + interceptB val lineEndX = startX + rangeX val lineEndY = slopeA * lineEndX + interceptB drawLine( color = Color.Red, start = Offset(mapX(lineStartX), mapY(lineStartY)), end = Offset(mapX(lineEndX), mapY(lineEndY)), strokeWidth = 6f // 线宽 ) } // 作用:画蓝色的圆点(散点)。外面画一个白色的稍大圆点做光圈,里面画蓝色小圆点,看起来有层次感。 points.forEach { p -> drawCircle(Color.White, radius = 12f, center = Offset(mapX(p.x), mapY(p.y))) drawCircle(Color.Blue, radius = 8f, center = Offset(mapX(p.x), mapY(p.y))) } } } } // --- 数据列表区 --- // 作用:卡片固定高度 150dp,防止列表太长把上面的图表挤没。 Card( modifier = Modifier .fillMaxWidth() .height(150.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { // 作用:内部垂直排布,标题在上,列表在下。 Column(modifier = Modifier.padding(8.dp)) { Text("数据列表 (点击可删除)", fontSize = 12.sp, color = Color.Gray) // 作用:LazyColumn 是懒加载列表。屏幕外的内容不渲染,性能好。可以上下滑动。 LazyColumn(modifier = Modifier.fillMaxSize()) { // 作用:循环遍历 points 列表。 // 语法:items(数据源) { 当前项变量名 -> 每一行长什么样 } // 这里 p 代表当前正在处理的某一个坐标点。 items(points) { p -> // 作用:列表的每一行。Row 横向排列。 Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) .background(Color(0xFFF0F0F0), RoundedCornerShape(4.dp)) .padding(horizontal = 8.dp, vertical = 6.dp), // SpaceBetween 让文字靠左,删除按钮靠右,中间撑开。 horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { // 作用:显示当前坐标点的文字。 Text("(${p.x}, ${p.y})", fontSize = 14.sp) // 作用:删除按钮。 // 因为官方图标库需要额外导包,这里我们用 Box 包着一个 "×" 文本,手动做一个删除按钮。 Box( modifier = Modifier .size(24.dp) // 按钮大小 .background(Color(0xFFFFCDD2), shape = CircleShape) // 浅红背景,圆形 // 点击时,从 points 列表中减去 p 这个点。数据一变,列表和图表就会自动刷新。 .clickable { points = points - p }, contentAlignment = Alignment.Center // 让 "×" 居中 ) { Text("×", color = Color.Red, fontWeight = FontWeight.Bold, fontSize = 16.sp) } } } } } } } }package com.example.brodcast.compents // === 导入需要用到的 Compose 组件和工具 === import androidx.compose.foundation.layout.* // 包含了 Column, Row, Spacer, padding, fillMaxWidth 等布局工具 import androidx.compose.foundation.shape.CircleShape // 用于把按钮变成圆形 import androidx.compose.material3.Button // 按钮组件 import androidx.compose.material3.ButtonDefaults // 按钮的默认样式(比如颜色) import androidx.compose.material3.Text // 文本组件 import androidx.compose.runtime.* // 包含了 remember, mutableStateOf 等状态管理工具 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.text.style.TextAlign // 文字对齐方式 import androidx.compose.ui.unit.dp // dp 尺寸单位 import androidx.compose.ui.unit.sp // sp 字体大小单位 @Composable fun CalculatorScreen() { // ==================== 状态管理(计算器的大脑) ==================== // displayText: 屏幕上显示的数字。用户每按一个数字,这个值就会改变,屏幕也会跟着刷新。 // "by remember" 是 Compose 的固定写法,表示“记住这个值,即使屏幕刷新了也不要丢掉”。 var displayText by remember { mutableStateOf("0") } // firstNumber: 用来偷偷存下第一个数字。比如按了 5+3,按加号时,5 就会存到这里。 var firstNumber by remember { mutableStateOf("") } // currentOperation: 用来记住当前按的是哪个运算符(+、-、×、÷)。 var currentOperation by remember { mutableStateOf("") } // isNewInput: 用来标记“是否要开始输入新数字了”。按了运算符之后,这个值会变成 true。 var isNewInput by remember { mutableStateOf(false) } // ==================== 逻辑处理函数 ==================== // 处理数字按钮的点击(0-9 和 小数点) fun onNumberClick(num: String) { if (isNewInput) { // 如果是新输入,直接替换屏幕上的内容 displayText = num isNewInput = false // 重置标记,表示现在开始接着拼数字了 } else { // 如果屏幕是 "0",并且按的不是小数点,就替换。否则就把新数字拼在后面 if (displayText == "0" && num != ".") { displayText = num } else { // 如果已经有小数点了,防止重复输入多个小数点(简单判断) if (num == "." && displayText.contains(".")) return displayText += num } } } // 处理运算符按钮的点击(+、-、×、÷) fun onOperatorClick(op: String) { // 把当前屏幕上的数字存到 firstNumber 里 firstNumber = displayText // 记住按下的运算符 currentOperation = op // 标记接下来要输入新数字了 isNewInput = true } // 处理等号按钮的点击(=) fun onEqualClick() { // 只有当有第一个数字,并且有运算符的时候才进行计算 if (firstNumber.isNotEmpty() && currentOperation.isNotEmpty()) { // 把字符串转成小数(Double),如果转不了就默认是 0.0 val num1 = firstNumber.toDoubleOrNull() ?: 0.0 val num2 = displayText.toDoubleOrNull() ?: 0.0 var result = 0.0 // 根据运算符,进行对应的数学计算 when (currentOperation) { "+" -> result = num1 + num2 "-" -> result = num1 - num2 "×" -> result = num1 * num2 "÷" -> if (num2 != 0.0) result = num1 / num2 else result = 0.0 // 防止除以0导致崩溃 } // 处理结果显示:如果是 5.0 这种整数,就显示成 5;如果是 5.5,就保留小数 displayText = if (result % 1.0 == 0.0) result.toInt().toString() else result.toString() // 算完之后,清空大脑,准备下一次计算 firstNumber = "" currentOperation = "" isNewInput = true } } // ==================== 界面布局 ==================== // 最外层的 Column,用来从上到下排列所有内容 Column( modifier = Modifier .fillMaxSize() // 占满整个屏幕 .padding(16.dp), // 屏幕四周留出 16dp 的空白 verticalArrangement = Arrangement.Bottom // 让所有内容靠屏幕底部对齐 ) { // ---- 顶部显示屏 ---- Text( text = displayText, // 显示当前计算器上的数字 fontSize = 64.sp, // 字体很大 fontWeight = FontWeight.Bold, // 加粗 modifier = Modifier .fillMaxWidth() // 占满整行宽度 .padding(bottom = 24.dp), // 显示屏底部留出空隙 textAlign = TextAlign.End // 文字靠右对齐 ) // ---- 键盘区域,一共 5 行 ---- // 第一行 Row(modifier = Modifier.fillMaxWidth()) { // AC:清空所有状态 CalcButton("AC") { displayText = "0" firstNumber = "" currentOperation = "" isNewInput = false } // +/-:正负号切换 CalcButton("+/-") { if (displayText != "0") { displayText = if (displayText.startsWith("-")) displayText.drop(1) else "-$displayText" } } // %:百分号 CalcButton("%") { val num = displayText.toDoubleOrNull() ?: 0.0 displayText = (num / 100).toString() } // ÷:除号 CalcButton("÷") { onOperatorClick("÷") } } // 第二行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("7") { onNumberClick("7") } CalcButton("8") { onNumberClick("8") } CalcButton("9") { onNumberClick("9") } CalcButton("×") { onOperatorClick("×") } } // 第三行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("4") { onNumberClick("4") } CalcButton("5") { onNumberClick("5") } CalcButton("6") { onNumberClick("6") } CalcButton("-") { onOperatorClick("-") } } // 第四行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("1") { onNumberClick("1") } CalcButton("2") { onNumberClick("2") } CalcButton("3") { onNumberClick("3") } CalcButton("+") { onOperatorClick("+") } } // 第五行 Row(modifier = Modifier.fillMaxWidth()) { // 这里的 "0" 按钮比较特殊,我们让它占两份宽度 (weight(2f)),宽高比是 2:1 Button( onClick = { onNumberClick("0") }, modifier = Modifier .weight(2f) // 占据两份宽度 .aspectRatio(2f) // 宽高比 2:1,这样它就会变成一个横着的椭圆 .padding(4.dp), shape = CircleShape, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEEEEEE)) ) { Text("0", fontSize = 24.sp, color = Color.Black) } // 小数点 CalcButton(".") { onNumberClick(".") } // 等号 CalcButton("=") { onEqualClick() } } } } // ==================== 自定义按钮组件 ==================== // RowScope. 的作用:告诉 Compose 这个按钮是属于 Row 的,这样它才能使用 weight() 来平分宽度。 // 如果没有 RowScope.,写 weight() 会报错。 @Composable fun RowScope.CalcButton(text: String, onClick: () -> Unit) { Button( onClick = onClick, // 点击事件传给外部 modifier = Modifier .weight(1f) // 把当前行的剩余宽度平分,每个按钮占一份 .aspectRatio(1f) // 让按钮变成正方形(宽高比 1:1) .padding(4.dp), // 按钮之间的缝隙 shape = CircleShape, // 按钮形状是圆形 // 按钮背景颜色,这里用十六进制颜色代码,避免引入不必要的颜色库 colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEEEEEE)) ) { // 按钮里面的文字 Text(text, fontSize = 24.sp, color = Color.Black) } }package com.example.brodcast.compents import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text 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.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable fun CalculatorScreen() { // 🪄 魔法1:state 状态。用来存屏幕上显示的文字。 // 只要你改了 displayText 的值,界面就会自动刷新! var displayText by remember { mutableStateOf("0") } // 整个屏幕的容器 Column( modifier = Modifier .fillMaxSize() .padding(16.dp), // 让所有内容都对齐到屏幕底部 verticalArrangement = Arrangement.Bottom ) { // ================= 顶部显示屏 ================= Text( text = displayText, fontSize = 64.sp, fontWeight = FontWeight.Bold, modifier = Modifier .fillMaxWidth() .padding(bottom = 24.dp), // 给底部留点空隙 textAlign = TextAlign.End // 文字靠右对齐 ) // ================= 键盘区域 ================= // 第一行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("AC") { displayText = "0" } CalcButton("+/-") { /* 暂时不写逻辑 */ } CalcButton("%") { /* 暂时不写逻辑 */ } CalcButton("÷") { displayText += "÷" } } // 第二行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("7") { displayText = appendNum(displayText, "7") } CalcButton("8") { displayText = appendNum(displayText, "8") } CalcButton("9") { displayText = appendNum(displayText, "9") } CalcButton("×") { displayText += "×" } } // 第三行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("4") { displayText = appendNum(displayText, "4") } CalcButton("5") { displayText = appendNum(displayText, "5") } CalcButton("6") { displayText = appendNum(displayText, "6") } CalcButton("-") { displayText += "-" } } // 第四行 Row(modifier = Modifier.fillMaxWidth()) { CalcButton("1") { displayText = appendNum(displayText, "1") } CalcButton("2") { displayText = appendNum(displayText, "2") } CalcButton("3") { displayText = appendNum(displayText, "3") } CalcButton("+") { displayText += "+" } } // 第五行(0占两格,我们用两个按钮拼起来) Row(modifier = Modifier.fillMaxWidth()) { // 这个 0 的按钮稍微特殊一点,我们让它占两份宽度 (weight 2f) Button( onClick = { displayText = appendNum(displayText, "0") }, modifier = Modifier .weight(2f) .aspectRatio(2f) // 宽高比 2:1 .padding(4.dp), shape = CircleShape, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEEEEEE)) ) { Text("0", fontSize = 24.sp, color = Color.Black) } CalcButton(".") { displayText += "." } CalcButton("=") { /* 暂时不写逻辑 */ } } } } // 🪄 魔法2:自定义一个按钮组件,避免写一堆重复的代码 @Composable fun CalcButton(text: String, onClick: () -> Unit) { Button( onClick = onClick, modifier = Modifier .weight(1f) // 把当前行的剩余宽度平分 .aspectRatio(1f) // 让按钮变成正方形 .padding(4.dp), // 按钮之间的缝隙 shape = CircleShape, // 圆形按钮 // 这里为了安全,直接写颜色代码,避免你再遇到 BlendMode 那个坑 colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEEEEEE)) ) { Text(text, fontSize = 24.sp, color = Color.Black) } } // 🪄 魔法3:处理数字输入的简单逻辑 fun appendNum(current: String, num: String): String { // 如果当前屏幕是 "0",就直接替换成新的数字;否则就拼在后面 return if (current == "0") num else current + num }