iOS换肤功能的简单处理框架

栏目: IOS · 发布时间: 5年前

内容简介:换肤功能是在APP开发过程中遇到的比较多的场景,为了提供更好的用户体验,许多APP会为用户提供切换主题的功能。主题颜色管理涉及到的的步骤有DEMO代码:因为涉及到多种配置,所以以代码的方式定义颜色实践和维护的难度是比较高的,一种合适的方案是--颜色的配置是通过配置文件的形式进行导入的。配置文件会经过转换步骤,最终形成代码层级的配置,以全局的方式提供给各个模块使用,这里会涉及到一个颜色管理者的概念,一般地这回事一个单例对象,提供全局访问的接口。同一个APP中在不同的模块中保存不同的主题颜色配置,在不同的层级中

换肤功能是在APP开发过程中遇到的比较多的场景,为了提供更好的用户体验,许多APP会为用户提供切换主题的功能。主题颜色管理涉及到的的步骤有

  • 颜色配置

  • 使用颜色

  • UI元素动态变更的能力

  • 动态修改配置

  • 主题包管理

  • 如何实施

  • 优化

效果如下:

iOS换肤功能的简单处理框架

DEMO代码: https://gitee.com/dhar/iosdemos/tree/master/YTThemeManagerDemo

颜色配置

因为涉及到多种配置,所以以代码的方式定义颜色实践和维护的难度是比较高的,一种合适的方案是--颜色的配置是通过配置文件的形式进行导入的。配置文件会经过转换步骤,最终形成代码层级的配置,以全局的方式提供给各个模块使用,这里会涉及到一个颜色管理者的概念,一般地这回事一个单例对象,提供全局访问的接口。同一个APP中在不同的模块中保存不同的主题颜色配置,在不同的层级中也可以存在不同的主题颜色配置,因为涉及到层级间的配置差异,所以颜色的配置需要引入一个等级的概念,一般地较高层级颜色的配置等级是高于较低层级的,存在相同的配置较高层级的配置会覆盖较低层级的配置。

我们采用的颜色配置的文件形如下面所示,为什么是在一个json文件的colorkey下面呢,是为了考虑到未来的扩展性,如果不同的主题会涉及到一些尺寸值的差异化,我们可以添加dimensionskey进行扩展配置。

{
  "color": {
      "Black_A":"323232",
      "Black_AT":"323232",
      "Black_B":"888888",
      "Black_BT":"888888",

      "White_A":"ffffff",
      "White_AT":"ffffff",
      "White_AN":"ffffff",

      "Red_A":"ff87a0",
      "Red_AT":"ff87a0",
      "Red_B":"ff5073",
      "Red_BT":"ff5073",

      "Colour_A":"377ce4",
      "Colour_B":"6aaafa",
      "Colour_C":"ff8c55",
      "Colour_D":"ffa200",
      "Colour_E":"c4a27a",
  }
}

有了以上的配置,颜色配置的工作主要就是解析该配置文件,把配置保存在一个单例对象中即可,这部分主要的步骤如下:

  • 配置文件类表根据等级排序

  • 获取每个配置文件中的配置,进行保存

  • 通知外部主题颜色配置发生改变

对应的代码如下,这里有个需要注意的地方是,加载配置文件的时候使用了文件读写锁进行读写的锁定操作,防止读脏数据的发生,直到配置文件加载完成,释放读写锁,这时读进程可以继续。

- (void)loadConfigWithFileName:(NSString *)fileName level:(NSInteger)level {
    if (fileName.length == 0) {
        return;
    }

    pthread_rwlock_wrlock(&_rwlock);
    __block BOOL finded = NO;
    [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
        if ([obj.fileName isEqualToString:fileName]) {
            finded = YES;
            *stop = YES;
        }
    }];
    if (!finded) {
        // 新增配置文件
        YTThemeConfigFile *file = [[YTThemeConfigFile alloc] init];
        file.fileName = fileName;
        file.level = level;
        [self.configFileQueue addObject:file];
        // 优先级排序
        [self.configFileQueue sortUsingComparator:^NSComparisonResult(YTThemeConfigFile *_Nonnull obj1, YTThemeConfigFile *_Nonnull obj2) {
            if (obj1.level > obj2.level) {
                return NSOrderedDescending;
            }
            return NSOrderedAscending;
        }];
        [self setupConfigFilesContainDefault:YES];
    }
    pthread_rwlock_unlock(&_rwlock);
}

- (void)setupConfigFilesContainDefault:(BOOL)containDefault {
    NSMutableDictionary *defaultColorDict = nil, *currentColorDict = nil;

    // 加载默认配置
    if (containDefault) {
        defaultColorDict = [NSMutableDictionary dictionary];
        [self loadConfigDataWithColorMap:defaultColorDict valueMap:nil isDefault:YES];

        self.defaultColorMap = defaultColorDict;
    }

    // 加载主题配置
    if (_themePath.length > 0) {
        currentColorDict = [NSMutableDictionary dictionary];
        [self loadConfigDataWithColorMap:currentColorDict valueMap:nil isDefault:NO];

        self.currentColorMap = currentColorDict;
    }

    // 发送主体颜色变更通知
    [self notifyThemeDidChange];
}

- (void)notifyThemeDidChange {
    NSArray *allActionObjects = self.actionMap.objectEnumerator.allObjects;
    for (YTThemeAction *action in allActionObjects) {
        [action notifyThemeDidChange];
    }
}

- (void)loadConfigDataWithColorMap:(NSMutableDictionary *)colorMap valueMap:(NSMutableDictionary *)valueMap isDefault:(BOOL)isDefault {
    // 每一次新增一个配置文件,所有配置文件都得重新计算一次,这里有很多重复多余的工作
    [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
        NSDictionary *dict = nil;
        if (isDefault) {
            dict = obj.defaultDict;
        } else {
            dict = obj.currentDict;
        }
        if (dict.count > 0) {
            [self loadThemeColorTo:colorMap from:dict]; // 将所有配置表中的color字段的数据都放到colorMap中
        }
    }];
}

- (void)loadThemeColorTo:(NSMutableDictionary *)dictionary from:(NSDictionary *)from {
    NSDictionary<NSString *, NSString *> *colors = from[@"color"];
    [colors enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) {
        // 十六进制字符串转为UIColor
        UIColor *color = [UIColor yt_nullcolorWithHexString:obj];
        if (color) {
            [dictionary setObject:color forKey:key];
        } else {
            [dictionary setObject:obj forKey:key];
        }
    }];
}

管理者处理处理配置之外,还需要暴露外部接口给客户端使用,以用于获取不同主题下对应的颜色色值、图片资源、尺寸信息等和主题相关的信息。比如我们会提供一个colorForKey方法获取不同主题下的同一个key对应的颜色色值,获取色值的大致步骤如下:

  • 从当前的主题配置中获取

  • 从默认的主题配置中获取

  • 从预留的主题配置中获取

  • 如果重定向的配置,递归处理

  • 以上步骤都完成还未找到返回默认黑色

这里使用了读写锁的写锁,如果同时有写操作获取了该锁,读取进程会阻塞直到写操作的完成释放锁。

/**
 获取颜色值
 */
- (UIColor *)colorForKey:(NSString *)key {
    pthread_rwlock_rdlock(&_rwlock);
    UIColor *color = [self colorForKey:key isReserveKey:NO redirectCount:0];
    pthread_rwlock_unlock(&_rwlock);
    return color;
}

- (UIColor *)colorForKey:(NSString *)key isReserveKey:(BOOL)isReserveKey redirectCount:(NSInteger)redirectCount {
    if (key == nil) {
        return nil;
    }

    ///正常获取色值
    id colorObj = [_currentColorMap objectForKey:key];
    if (colorObj == nil) {
        colorObj = [_defaultColorMap objectForKey:key];
    }

    if (isReserveKey && colorObj == nil) {
        return nil;
    }

    ///看看是否有替补key
    if (colorObj == nil) {
        NSString *reserveKey = [_reserveKeyMap objectForKey:key];
        if (reserveKey) {
            colorObj = [self colorForKey:reserveKey isReserveKey:YES redirectCount:redirectCount];
        }
    }

    ///查看当前key 能否转成 color
    if (colorObj == nil) {
        colorObj = [UIColor yt_colorWithHexString:key];
    }

    if ([colorObj isKindOfClass:[UIColor class]]) {
        ///如果是 重定向 或者  替补 key 的color  要设置到 当前 colorDict 里面
        // 重定向的配置形如:"Red_A":"Red_B",
        if (redirectCount > 0 || isReserveKey) {
            [_currentColorMap ?: _defaultColorMap setObject:colorObj forKey:key];
        }
        return colorObj;
    } else {
        if (redirectCount < 3) { // 重定向递归
            return [self colorForKey:colorObj isReserveKey:NO redirectCount:redirectCount + 1];
        } else {
            return [UIColor blackColor];
        }
    }
}

使用颜色

颜色的使用也是经由管理者的,为了方便,定义一个颜色宏提供给客户端使用

#define YTThemeColor(key) ([[YTThemeManager sharedInstance] colorForKey:key])

客户端使用的代码如下:

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 200, 40)];
label.text = @"Text";
label.textColor = YTThemeColor(kCK_Red_A);
label.backgroundColor = YTThemeColor(kCK_Black_H);
[self.view addSubview:label];

另外,因为颜色配置的key为字符串类型,直接使用字符串常量并不是个好办法,所以把对应的字符串转换为宏定义是一个相对好的办法。第一个是方便使用,可以使用代码提示;第二个是不容易出错,特别是长的字符串;第三个也会一定程度上的提高效率。

YTColorDefine类的宏定义

// .h 中的声明
///Black
FOUNDATION_EXTERN NSString *kCK_Black_A;
FOUNDATION_EXTERN NSString *kCK_Black_AT;
FOUNDATION_EXTERN NSString *kCK_Black_B;
FOUNDATION_EXTERN NSString *kCK_Black_BT;

// .m 中的定义
NSString *kCK_Black_A = @"Black_A";
NSString *kCK_Black_AT = @"Black_AT";
NSString *kCK_Black_B = @"Black_B";
NSString *kCK_Black_BT = @"Black_BT";

主题包管理

在实际的落地项目中,主题包管理涉及到的事项包括主题包下载和解压和动态加载主题包等内容,最后的一步是更换主题配置文件所在的配置路径,为了演示的方便,我们会把不同主题的资源放置在bundle中某一个特定的文件夹下,通过切换管理者中的主题路径配置来达到切换主题的效果,和动态下载更换主题的步骤是一样的。

管理者提供一个设置主题配置的配置路径的方法,在该方法中改变配置路径的同时,重新加载配置即可,代码如下

/**
 设置主题文件的路径
 @param themePath 文件的路径
 */
- (void)setupThemePath:(NSString *)themePath {
    pthread_rwlock_wrlock(&_rwlock);

    _themePath = [themePath copy];

    self.currentColorMap = nil;

    if ([_themePath.lowercaseString isEqualToString:[[NSBundle mainBundle] resourcePath].lowercaseString]) {
        _themePath = nil;
    }

    self.currentThemePath = _themePath;

    for (int i = 0; i < self.configFileQueue.count; i++) {
        YTThemeConfigFile *obj = [self.configFileQueue objectAtIndex:i];
        [obj resetCurrentDict];
    }
    [self setupConfigFilesContainDefault:NO];

    pthread_rwlock_unlock(&_rwlock);
}

如何实施

以上的流程涉及到的只是iOS平台下的一个技术解决方案,真实的实践过程中会涉及到安卓平台、Web页面、UI出图的标注,这些是要进行统一处理的,才能在各个端上有一致的体验。第一步就是制定合理的颜色规范,把规范同步给各个端的利益相关人员;第二部是UI出图颜色是规范的颜色定义值,而不是比如#ffffff这样的颜色,需要是比如White_A这样规范的颜色定义值,这样客户端处理使用的就是White_A这个值,不用管在不同主题下不同的颜色表现形式。

优化

loadConfigDataWithColorMap方法调用的优化

如果模块很多,每个模块都会调用loadConfigWithFileName加载配置文件,那么loadConfigDataWithColorMap方法处理文件的时间复杂度是O(N*N),会重复处理很多多余的工作,理想的做法是底层保存一份公有的颜色配置,然后在APP层加载一份定制化的配置,在模块中不用再加载主题配置文件,这样会提高效率。

参考资料

读写锁pthread_rwlock_t的使用


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大学程序设计课程与竞赛训练教材

大学程序设计课程与竞赛训练教材

吴永辉、王建德 / 机械工业出版社 / 2013-6 / 69.00

本书每章为一个主题,实验内容安排紧扣大学算法和数学的教学,用程序设计竞赛中的算法和数学试题作为实验试题,将算法和数学的教学与程序设计竞赛的解题训练结合在一起;在思维方式和解题策略的训练方面,以问题驱动和启发式引导为主要方式,培养读者通过编程解决问题的能力。 本书特点: 书中给出的234道试题全部精选自ACM国际大学生程序设计竞赛的世界总决赛以及各大洲赛区现场赛和网络预赛、大学程序设计竞......一起来看看 《大学程序设计课程与竞赛训练教材》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具