HarmonyOS应用开发实战:猫猫大作战-样式抽成可复用类,一处定义、多处复用,还能针对不同状态(正常/按下/禁用)应用不同样式

前言

回顾「猫猫大作战」的按钮------「开始游戏」(绿)、「暂停」(灰)、「重新开始」(红)、「再来一局」(绿)、「返回主菜单」(浅灰)。每个按钮都重复写了 width/height/fontSize/borderRadius 这堆属性,代码重复严重,改一处要改 N 处。

HarmonyOS 提供了 AttributeModifier 属性修饰器 ------把样式抽成可复用类,一处定义、多处复用,还能针对不同状态(正常/按下/禁用)应用不同样式。本篇以「猫猫大作战」按钮样式复用为锚点,把 AttributeModifier 的定义与复用多状态样式与 @Styles/@Extend 的取舍三大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1--29 篇。本篇是布局进阶的收尾篇。

一、场景拆解:按钮样式重复

回顾「猫猫大作战」底部栏按钮(第 19 篇):

ts 复制代码
// 来源:entry/src/main/ets/pages/Index.ets  GameView() 底部栏
Button('暂停')
  .width(100).height(44)
  .fontSize(15).fontColor('#FFFFFF')
  .backgroundColor('#95A5A6')
  .borderRadius(22)
  .onClick(() => { this.pauseGame(); })

Button('重新开始')
  .width(100).height(44)
  .fontSize(15).fontColor('#FFFFFF')
  .backgroundColor('#E74C3C')
  .borderRadius(22)
  .onClick(() => { this.clearTimers(); this.startGame(); })

痛点

问题 表现
重复代码 width(100).height(44).fontSize(15)... 每个按钮写一遍
改一处动全身 按钮高度 44 改 48,要全文搜索替换
多状态难管 按下/禁用样式要 onTouch 手动切,代码冗长

AttributeModifier 的解法

ts 复制代码
// 1. 定义样式类
class SecondaryButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance.width(100).height(44).fontSize(15)
      .fontColor('#FFFFFF').backgroundColor('#95A5A6').borderRadius(22)
  }
}

// 2. 复用
Button('暂停').attributeModifier(new SecondaryButtonModifier())

关键经验AttributeModifier 把样式抽成类,一处定义多处复用

二、AttributeModifier 基本用法

2.1 定义样式类

ts 复制代码
class PrimaryButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width('70%')
      .height(56)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .backgroundColor('#2ECC71')
      .borderRadius(28)
      .shadow({ radius: 8, color: 'rgba(46, 204, 113, 0.4)', offsetY: 4 })
  }
}

核心要素

要素 说明
implements AttributeModifier<ButtonAttribute> 泛型指定目标组件类型
applyNormalAttribute(instance) 正常状态应用样式
instance.width(...) 链式调用,与组件属性一致

2.2 应用样式

ts 复制代码
Button($r('app.string.start_game'))
  .attributeModifier(new PrimaryButtonModifier())
  .onClick(() => { this.startGame(); })

关键经验.attributeModifier(new XxxModifier()) 一行应用全套样式

2.3 三种按钮样式类

ts 复制代码
// 主操作按钮(绿色,开始游戏)
class PrimaryButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width('70%').height(56)
      .fontSize(20).fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF').backgroundColor('#2ECC71')
      .borderRadius(28)
      .shadow({ radius: 8, color: 'rgba(46, 204, 113, 0.4)', offsetY: 4 })
  }
}

// 次要操作按钮(灰色,暂停)
class SecondaryButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width(100).height(44)
      .fontSize(15).fontColor('#FFFFFF')
      .backgroundColor('#95A5A6').borderRadius(22)
  }
}

// 危险操作按钮(红色,重新开始)
class DangerButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width(100).height(44)
      .fontSize(15).fontColor('#FFFFFF')
      .backgroundColor('#E74C3C').borderRadius(22)
  }
}

三、多状态样式

3.1 applyPressedAttribute 按下状态

ts 复制代码
class PrimaryButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#2ECC71')      // 正常:亮绿
  }

  applyPressedAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#27AE60')      // 按下:深绿
  }
}

状态回调对照

回调 触发时机 适用
applyNormalAttribute 正常显示 默认样式
applyPressedAttribute 按下时 按压反馈
applyDisabledAttribute enabled(false) 禁用样式
applyFocusedAttribute 获得焦点时 键盘/遥控器焦点
applySelectedAttribute 选中态 Toggle/Tab

3.2 禁用状态样式

ts 复制代码
class SubmitButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#2ECC71').fontColor('#FFFFFF')
  }

  applyDisabledAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#BDC3C7').fontColor('#ECF0F1')   // 灰色禁用
  }
}

// 使用:禁用态自动应用 applyDisabledAttribute
Button('提交').attributeModifier(new SubmitButtonModifier()).enabled(this.isFormValid)

3.3 完整多状态按钮

ts 复制代码
class FullStateButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width(200).height(48)
      .fontSize(16).fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .backgroundColor('#2ECC71')
      .borderRadius(24)
  }

  applyPressedAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#27AE60')          // 按下深绿
    instance.scale({ x: 0.95, y: 0.95 })         // 按下缩小 5%
  }

  applyDisabledAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#BDC3C7')          // 禁用灰
    instance.fontColor('#ECF0F1')
  }
}

执行流程

  1. 正常显示 → applyNormalAttribute
  2. 用户按下 → applyPressedAttribute(覆盖 normal 的差异属性)
  3. 用户松开 → 回到 applyNormalAttribute
  4. enabled(false)applyDisabledAttribute

四、改造底部栏用 AttributeModifier

4.1 定义三个按钮样式类

ts 复制代码
// 次要按钮(暂停)
class SecondaryButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width(100).height(44)
      .fontSize(15).fontColor('#FFFFFF')
      .backgroundColor('#95A5A6').borderRadius(22)
  }
  applyPressedAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#7F8C8D')          // 按下深灰
  }
}

// 危险按钮(重新开始)
class DangerButtonModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance
      .width(100).height(44)
      .fontSize(15).fontColor('#FFFFFF')
      .backgroundColor('#E74C3C').borderRadius(22)
  }
  applyPressedAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#C0392B')          // 按下深红
  }
}

4.2 改造底部栏

ts 复制代码
@Builder
GameView() {
  Column() {
    this.GameHUD()

    Column() {
      // 预告区
      Row() { /* ... */ }

      // 游戏棋盘
      Stack() { /* 3 层叠 */ }
        .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE)
        .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
        .borderRadius(12)
        .clip(true)
        .backgroundColor('#D6EEF5')
    }
    .alignItems(HorizontalAlign.Center)

    Spacer()                       // 弹性占位,推底部栏到最底

    // 底部控制栏(AttributeModifier 改造)
    Row() {
      Button($r('app.string.pause'))
        .attributeModifier(new SecondaryButtonModifier())
        .onClick(() => { this.pauseGame(); })

      Spacer()                    // 撑开两按钮到两端

      Button($r('app.string.restart'))
        .attributeModifier(new DangerButtonModifier())
        .onClick(() => {
          this.clearTimers();
          this.startGame();
        })
    }
    .width('100%')
    .padding({ left: 24, right: 24, bottom: 24, top: 12 })
  }
  .width('100%').height('100%')
  .linearGradient({
    direction: GradientDirection.Bottom,
    colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]]
  })
  .alignItems(HorizontalAlign.Center)
}

改造对比

维度 原版(属性堆叠) AttributeModifier 版
代码量 每按钮 7 行属性 每按钮 1 行 modifier
按压反馈 无(默认 stateEffect) applyPressedAttribute 自定义
改样式 改每个按钮的属性 改 Modifier 类,全应用生效
复用 无法复用 一处定义多处用

五、AttributeModifier vs @Styles vs @Extend

ArkUI 有三种样式复用机制:

5.1 @Styles 通用样式

ts 复制代码
@Styles
function primaryButtonStyle() {
  .width('70%').height(56)
  .fontSize(20).fontWeight(FontWeight.Bold)
}

Button('开始').primaryButtonStyle()

5.2 @Extend 特定组件样式

ts 复制代码
@Extend(Button)
function primaryButtonExtend() {
  .width('70%').height(56)
  .backgroundColor('#2ECC71').borderRadius(28)
}

Button('开始').primaryButtonExtend()

5.3 三者对比

机制 适用组件 多状态 跨组件复用 推荐
@Styles 任意 简单通用样式
@Extend 指定组件 特定组件样式
AttributeModifier 任意 复杂多状态样式

实战经验简单样式用 @Styles,特定组件用 @Extend,多状态/复杂复用用 AttributeModifier

5.4 选择决策树

复制代码
样式需要多状态(按下/禁用)?
  ├─ 是 → AttributeModifier
  └─ 否
      └─ 样式跨多种组件复用?
          ├─ 是 → @Styles
          └─ 否
              └─ 样式只针对某组件? → @Extend

六、踩坑提示

6.1 AttributeModifier 泛型不匹配

ts 复制代码
// ❌ 错误:Modifier 声明 ButtonAttribute,但用在 Text 上
class MyModifier implements AttributeModifier<ButtonAttribute> { /* ... */ }
Text('A').attributeModifier(new MyModifier())   // 类型不匹配

// ✅ 正确:泛型与组件匹配
class MyModifier implements AttributeModifier<TextAttribute> { /* ... */ }
Text('A').attributeModifier(new MyModifier())

6.2 忘记 implements AttributeModifier

ts 复制代码
// ❌ 错误:没 implements,attributeModifier 不接受
class MyModifier { applyNormalAttribute() {} }

// ✅ 正确:implements AttributeModifier<T>
class MyModifier implements AttributeModifier<ButtonAttribute> { /* ... */ }

6.3 Modifier 与组件属性冲突

ts 复制代码
// ⚠️ Modifier 设了 backgroundColor,组件又设了一次,后者覆盖前者
Button('A')
  .attributeModifier(new MyModifier())    // 设 backgroundColor('#2ECC71')
  .backgroundColor('#E74C3C')             // 覆盖 Modifier 的绿色

// ✅ 推荐:样式统一在 Modifier 里管,组件只设业务属性
Button('A')
  .attributeModifier(new MyModifier())
  .onClick(() => { /* ... */ })

6.4 applyPressedAttribute 没覆盖 normal 的所有差异

ts 复制代码
class MyModifier implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#2ECC71').scale({ x: 1, y: 1 })
  }
  applyPressedAttribute(instance: ButtonAttribute): void {
    instance.backgroundColor('#27AE60')   // 只改了颜色,scale 还是 1
    // 想要按下缩小,要显式设 scale
    instance.scale({ x: 0.95, y: 0.95 })
  }
}

七、调试技巧

  1. 临时加 console.info 在 applyNormalAttribute 里,追样式应用时机。
  2. 按压无反馈 :检查是否实现了 applyPressedAttribute;检查组件是否 enabled(false)
  3. 样式不生效:检查 Modifier 泛型是否与组件匹配;检查是否被组件属性覆盖。
  4. 多 Modifier 冲突 :一个组件只能用一个 attributeModifier,多个样式要合并到一个 Modifier 类。

八、性能与最佳实践

  1. 复杂多状态样式用 AttributeModifier------一处定义多处复用。
  2. 泛型与组件匹配------ButtonAttribute 配 Button,TextAttribute 配 Text。
  3. 样式统一在 Modifier 管------组件只设业务属性(onClick、state)。
  4. 简单通用样式用 @Styles------AttributeModifier 杀鸡用牛刀。
  5. 状态回调全覆盖------normal/pressed/disabled 都实现,避免状态残留。
  6. Modifier 类可继承复用 ------class PrimaryModifier extends BaseModifier

九、阶段一收尾:UI 组件与布局实战总结(1-30)

本篇是阶段一「UI 组件与布局实战(1-30)」的收尾篇。回顾阶段一完整覆盖:

主题 核心组件
1-5 主菜单排版 Text、Button、Spacer、Column
6-10 主菜单视觉 linearGradient、justifyContent、Row、Stack、if/else
11-12 游戏页 HUD + 预告 GameHUD、Circle 预告
13-14 棋盘背景 ForEach 双层循环、隔行变色
15-16 猫咪渲染 Emoji Text、Circle 背景、position 绝对定位
17 列点击层 透明 onClick 覆盖层
18-19 底部控制栏 Row 双按钮、Button 颜色与圆角
20 SymbolGlyph 系统符号字体替代 Emoji
21 RelativeContainer 扁平复杂布局
22 Blank 替代 Spacer 的间距
23 layoutWeight 按比例分配宽度
24 displayPriority 窄屏自适应隐藏
25 Scroller 长内容滚动容器
26 Badge 消息角标
27 Image 资源 r 与 rawfile
28 暗色模式 深色资源与 colorMode
29 多语言 i18n string.json 多语言
30(本篇) AttributeModifier 按钮样式复用

阶段一核心收获

  1. ArkUI 声明式 UI:组件 + 属性 + 事件三件套。
  2. 布局容器:Column/Row/Stack/RelativeContainer 四大布局。
  3. 状态驱动:@State 改变 → build() 重渲染。
  4. 资源系统r 编译期资源 + rawfile 原始文件。
  5. 多端适配:暗色模式、多语言、多分辨率。
  6. 样式复用:@Styles/@Extend/AttributeModifier 三层方案。

接下来阶段二(31-70)将进入状态管理 + 交互 + 动画深水区:@State/@Link/@Provide/@Consume 状态管理、@Watch 监听、setInterval 主循环、animateTo 显式动画、@Reusable 列表复用、LazyForEach 大列表等。

总结

本篇我们从 AttributeModifier 切入,掌握了样式类定义与复用多状态样式(normal/pressed/disabled)与 @Styles/@Extend 的取舍 三大要点,并给出了底部栏 AttributeModifier 改造完整代码。核心要点:AttributeModifier 一处定义多处复用;多状态用 applyPressedAttribute 等;泛型与组件匹配

下一篇我们将进入阶段二,拆解 @State 单组件状态------得分驱动 UI。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
程序员黑豆5 小时前
鸿蒙应用开发:@Link 装饰器实现父子组件双向同步
前端·后端·harmonyos
2501_919749035 小时前
华为鸿蒙免费解压缩APP—小羊解压缩
华为·harmonyos
爱写代码的森6 小时前
鸿蒙三方库 | harmony-utils之EncryptUtil URL编解码详解
服务器·华为·harmonyos·鸿蒙·huawei
youtootech6 小时前
HarmonyOS 6.0 卡片式信息流与瀑布流布局
华为·harmonyos
luoyayun3619 小时前
【Qt for Harmony】 Qt 5.12.12 鸿蒙版 Windows 交叉编译
qt·harmonyos·qt for harmony
木木子2210 小时前
# 鸿蒙 HarmonyOS 应用开发实战(第25期)|骰子(Dice Roller)— Unicode 符号与动画渲染精讲
华为·harmonyos
qizayaoshuap10 小时前
# HarmonyOS ArkTS 记忆翻牌游戏深度解析 —— Fisher-Yates 洗牌与翻牌匹配机制
游戏·华为·harmonyos
youtootech11 小时前
HarmonyOS 6.0 自定义绘图表盘与仪表
华为·harmonyos
yaoyaoxingzhe21 小时前
HarmonyOS应用《玄象》开发实战:Canvas 绘制星宿连线:moveTo / lineTo / stroke 路径绘制
harmonyos·鸿蒙