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

文章配图:样式抽成可复用类,一处定义、多处复用,还能针对不同状态(正常/按下/禁用)应用不同样式

页面预览

前言

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

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

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

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

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

// 来源: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 的解法

// 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 定义样式类

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 应用样式

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

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

2.3 三种按钮样式类

// 主操作按钮(绿色,开始游戏)
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 按下状态

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

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

状态回调对照

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

3.2 禁用状态样式

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 完整多状态按钮

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 定义三个按钮样式类

// 次要按钮(暂停)
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 改造底部栏

@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 通用样式

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

Button('开始').primaryButtonStyle()

5.2 @Extend 特定组件样式

@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 泛型不匹配

// ❌ 错误: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

// ❌ 错误:没 implements,attributeModifier 不接受
class MyModifier { applyNormalAttribute() {} }

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

6.3 Modifier 与组件属性冲突

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

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

6.4 applyPressedAttribute 没覆盖 normal 的所有差异

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 颜色与圆角
20SymbolGlyph系统符号字体替代 Emoji
21RelativeContainer扁平复杂布局
22Blank替代 Spacer 的间距
23layoutWeight按比例分配宽度
24displayPriority窄屏自适应隐藏
25Scroller长内容滚动容器
26Badge消息角标
27Image 资源$r 与 $rawfile
28暗色模式深色资源与 colorMode
29多语言 i18nstring.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。

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


相关资源:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值