2022-06-17

需求背景

对于一些使用广泛可变性强的组件比如按钮,单一的视觉无法满足其他项目直接调用,为满足应用界面设计的多变性,同时提高开发者效率,我们希望提高组件库的可定制化,因此提供换肤功能以及多种类组件中的样式定制功能,允许其他项目调用组件库时可以自己定义自己项目的主题风格的皮肤,也允许开发者对指定组件进行样式改造。

设计目标

性能

一个方案的落地前提得有性能的保障,不重新初始化视图,避免出现闪屏、卡顿等性能缺陷现象,同时也要保障功能稳定,不能存在部分组件不按预期切换主题想象。

可维护性

组件库需不断迭代完善,应避免过多的条件判断,避免在单个组件上有过多的主题特殊逻辑,主题的设置和组件的实现应解藕,保证后续可维护可扩展。

可配置

可配置分为两部分,一部分为可配置任意全局统一的样式变量,或者某个组件的局部样式;另一部分为强制模式,即指定部分组件不跟随主题变化而变化,保留着本身的一种样式。

易用性

提供快捷接入主题的接口,降低学习成本和时间成本。

粒度细分

组件层面的主题定制、整套组件库的主题定制。开发者可以修改全局样式,比如更换全局中字号的字体大小,也可以局部修改样式,比如按钮组件的边框颜色。

样式提取

暴露出提取当前整套样式的接口,方便开发者提取指定样式做二次操作。比如开发者需要提取当前主题颜色作为视图背景色,可从组件库中获取。

样式可定制内容,包括但不限于:

颜色、文本、按钮、图片

设计方案

主题功能目录结构

[图片上传失败...(image-b8a671-1655458474131)]

wsf_initializer: 主题初始化文件,由组件库使用方调用;

wsf_theme: 主题导出;

wsf_theme_configurator: 主题配置器,可以配置一套默认主题、获取配置主题、注册主题;

configs文件夹:放组件配置文件;

base文件夹: 基础配置类以及一些给定的配置参数;


生成样式配置文件

配置文件大体分为这三类:全部配置文件、公共配置文件和组件配置文件

全部配置文件:主要是用于集合公共配置和组件配置, 它的构造函数如下:

  WsfAllThemeConfig({
    WsfCommonConfig? commonConfig,
    WsfButtonConfig? buttonConfig,
    ..
    String configId = globalConfigID,
  })  : _commonConfig = commonConfig,
        _buttonConfig = buttonConfig,
        ...
        _calendarConfig = calendarConfig;

公共配置文件:配置公共样式的对象,它的构造函数如下:

WsfCommonConfig({
    Color? brandPrimary,
    Color? brandSuccess,
    Color? brandWarning,
    ...
    String configId = globalConfigID,
  })  : _brandPrimary = brandPrimary,
        _brandSuccess = brandSuccess,
        _brandWarning = brandWarning,
        ...
        super(configId: configId);

组件配置文件:配置需要配置主题的某一组件的对象,它的构造函数如下:

WsfButtonConfig({
    double? btnRadius,
    double? btnHeight,
    ...
    String configId = globalConfigID,
  })  : _btnRadius = btnRadius,
        _btnHeight = btnHeight,
        ...
        super(configId: configId);

主题配置的使用

调用初始化文件,将项目上下午context传入主题注册函数中用于screenutil插件完成自适应功能

WsfInitializer.register(context: context);

WsfInitializer.register(context: context,allThemeConfig); //allThemeConfig为配置的全局主题

单一组件主题配置

//吸底按钮文字加按钮
WsfBottomTextAndButtonPanel(
  mainTitle: '主按钮',
  themeDataMain: WsfButtonConfig(btnWidth: 120.w),
  textChild: const Text("¥200"),
  onTapMain: () {

  },
),

初始化文件代码


/// Wsf 初始化
class WsfInitializer {
  /// 手动注册时,默认注册渠道是 GLOBAL_CONFIG_ID
  static register({
    required BuildContext context,
    WsfAllThemeConfig? allThemeConfig,
    String configId = globalConfigID,
  }) {
    /// 屏幕适配
    ScreenUtil.init(
      BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width,
          maxHeight: MediaQuery.of(context).size.height),
      designSize: const Size(375, 667),
      context: context,
      minTextAdapt: true,
      orientation: Orientation.portrait,
    );

    /// 配置主题定制
    WsfThemeConfigurator.instance.register(allThemeConfig, configId: configId);
  }
}

主题配置器

import 'package:wsf_ui/src/theme/base/wsf_default_config_utils.dart';

import 'configs/wsf_all_config.dart';

const String wsfConfigId = 'WSF_CONFIG_ID';
const String globalConfigID = 'GLOBAL_CONFIG_ID';

class WsfThemeConfigurator {
  WsfThemeConfigurator._() {
    _checkAndInitWsfConfig();
  }

  static final WsfThemeConfigurator _instance = WsfThemeConfigurator._();

  static WsfThemeConfigurator get instance {
    return _instance;
  }

  Map<String, WsfAllThemeConfig> globalConfig = <String, WsfAllThemeConfig>{};

  /// 手动注册时,默认注册渠道是 GLOBAL_CONFIG_ID
  void register(
    WsfAllThemeConfig? allThemeConfig, {
    String configId = globalConfigID,
  }) {
    // 打平内部字段
    if (allThemeConfig != null) {
      // 赋值传入配置
      globalConfig[configId] = allThemeConfig..initThemeConfig(configId);
    }
  }

  /// 获取合适的配置
  /// 1、获取 configId 对应的全局主题配置,
  /// 2、若获取的为 null,则使用默认的全局配置。
  /// 3、若没有配置 GLOBAL_CONFIG_ID ,则使用 WSF_CONFIG_ID 的配置。
  WsfAllThemeConfig getConfig({String configId = globalConfigID}) {
    WsfAllThemeConfig? allThemeConfig = globalConfig[configId] ??
        globalConfig[globalConfigID] ??
        globalConfig[wsfConfigId];
    assert(allThemeConfig != null, 'No suitable config found.');
    return allThemeConfig!;
  }

  /// 检查是否有默认配置
  bool isWsfConfig() => globalConfig[wsfConfigId] != null;

  /// 没有默认配置 配置默认配置
  void _checkAndInitWsfConfig() {
    if (!isWsfConfig()) {
      globalConfig[wsfConfigId] = WsfDefaultConfigUtils.defaultAllConfig;
    }
  }
}


注册主题可先配置好WsfAllThemeConfig 对象 ,定制好主题, 传入的主题id为:GLOBAL_CONFIG_ID, 默认的id为:WSF_CONFIG_ID

分别为全局主题、默认的组件库主题

主题配置优先级


组件自定义主题 > 外部定义的全局主题 > 组件库默认主题

核心操作

WsfButtonConfig defaultThemeConfig = themeData ?? WsfButtonConfig();

defaultThemeConfig = WsfThemeConfigurator.instance
    .getConfig(configId: defaultThemeConfig.configId)
    .buttonConfig
    .merge(defaultThemeConfig);

themeData为自定义单一主题,WsfButtonConfig() 为默认主题或者全局主题;

WsfButtonConfig() 设计代码如下:

double get btnRadius =>
      _btnRadius ?? WsfDefaultConfigUtils.defaultButtonConfig.btnRadius;

组件属性的获取是遵循以下规律,如果外部全局配置了属性则返回该属性,如果没有,则返回组件库默认配置的属性。

如果自定义了单一主题的某一些属性,其他属性采用组件库默认配置的属性,则使用配置对象里面的merge函数,将自定义的属性和默认的组件库属性进行合并,返回新的配置对象;


主题的输出

新建wsf_theme.dart 文件,将组件配置对象export给外部调用

/// theme export

library wsf_theme;

export 'base/wsf_base_config.dart';
export 'base/wsf_default_config_utils.dart';
export 'configs/wsf_all_config.dart';
export 'configs/wsf_button_config.dart';
export 'configs/wsf_calendar_config.dart';
export 'configs/wsf_checkbox_config.dart';
export 'configs/wsf_common_config.dart';
export 'configs/wsf_radio_config.dart';
export 'wsf_initializer.dart';
export 'wsf_theme_configurator.dart';

总结

这个主题设计思想主要采用面向对象的设计方法,有灵活运用抽象类、单例模式、对象属性merge以及运算符??等方式,非常巧妙的分离解藕了主题和组件之间的关系,达到了设计目标,满足了主题定制化以及开发对指定组件样式改造的需求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容

  • 第二大板块就是学习。学习的核心是一句话,叫作“你如何改善你看不到的东西”,这就是学习的意义。当你不学习的时候,你不...
    bycall阅读 349评论 0 1
  • Skia[https://skia.org/docs/] 跨平台2D图形库,有软\硬两种模式, 39W行代码(sr...
    SMSM阅读 4,906评论 1 1
  • 免费加优惠策略——营销中最常用的促销工具 引言:在商业世界中,促销就像空气,无处不在。在消费者空间中,促销就像水一...
    赵汝滔058阅读 960评论 0 0
  • 加强非公有制企业党建工作有多方面的重要意义 在我国非公有制企业是社会主义市场...
    你咋不上天阅读 181评论 0 0
  • 2021 年初,输液护理学会发布了最新版本的输液治疗实践标准。在过去的二十年里,这些标准一直是静脉通路领域最重要的...
    朗月斋主阅读 109评论 0 0