[HarmonyOS] 谈谈鸿蒙开发中的“样式复用”

329 阅读8分钟

[HarmonyOS] 谈谈鸿蒙开发中的“样式复用”

前言

在我们开发鸿蒙应用时,我们可能会遇到一些重复的样式,比如按钮、输入框等。为了避免重复的代码,我们可以使用样式复用来减少代码量。

在鸿蒙官方文档中,为我们提供了好几种复用样式的方式,每一种都有自己合适的使用场景,今天我们就来学习下这些方式的使用场景以及各自的优缺点和差异性。

总结

属性@Styles@ExtendAttributeModifier
跨文件导出
通用属性设置
通用事件设置✅❌(部分不支持)
组件特有属性设置✅❌(部分不支持)
组件特有事件设置✅❌(部分不支持)
参数传递
多态样式
业务逻辑

案例分析

@Styles 装饰器

@Styles装饰器可以将多条样式设置提炼成一个 方法 ,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。

敲黑板:@Styles装饰的是一个 方法,在方法内部定义通用的样式属性或者事件

使用注意:

  • @Styles 只支持 通用属性 和 通用事件
  • @Styles 可以定义在组件内全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字

全局使用示例:

@Styles
function MyTextGlobalStyle() {
  .height(40)
  .backgroundColor(Color.Black)
  .borderRadius(20)
  .padding({ left: 10, right: 10 })
}

组件内使用示例:

@Component
class struct test {
  build(){
  }
  @Styles
  MyTextLocalStyle() {
    .height(40)
    .backgroundColor(Color.Red)
    .borderRadius(20)
    .padding({ left: 10, right: 10 })
  }
}

上述例子,是在我们的这个页面中,假如有一个通用的按钮样式需要多处使用,那么就可以考虑在这个文件中去声明这个样式,然后通过调用这个样式来达到复用的目的。

声明了一个组件内的,一个全局的,设置的参数有:高度40、背景色为黑色(组件内的是红色)、圆角为20、左右内边距为10

代码中实际使用如下:

 build() {
      Text("text-styles-1").margin({ top: 10 }).MyTextLocalStyle().fontColor(Color.White)
      Text("text-styles-1").margin({ top: 10 }).MyTextGlobalStyle().fontColor(Color.White)
    }

图1 1

既然存在 组件内@Styles全局@Styles,那么二者有什么区别呢?主要区别如下:

  • 变量访问:组件内@Styles,支持访问当前组件内的变量、状态变量等,并且可以在内部去修改这些变量
  • 优先级:组件内@Styles > 全局@Styles

接下来用代码来说明。

1.变量访问

下边的代码中,我们定义了一个组件内的状态变量,然后将他作用到一个 Text 上。 Text 的内容取了组件内的状态变量 title1, MyTextLocalStyle 中通过 onClick 事件修改了这个变量的值。

最终效果是:默认显示 Hello World,点击按钮后,显示 Hello! 达到了操作组件内状态变量的作用。

    // 定义组件内状态变量
    @Local title1: string = 'Hello World';
    
    // UI使用
    build() {
        Text(this.title1).margin({ top: 10 }).MyTextLocalStyle().fontColor(Color.White)
    }
    // 组件内@Styles
      @Styles
      MyTextLocalStyle() {
        .height(40)
        .backgroundColor(Color.Red)
        .borderRadius(20)
        .padding({ left: 10, right: 10 })
        .onClick((e) => {
          this.title1 = "Hello"
        })
      }

2.优先级

如果我在 组件内 和 全局,分别声明了一个同名的 MyTextStyle,然后引用的时候,在组件内声明的优先级更高,即组件内的样式会生效。也就是组件内的样式会覆盖全局的样式。


这么看,用 @Styles 看来也很方便了,那是不是可以大力推广了?先别急,他也是有弊端的。。。

缺点:

    1. @Styles 只支持在当前文件内使用,不能跨文件使用。
    1. @Styles 内部无法使用一些逻辑运算
    1. @Styles 的方法无法传递参数,这也就导致样式的复用性比较弱,定制化难。
    1. 只支持通用属性和通用事件,对于某些组件的具体属性,比如 Text 的 fontColor 就无法使用了

@Extend 装饰器

前边讲述的 @Styles 着重用来解决样式复用的问题,但是对于一些特殊的样式,比如 Text 组件的 fontColor 属性,我们无法通过 @Styles 来解决。这个时候 就可以 使用 @Extend 装饰器了

如何使用:

@Extend(组件名) function 方法名(参数) {}

使用规则:

  • 1.支持通用属性、通用事件,以及组件特有属性、特有事件
  • 2.只能在全局声明
  • 3.方法支持传递参数(普通常量、状态变量);传递状态变量可以支持UI刷新
  • 4.方法参数支持 function

代码解释一下:

全局声明定义 如下代码中,在 StyleReusePage.ets 中定义了一个全局的 extendTextBlue 方法,通过 @Extend(Text) 来声明,然后通过 .fontColor(Color.Blue) 来设置字体颜色为蓝色。

支持的属性就不列了,如前边所讲,通用属性、通用事件,以及组件特有属性、特有事件 都是支持的

StyleReusePage.ets// 定义扩展样式
@Extend(Text)
function extendTextBlue() {
  .fontColor(Color.Blue)
}

// 使用样式,在build()中
Text("Extend-blue").margin({ top: 10 }).extendTextBlue()

方法支持传递参数

定义 extendTextColor ,传递一个 Color 类型的参数 color,并且在方法内部使用。这样更加灵活,可以让这个用处更多~

// 定义
@Extend(Text)
function extendTextColor(color: Color) {
  .fontColor(color)
}

// 使用
定义变量:
@Local color: Color = Color.Red

使用:
Text("Extend-自定义颜色").margin({ top: 10 }).extendTextColor(this.color)

上述代码的好处是,可以通过传递参数,来做到大部分样式复用+某些特殊样式定制。同时参数支持状态变量,比如我在其他地方修改了了状态变量 color ,那么 对应的UI的色值就会同步进行变化。

图2

2

方法参数支持function

方法的参数中,支持传递 function,这样我们可以对于一些事件来做定制化处理。 比如如下case实现:

Text的字体颜色、字号、高度、内边距、背景色,是固定的,但是要求他们点击事件是不同的,那么就可以通过传递一个 function 来实现不同的点击事件,其他的通用样式统一实现,代码如下:

// 定义扩展函数,通过传递一个 diyClick 事件,实现定制化点击事件逻辑
@Extend(Text)
function extendTextWithFunction(diyClick: () => void) {
  .fontColor(Color.White)
  .backgroundColor(Color.Black)
  .borderRadius(20)
  .height(40)
  .width(200)
  .textAlign(TextAlign.Center)
  .onClick((e) => {
    diyClick()
  })
}

// 使用:将 extendTextWithFunction 应用到 两个不同的 Text上,同时传递不同的 diyClick 事件,实现 不同的 toast 内容
      Text("Extend-toast:1").margin({ top: 10 }).extendTextWithFunction(() => {
        promptAction.showToast({ message: "1" })
      })
      Text("Extend-toast:2").margin({ top: 10 }).extendTextWithFunction(() => {
        promptAction.showToast({ message: "2" })
      })

图3

3

图4

4

以上,就是 @Extend的相关使用。最终总结一个完整的例子:

两个 Text样式一致,但是点击事件不同,第一个Text点击,可以将第二个Text的背景色改为红色;第二个Text点击,将第一个Text的背景色改为蓝色。直接上源码了:

// 定义扩展方法
@Extend(Text)
function extendTextWithFunctionChangeColor(diyClick: () => void, color: Color) {
  .fontColor(Color.White)
  .backgroundColor(color)
  .borderRadius(20)
  .height(40)
  .width(300)
  .textAlign(TextAlign.Center)
  .onClick((e) => {
    diyClick()
  })
}

// 定义状态变量
@Local color1: Color = Color.Black
@Local color2: Color = Color.Black

// 使用
      Text("Extend-点击,下边Text背景色变成红色").margin({ top: 10 }).extendTextWithFunctionChangeColor(() => {
        this.color2 = Color.Red
      }, this.color1)
      Text("Extend-点击,上边Text背景色变成蓝色").margin({ top: 10 }).extendTextWithFunctionChangeColor(() => {
        this.color1 = Color.Blue
      }, this.color2)

效果图:

图5(默认状态)

5

图6(点击上边后的状态)

6

图7(点击下边后的状态)

7

但是会发现,他跟 @Styles 一样,最大的缺陷是不支持跨文件使用,这样就有很大的局限性了。。。那么接下来就看看是否有支持跨文件使用的方式呢?

AttributeModifier

前边的@Styles@Extend 都是定义在当前组件内部,如果在其他组件中想要复用,就无法实现了;同时在内部对于业务逻辑的编写可能也捉襟见肘,那么此时就轮到 AttributeModifier 出马了。

从官方文档来看的话,AttributeModifier 提供了更强的能力和灵活性,且在持续完善全量的属性和事件设置能力,因此推荐优先使用 AttributeModifier

源码

declare interface AttributeModifier<T> {
    // 按压状态
    applyPressedAttribute?(instance: T): void;
    // 获得焦点状态
    applyFocusedAttribute?(instance: T): void;
    // 禁用状态
    applyDisabledAttribute?(instance: T): void;
    // 选中状态
    applySelectedAttribute?(instance: T): void;
}

可见 AttributeModifier 提供了不同状态的属性设置能力。

开发者需要实现其中的applyXxxAttribute方法来实现对应场景的属性设置。Xxx表示多态的场景, 支持默认态(Normal)、按压态(Pressed)、焦点态(Focused)、禁用态(Disabled)、选择态(Selected)。 T是组件的属性类型

一个简单的例子:

// 在一个单独的文件中定义 AttributeModifier
MyTextModifier.ets

export class MyTextModifier implements AttributeModifier<TextAttribute> {
  applyNormalAttribute(instance: TextAttribute): void {
    instance.fontColor(Color.White)
      .backgroundColor("#D8000000")
      .borderRadius(60)
      .width(200)
      .textAlign(TextAlign.Center)
      .padding({ top: 6, bottom: 6 })
  }

  applyPressedAttribute(instance: TextAttribute): void {
    instance.backgroundColor(Color.Red).width(100)
  }

  applyFocusedAttribute(instance: TextAttribute): void {
  }

  applyDisabledAttribute(instance: TextAttribute): void {
  }

  applySelectedAttribute(instance: TextAttribute): void {
  }
}


// 使用
StyleReusePage.ets 

build() {
  Text("Modifier").margin({ top: 20 }).attributeModifier(new MyTextModifier())
}

代码中针对 Text 的 默认状态设置了样式,按下状态设置了背景色+宽度缩小。