【harmonyOS】主题/换肤方案实现

130 阅读5分钟

鸿蒙项目主题/换肤方案

在鸿蒙项目实现一键切换主题功能。使用两种方案实现:切换颜色模式和动态属性。持久化主题切换配置,刷新和重新打开应用保持已切换的主题样式。

主题切换的实现应当尽量避免页面或组件中通过判断条件更换样式的代码,保证主题统一管理。

切换颜色模式

DarkLight颜色模式分别对应的baedark资源目录,更换颜色模式时自动使用对应的资源文件的同名属性,在使用setColorMode()更换颜色模式时加载对应的资源文件同名属性,已达到换肤功能。

setColorMode: 设置颜色模式。dark | light | 跟随系统

resources
+---base
|   +---element
|   |       color.json
|   |       string.json
|   |
|   \---media
|           image.png     
+---dark
|   +---element
|   |       color.json
|   |       string.json
|   |
|   \---media
|           image.png
|
\---rawfile

basedark两目录要求文件名称相同。媒体资源image.png两个目录名称相同,color.jsonstring.json两个目录的文件名称相同。color.jsonstring.json文件内所使用的颜色和字符串name相同value不同

// base颜色:color.json
{
  "name": "column_bg_color",
  "value": "#71c9ce"
}
// base文本:string.json
{
  "name": "text_content",
  "value": "主题切换改变这行文字的颜色"
}

// dark颜色:color.json
{
  "name": "column_bg_color",
  "value": "#e3fdfd"
}
// dark文本:string.json
{
  "name": "text_content",
  "value": "改变之后的文字的颜色和内容"
}

切换主题,应用不同的颜色、文本、媒体资源

import { AppFramework, ScreenUtil } from '@test/app_framework'
import { ConfigurationConstant } from '@kit.AbilityKit'
// AppFramework、ScreenUtil自定义的项目全局使用的属性、方法和窗口有关的全屏、安全区域等等。和主题切换无关
const ctx =  AppFramework.getInstance().getUIAbilityContext().getApplicationContext()
@Entry
@ComponentV2
struct Index {

  build() {
    Column({space: 30}) {
      // 主题1、2文本属性固定,切换主题不会改变
      Row(){
        Text('主题1').customTextStyle().onClick(() => {
          ctx.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK)
        })
        Text('主题2').customTextStyle().onClick(() => {
          ctx.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT)
        })
      }.width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      // 背景色
      Column().width(150).height(150).backgroundColor($r('app.color.column_bg_color'))
      Text($r('app.string.text_content')).fontSize(28).fontColor($r('app.color.text_color'))
      Image($r('app.media.image')).width(200)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .padding({
      top: ScreenUtil.getInstance().topAvoidHeight,
      bottom: ScreenUtil.getInstance().bottomAvoidHeight
    })
  }
}

@Extend(Text)
function customTextStyle() {
  .fontSize(30)
  .fontColor('#445566')
  .width('20%')
  .textAlign(TextAlign.Center)
}

动画.gif

动态属性实现

使用动态属性AttributeModifier<T>动态切换样式。相比较于切换颜色模式的只能应用两种主题,动态属性更加灵活且没有限制,并且可以把公共的样式、组件的常态、按压态、禁用态等样式统一管理,为所有事件添加处理函数(防抖、节流),扩展组件功能(配合文字转语音实现点击时语音播报)等等功能。

1、定义主题控制类,并使用PersistenceV2持久化当前主题状态使下次启动应用保持当前主题。

@ObservedV2
export class CustomThemeControl {
  // 主题切换标识,如果有多种主题定义成枚举类型
  @Trace isDark: boolean = false

  static setDark() {
    CustomThemeControl.getTheme().isDark = true
  }
  
  static setLight() {
    CustomThemeControl.getTheme().isDark = false
  }
  
  // 获取当前主题,用于在动态属性中判断
  static getTheme() {
    return PersistenceV2.connect<CustomThemeControl>(CustomThemeControl,() => new CustomThemeControl())!
  }
}

2、实现动态属性接口,根据需求实现功能

export class TextModifier implements AttributeModifier<TextAttribute> {
  private darkFC?: ResourceColor
  private lightFC?: ResourceColor
  // text常态时的样式,还有其它的聚焦、禁用等状态可以设置
  applyNormalAttribute(instance: TextAttribute): void {
    if (CustomThemeControl.getTheme().isDark) {
      instance.fontColor(this.darkFC)
    } else {
      instance.fontColor(this.lightFC)
    }
  }

  setDarkFC(c: ResourceColor) {
    this.darkFC = c
    return this
  }
  setLightFC(c: ResourceColor) {
    this.lightFC = c
    return this
  }
}

在页面中应用

const ctx =  AppFramework.getInstance().getUIAbilityContext().getApplicationContext()
@Entry
@ComponentV2
struct Index {

  build() {
    Column({space: 30}) {
      Row(){
        Text('主题1').customTextStyle().onClick(() => {
          CustomThemeControl.setDark()
        })
        Text('主题2').customTextStyle().onClick(() => {
          CustomThemeControl.setLight()
        })
      }.width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      Text('这是一段文本').fontSize(26).attributeModifier(new TextModifier().setDarkFC('#fff').setLightFC('#000'))
    }
    // ColumnModifier和TextModifier的实现类似
    .attributeModifier(new ColumnModifier().setDarkBgC('#000').setLightBgC('#fff'))
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .padding({
      top: ScreenUtil.getInstance().topAvoidHeight,
      bottom: ScreenUtil.getInstance().bottomAvoidHeight
    })
  }
}

@Extend(Text)
function customTextStyle() {
  .fontSize(30)
  .fontColor('#445566')
  .width('20%')
  .textAlign(TextAlign.Center)
}

动画1.gif

需要注意当页面设置沉浸式时,切换主题时要更换状态栏颜色,如上图展示,切换成黑色主题时和状态栏同色,状态栏不可见、

3、在颜色模式切换时可以改变文本内容,使用AttributeUpdater也可以达到这样的效果

export class CustomButtonModifier extends AttributeUpdater<ButtonAttribute, ButtonInterface> {
  private bgDark?: ResourceColor
  private bgLight?: ResourceColor
  private textD?: ResourceStr
  private textL?: ResourceStr
  initializeModifier(instance: ButtonAttribute): void {
    instance.backgroundColor(CustomThemeControl.getTheme().isDark ? this.bgDark : this.bgLight)
    this.updateConstructorParams(CustomThemeControl.getTheme().isDark ? this.textD : this.textL)
  }

  setDarkC(c: ResourceColor) {
    this.bgDark = c
    return this
  }
  setLightC(c: ResourceColor) {
    this.bgLight = c
    return this
  }
  setTextD(str: ResourceStr) {
    this.textD = str
    return this
  }
  setTextL(str: ResourceStr) {
    this.textL = str
    return this
  }
}

在页面中使用

const ctx =  AppFramework.getInstance().getUIAbilityContext().getApplicationContext()
@Entry
@ComponentV2
struct Index {

  build() {
    Column({space: 30}) {
      Row(){
        Text('主题1').customTextStyle().onClick(() => {
          CustomThemeControl.setDark()
        })
        Text('主题2').customTextStyle().onClick(() => {
          CustomThemeControl.setLight()
        })
      }.width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      Text('这是一段文本').fontSize(26).attributeModifier(new TextModifier().setDarkFC('#fff').setLightFC('#000'))
      Button()
        .attributeModifier(
          new CustomButtonModifier()
            .setDarkC(Color.Blue)
            .setLightC(Color.Red)
            .setTextD('黑暗迪迦')
            .setTextL('闪耀迪迦')
        )
    }
    .attributeModifier(new ColumnModifier().setDarkBgC('#000').setLightBgC('#fff'))
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .padding({
      top: ScreenUtil.getInstance().topAvoidHeight,
      bottom: ScreenUtil.getInstance().bottomAvoidHeight
    })
  }
}

@Extend(Text)
function customTextStyle() {
  .fontSize(30)
  .fontColor('#445566')
  .width('20%')
  .textAlign(TextAlign.Center)
}

动画2.gif

总结

1、当项目只需要两种主题且组件只涉及颜色、内容更换时使用切换颜色模式方案。当项目多种主题、有标准的UI方案、需要整体扩展组件功能时使用动态属性方案。

2、动态属性并不是只能使用样式还可以使用原组件可用的所有事件,在自定义的实现类中自定义方法,应用在clickchange等事件上扩展组件功能。

3、集成属性。@Extend@Styles无法导入导出,动态属性是个类可以导入导出。例如项目中UI文本通常只有几类,使用动态属性方案设置文本的颜色、大小、字重等通过方法传参便捷完成样式,后续ui更改也可集中修改。