iOS实现换肤功能的简单处理框架(附源码)

前言

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

  • 颜色配置
  • 使用颜色
  • UI元素动态变更的能力
  • 动态修改配置
  • 主题包管理
  • 如何实施
  • 优化

效果如下:

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的使用

读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。

具有强读者同步和强写者同步两种形式

强读者同步:当写者没有进行写操作,读者就可以访问;

强写者同步:当所有写者都写完之后,才能进行读操作,读者需要最新的信息,一些事实性较高的系统可能会用到该所,比如定票之类的。

读写锁的操作:

读写锁的初始化:

定义读写锁:          pthread_rwlock_t  m_rw_lock;

函数原型:              pthread_rwlock_init(pthread_rwlock_t * ,pthread_rwattr_t *);

返回值:0,表示成功,非0为一错误码

读写锁的销毁:

函数原型:             pthread_rwlock_destroy(pthread_rwlock_t* );

返回值:0,表示成功,非0表示错误码

获取读写锁的读锁操作:分为阻塞式获取和非阻塞式获取,如果读写锁由一个写者持有,则读线程会阻塞直至写入者释放读写锁。

阻塞式:

函数原型:pthread_rwlock_rdlock(pthread_rwlock_t*);

非阻塞式:

函数原型:pthread_rwlock_tryrdlock(pthread_rwlock_t*);

返回值: 0,表示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待

获取读写锁的写锁操作:分为阻塞和非阻塞,如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。

阻塞式:

函数原型:pthread_rwlock_wrlock(pthread_rwlock_t*);

非阻塞式:

函数原型:pthread_rwlock_trywrlock(pthread_rwlock_t*);

返回值: 0,表示成功

释放读写锁:

函数原型:pthread_rwlock_unlock(pthread_rwlock_t*);

总结(转):

互斥锁与读写锁的区别:

当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;

当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。

读写锁的优点:

对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

读写锁描述:

获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁。

有关这种类型问题(多个读出者和一个写入者)的其它说法有读出者与写入者问题以及多读出者-单写入者锁。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • iOS开发之App主题切换解决方案完整版(Swift版)

    本篇博客就来介绍一下iOS App中主题切换的常规做法,当然本篇博客中只是提到了一种主题切换的方法,当然还有其他方法,在此就不做过多赘述了.本篇博客中所涉及的Demo完全使用Swift3.0编写完成,并使用iOS的NSNotification来触发主题切换的动作.本篇博客我们先对我们的主题系统进行设计,然后给出具体实现方式.当然在我们设计本篇博客所涉及的Demo时,我们要遵循"高内聚,低耦合","面向接口编程","便于维护与扩充"等特点. 本篇博

  • 一步一步实现iOS主题皮肤切换效果

    本文实例为大家分享了iOS主题皮肤切换代码,供大家参考,具体内容如下 1. 主题皮肤功能切换介绍 主题切换就是根据用户设置不同的主题,来动态改变用户的界面,通常会改变navigationBar背景图片.tabBar背景图片.tabBar中的按钮的图片和选中的背景图片.navigationItem.title 标题的字体颜色.UI中其他元素控件 下载源代码地址: http://xiazai.jb51.net/201609/yuanma/ThemeSkinSetup(jb51.net).rar 2.

  • iOS实现换肤功能的简单处理框架(附源码)

    前言 换肤功能是在APP开发过程中遇到的比较多的场景,为了提供更好的用户体验,许多APP会为用户提供切换主题的功能.主题颜色管理涉及到的的步骤有 颜色配置 使用颜色 UI元素动态变更的能力 动态修改配置 主题包管理 如何实施 优化 效果如下: DEMO代码:https://gitee.com/dhar/iosdemos/tree/master/YTThemeManagerDemo 颜色配置 因为涉及到多种配置,所以以代码的方式定义颜色实践和维护的难度是比较高的,一种合适的方案是--颜色的配置是通

  • 仿百度换肤功能的简单实例代码

    效果:(换肤出来一个div,选择你想要的图片,作为网页背景,保存) 要点:cookie保存状态 html代码: <body> <div id="header"> <div id="header_con"> <div class="dbg"><a href="javascript:;" onclick="showImgBox()">换肤</a&

  • Android编程实现画板功能的方法总结【附源码下载】

    本文实例讲述了Android编程实现画板功能的方法.分享给大家供大家参考,具体如下: Android实现画板主要有2种方式,一种是用自定义View实现,另一种是通过Canvas类实现.当然自定义View内部也是用的Canvas.第一种方式的思路是,创建一个自定义View(推荐SurfaceView),在自定义View里通过Path对象记录手指滑动的路径调用lineTo()绘制:第二种方式的思路是,先用Canvas绘制一张空的Bitmap,通过ImageView的setImageBitmap()方

  • PHP实现简单聊天室(附源码)第1/2页

    一,聊天室模块实现1,聊天室主页面窗口设置 复制代码 代码如下: <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /><!--载入配置文件--><?php include_once 'config.php';?><!--页面标题--><title><?php echo CHAT_NAME; ?></ti

  • jQuery实现带右侧索引功能的通讯录示例【附源码下载】

    本文实例讲述了jQuery实现带右侧索引功能的通讯录.分享给大家供大家参考,具体如下: 通过jquery.charfirst.pinyin.js实现点击字母自动定位.实现动态加载通讯录数据. 完整实例代码点击此处本站下载. 主要代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/htm

  • JavaScript实现简单图片滚动附源码下载

    昨晚德国和葡萄牙的焦点之战你看了吗?北京时间凌晨的比赛中,C罗领衔的葡萄牙0-4德国被完灭--他是金球奖得主.欧洲金靴.欧冠冠军核心,在葡萄牙队--9张图 C罗告诉你什么叫欲哭无泪 复制代码 代码如下: <span style="font-size:14px;"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtm

  • js简单实现网页换肤功能

    我发现网上写换肤功能写的有点长,就想想如何更简单方法实现这个功能,于是我自己写了一个. html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> <link id="changelink" rel="stylesheet" href="css/default.css&

  • jquery cookie实现的简单换肤功能适合小网站

    前段时间试着使用jquery cookie的时候,做了一个简单的换肤功能,只适合小网站并且代码低级. 首先引入jquery和cookie插件 复制代码 代码如下: <script type="text/javascript" src="__PUBLIC__/js/jquery.js"></script> <script type="text/javascript" src="__PUBLIC__/js/co

  • 使用vue + less 实现简单换肤功能的示例

    做的换肤效果比较简单,只是顶部导航背景色的改变.下面是效果图. 首先,先说一下我最初的思路. 我最初的想法是使用less定义变量,然后通过js来切换变量,通过切换的变量来达到换肤的效果. 我先新建了一个 theme.less文件,代码如下: @theme:@themea; @themea:pink; @themeb:blue; @themec:gray; 如我最开始的想法,应该是通过点击事件来改变变量 @theme 的值. 我用了element-ui这个框架,所以我的下拉菜单的代码也不复杂: <

  • JavaScript实现换肤功能

    一,js换肤的基本原理 基本原理很简单,就是使用 JS 切换对应的 CSS 样式表文件.例如导航网站 Hao123 的右上方就有网页换肤功能.除了切换 CSS 样式表文件之外,通常的网页换肤还需要通过 Cookie 来记录用户之前更换过的皮肤,这样下次用户访问的时候,就可以自动使用上次用户配置的选项. 那么基本工作流程就出来了:访问网页--JS 读取 Cookie --如果没有,使用默认皮肤--如果有,使用指定皮肤:用户点击换肤选项--JS 控制替换对应的 CSS 样式表--将皮肤选项写进 Co

随机推荐