夜雨聆风学习资料网

ARTICLE · 1253895

保姆级学习开发安卓手机软件实战(十四)--UI界面可复用的组件

保姆级学习开发安卓手机软件实战(十四)--UI界面可复用的组件

前面我们已经完成了两个界面,你大概也发现了:其中不少控件是可以复用的。这一节就把它们单独提取出来,做成可复用的组件。

设计规范:ui/theme/Dimens.kt

import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.unit.dp/** 全局圆角(对齐效果图 --radius:4px):卡片/面板/输入框统一用它 */ internal val SHAPE = RoundedCornerShape(4.dp)/** 按钮 / 分段控件的统一高度(原 CTRL_BTN_HEIGHT) */ internal val BTN_HEIGHT = 36.dp
标题栏:ui/component/AppTopBar.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.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))            }            // logo            Box(                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 Btn    val border    = if (enabled) MaterialTheme.colorScheme.primary else PBorder2    val content   = if (enabled) MaterialTheme.colorScheme.onPrimary    else MaterialTheme.colorScheme.onSurfaceVariant    Box(        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        -> onText                offText != null -> offText                else            -> 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 == selectedIndex            Box(                Modifier                    .weight(1f)                    .fillMaxHeight()                    .background(                        if (selected) MaterialTheme.colorScheme.primaryContainer                        else Color.Transparent                    )                    .clickable { onSelect(i) },                contentAlignment = Alignment.Center            ) {                Text(                    label,                    style = MaterialTheme.typography.bodyMedium,                    color = if (selected) MaterialTheme.colorScheme.onBackground                    else 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.primaryContainer                else 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.primary                    else 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.width        val 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),所以原有界面里要删掉哦,然后引用这次新建的类。 这节就这样,不算很重要~

相关学习资料