ARTICLE · 1253895
保姆级学习开发安卓手机软件实战(十四)--UI界面可复用的组件
保姆级学习开发安卓手机软件实战(十四)--UI界面可复用的组件标题栏:ui/component/AppTopBar.kt
前面我们已经完成了两个界面,你大概也发现了:其中不少控件是可以复用的。这一节就把它们单独提取出来,做成可复用的组件。
设计规范:ui/theme/Dimens.kt
import androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.ui.unit.dp/** 全局圆角(对齐效果图 --radius:4px):卡片/面板/输入框统一用它 */internal val SHAPE = RoundedCornerShape(4.dp)/** 按钮 / 分段控件的统一高度(原 CTRL_BTN_HEIGHT) */internal val BTN_HEIGHT = 36.dp
package com.example.jstz1.ui.componentimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.borderimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Brushimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport com.example.jstz1.ui.theme.TopAimport com.example.jstz1.ui.theme.TopB@Composableinternal funTopBar(title: String, onBack:(()-> Unit)?=null) {Column(Modifier.fillMaxWidth().background(Brush.verticalGradient(listOf(TopA, TopB)))) {Row(Modifier.fillMaxWidth().height(52.dp).padding(horizontal = 20.dp),verticalAlignment = Alignment.CenterVertically) {if(onBack!=null){Text("‹ 返回",modifier = Modifier.clickable(onClick = onBack).padding(vertical = 6.dp),style = MaterialTheme.typography.titleLarge,color = MaterialTheme.colorScheme.onBackground)Spacer(Modifier.width(16.dp))}// logoBox(Modifier.size(34.dp).background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(6.dp)).border(1.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(6.dp)),contentAlignment = Alignment.Center) {Text("J",style = MaterialTheme.typography.titleMedium,fontWeight = FontWeight.Bold,color = MaterialTheme.colorScheme.primary)}Spacer(Modifier.weight(1f))Text(text = title ,style = MaterialTheme.typography.titleLarge,color = MaterialTheme.colorScheme.onBackground,letterSpacing = 4.sp)Spacer(Modifier.weight(1f))// 用户 + 角色Text("admin",style = MaterialTheme.typography.labelMedium,color = MaterialTheme.colorScheme.onSurfaceVariant)Spacer(Modifier.width(6.dp))Box(Modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(9.dp)).padding(horizontal = 8.dp, vertical = 2.dp)) {Text("用户管理",style = MaterialTheme.typography.labelSmall,fontWeight = FontWeight.Bold,color = MaterialTheme.colorScheme.onPrimary)}Spacer(Modifier.width(14.dp))// 退出登录Text("退出登录",style = MaterialTheme.typography.labelMedium,color = MaterialTheme.colorScheme.onSurfaceVariant,modifier = Modifier.border(1.dp,MaterialTheme.colorScheme.outlineVariant,RoundedCornerShape(6.dp)).padding(horizontal = 8.dp, vertical = 4.dp))}// 底部橙色装饰线Box(Modifier.fillMaxWidth().height(2.dp).background(Brush.horizontalGradient(0.0f to Color.Transparent,0.2f to MaterialTheme.colorScheme.primary,0.8f to MaterialTheme.colorScheme.primary,1.0f to Color.Transparent)))}}
按钮:ui/component/Buttons.kt
package com.example.jstz1.ui.componentimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.borderimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.defaultMinSizeimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.shape.CircleShapeimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.clipimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport com.example.jstz1.ui.theme.BTN_HEIGHTimport com.example.jstz1.ui.theme.Btnimport com.example.jstz1.ui.theme.PBorder2/** 主操作按钮(确定下发):不可用时自动置灰并点不动 (登录/新增/修改/下发)*/@Composableinternal funPrimaryAction(label: String, enabled: Boolean, onClick: () -> Unit) {val container = if (enabled) MaterialTheme.colorScheme.primary else Btnval border = if (enabled) MaterialTheme.colorScheme.primary else PBorder2val content = if (enabled) MaterialTheme.colorScheme.onPrimaryelse MaterialTheme.colorScheme.onSurfaceVariantBox(Modifier.fillMaxWidth().height(BTN_HEIGHT).background(container, RoundedCornerShape(7.dp)).border(1.dp, border, RoundedCornerShape(7.dp)).clickable(enabled = enabled, onClick = onClick),contentAlignment = Alignment.Center) {Text(label,style = MaterialTheme.typography.titleSmall,fontWeight = FontWeight.Bold,color = content)}}/*** 可选中的模式按钮** 选中 → 用该模式自己的配色(运行绿 / 停止红 / 橙色系)* 未选 → 灰底灰边,一眼看出"还没选"**/@Composableinternal funSelectableButton(label: String,selected: Boolean,onBg: Color,onBorder: Color,onText: Color,offText: Color? = null,modifier: Modifier = Modifier,onClick: () -> Unit) {Box(modifier.fillMaxWidth().defaultMinSize(minHeight = BTN_HEIGHT).background(if (selected) onBg else Btn, RoundedCornerShape(6.dp)).border(1.dp, if (selected) onBorder else PBorder2, RoundedCornerShape(6.dp)).clickable(onClick = onClick),contentAlignment = Alignment.Center) {Text(label,style = MaterialTheme.typography.bodyMedium,color = when {selected -> onTextoffText != null -> offTextelse -> MaterialTheme.colorScheme.onSurfaceVariant})}}/** 二选一分段控件按钮(选中项底部橙色下划线 )*/@Composableinternal funSegmentedControl(items: List<String>,selectedIndex: Int,onSelect: (Int) -> Unit) {Row(Modifier.height(BTN_HEIGHT)// 先裁成圆角:选中态的橙色下划线贴到底边时,不会被画到圆角外面露出尖角.clip(RoundedCornerShape(7.dp)).border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(7.dp))) {items.forEachIndexed { i, label ->val selected = i == selectedIndexBox(Modifier.weight(1f).fillMaxHeight().background(if (selected) MaterialTheme.colorScheme.primaryContainerelse Color.Transparent).clickable { onSelect(i) },contentAlignment = Alignment.Center) {Text(label,style = MaterialTheme.typography.bodyMedium,color = if (selected) MaterialTheme.colorScheme.onBackgroundelse MaterialTheme.colorScheme.onSurfaceVariant)// 选中态:底部橙色下划线(对应效果图 .seg .on 的 border-bottom)if (selected) {Box(Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(2.dp).background(MaterialTheme.colorScheme.primary))}}}}}/*** Switch开关(34×18 轨道 + 14dp 圆钮)** 为什么不用 Material3 的 Switch:* 它最小 52×32dp,两张卡片并排时把「恢复出厂」挤出卡片外。*/@Composableinternal funMiniSwitch(checked: Boolean, onToggle: () -> Unit) {Box(Modifier.size(width = 34.dp, height = 18.dp).background(if (checked) MaterialTheme.colorScheme.primaryContainerelse MaterialTheme.colorScheme.outline,RoundedCornerShape(9.dp)).border(1.dp,if (checked) MaterialTheme.colorScheme.primary else Color.Transparent,RoundedCornerShape(9.dp)).clickable(onClick = onToggle),contentAlignment = if (checked) Alignment.CenterEnd else Alignment.CenterStart) {Box(Modifier.padding(horizontal = 2.dp).size(14.dp).background(if (checked) MaterialTheme.colorScheme.primaryelse MaterialTheme.colorScheme.onSurfaceVariant,CircleShape))}}
自绘图片:ui/component/Images.kt
package com.example.jstz1.ui.componentimport androidx.compose.foundation.Canvasimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.geometry.CornerRadiusimport androidx.compose.ui.geometry.Offsetimport androidx.compose.ui.geometry.Sizeimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.StrokeCapimport androidx.compose.ui.graphics.drawscope.Strokeimport androidx.compose.ui.unit.dp/*** 仪器图标(对应效果图卡片左上角那个箱子图形)** 手绘而不是放 png:* 1. 能跟着状态换颜色(正常橙 / 报警红 / 离线灰),一张静态图做不到* 2. 不用为不同屏幕密度准备多套切图,矢量永远不糊* 3. 不增加 APK 体积*/@Composableinternal funDeviceIcon(color: Color, modifier: Modifier = Modifier) {Canvas(modifier) {val stroke = 1.5.dp.toPx()val w = size.widthval h = size.height// 顶部提手drawLine(color = color,start = Offset(w * 0.30f, h * 0.13f),end = Offset(w * 0.70f, h * 0.13f),strokeWidth = stroke,cap = StrokeCap.Round)// 箱体drawRoundRect(color = color,topLeft = Offset(w * 0.14f, h * 0.26f),size = Size(w * 0.72f, h * 0.62f),cornerRadius = CornerRadius(w * 0.14f, w * 0.14f),style = Stroke(width = stroke))// 中心圆环drawCircle(color = color,radius = w * 0.15f,center = Offset(w * 0.50f, h * 0.58f),style = Stroke(width = stroke))// 圆心drawCircle(color = color,radius = w * 0.05f,center = Offset(w * 0.50f, h * 0.58f))}}
输入控件:ui/component/InputBox.kt
package com.example.jstz1.ui.componentimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.text.KeyboardOptionsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.OutlinedTextFieldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.input.KeyboardTypeimport com.example.jstz1.ui.theme.SHAPE/** 带标题的单行输入框 */@Composableinternal funFormField(label: String,value: String,onValueChange: (String) -> Unit,placeholder: String = "",keyboardType: KeyboardType = KeyboardType.Text,maxLength: Int = Int.MAX_VALUE) {OutlinedTextField(value = value,onValueChange = { if (it.length <= maxLength) onValueChange(it) },modifier = Modifier.fillMaxWidth(),label = { Text(label, style = MaterialTheme.typography.labelSmall) },placeholder = {Text(placeholder,style = MaterialTheme.typography.labelSmall,color = MaterialTheme.colorScheme.onSurfaceVariant)},singleLine = true,textStyle = MaterialTheme.typography.bodyMedium,keyboardOptions = KeyboardOptions(keyboardType = keyboardType),shape = SHAPE)}
文本控件:ui/component/Lables.kt
package com.example.jstz1.ui.componentimport androidx.compose.foundation.layout.Rowimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.graphics.Color@Composableinternal funDataRow(label: String, value: String, valueColor: Color? = null) {Row(verticalAlignment = Alignment.CenterVertically) {Text("$label ",style = MaterialTheme.typography.bodySmall,color = MaterialTheme.colorScheme.onSurfaceVariant)Text(value,style = MaterialTheme.typography.bodySmall,color = valueColor ?: MaterialTheme.colorScheme.onSurface)}}
容器–ui/component/Panels.kt
package com.example.jstz1.ui.componentimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.borderimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport com.example.jstz1.ui.theme.SHAPE/** 群控面板里的一个分区(对应 .block)*/@Composableinternal funBlock(title: String ="",modifier: Modifier = Modifier,content: @Composable () -> Unit) {Column(modifier.background(MaterialTheme.colorScheme.surfaceVariant, SHAPE).border(1.dp, MaterialTheme.colorScheme.outline, SHAPE).padding(12.dp)) {if(title!=""){Text(title,style = MaterialTheme.typography.labelMedium,color = MaterialTheme.colorScheme.onSurfaceVariant)Spacer(Modifier.height(9.dp))}content()}}
这些都是从原来的界面中提取出来的(主要是private改为了internal),所以原有界面里要删掉哦,然后引用这次新建的类。 这节就这样,不算很重要~















































































