前言 iOS 开发中,UITableView 随处可见,而在点击 UITableView 的 cell 的时候,如果他的子视图设置了透明颜色以外的颜色,子视图的背景颜色会进行相关的改变,效果如下图。 ![cell 被点击时,子视图的背景颜色会产生变化.gif](https://static.oschina.net/uploads/img/201611/21150327_HuGk.gif) 这种情况是不是有种似曾相识的感觉 如果没有,我再举几个很多人使用的 App 上对于这种情况处理不佳的例子,注意左右对比 ![新浪微博 cell 点击时消失的灰色分割线](https://static.oschina.net/uploads/img/201611/21150327_G62i.png) ![简书 cell 点击时消失的蓝色小圈圈](https://static.oschina.net/uploads/img/201611/21150327_xMWP.png) 产生这种情况的原因是因为 cell 在点击的时候会将子视图的背景颜色设置为透明色,而这里微博的分割线和简书的蓝色小圈圈应该是用了一种 UIView 设置了背景颜色来实现的,于是在点击过程中,本来是灰色的分割线,和本来是蓝色的小圈圈,都“消失”了。 网上对于这种情况的解决方案,大多是下面这种解决方式: 修改 cell 的选中样式: -
cell.selectionStyle = UITableViewCellSelectionStyleNone; 这种解决方案,可以正确的达到点击的时候子视图背景颜色不再改变,只是美中不足的是,这种方法不仅去除了我们不想要的子视图背景颜色改变的效果,还去除了 cell 本身 contentView 的背景颜色改变的效果。 此时点击 cell ,用户不再会感觉到有任何变化,为了取消子视图背景颜色改变效果,而取消 cell 的选中效果,这种做法不太友好。 此时又有网友提出,自己实现这种选中效果,在 cell 的 contentView 的最下层添加一个 button,很好的思路,只是,如果此时你的 cell 已经使用 xib 布局的差不多了,或者使用纯代码写的差不多了,再向 cell 的 contentView 和你所添加的 subview 中间添加一个 button 就显得有点麻烦。 懒癌晚期,想有一劳永逸的解决办法,最好是那种不对原工程做任何改动的 下面是我思考的过程: 我先看看 cell 的 contentView 中所有子视图调用设置背景颜色的方法时的函数调用栈,看看颜色改变成透明眼色之前调用了哪些方法 cell 子视图更改颜色之前,究竟做了些什么? 我新建了一个 .m 文件,并在其中使用 runtime 的 Method Swizzle 对设置背景颜色方法进行替换,在新的设置背景颜色方法中打印函数调用栈,具体代码如下: ![使用 runtime 打印设置背景颜色透明时的函数调用栈](https://static.oschina.net/uploads/img/201611/21150327_82Et.png) 这里如果不懂运行时的相关用法,可以自行去了解一下 runtime 的相关使用以及概念。 然后我们运行进行相关测试,发现打印了两次函数调用栈,意味着,在点击开始和结束之后,cell 的子视图有两次被设置成了透明颜色。其中一次是点击开始时,改变子视图背景颜色为透明色,还有一次是点击结束时,改变子视图背景颜色为透明色。 ![点击 cell 改变子视图背景颜色时的函数调用栈](https://static.oschina.net/uploads/img/201611/21150327_O8uQ.png) ![取消点击 cell 时恢复子视图背景颜色时的函数调用栈](https://static.oschina.net/uploads/img/201611/21150327_wPAt.png) 对比点击前后的函数调用栈,我们可以观察到,在点击 cell 的设置子视图背景颜色的前后都调用了 -
_setOpaque:forSubview: showSelectedBackgroundView:animated: setHighlighted:animated: 等方法; 其中: - 前两个方法是私有 API,使用私有 API 的结果无法预料。。可能会被苹果爸爸拒绝上线,所以,这里不考虑他们
- 最后一个
setHighlighted![:animated:](https://static.oschina.net/uploads/img/201611/21150330_FNSd.png) 方法在点击时,传入的 highlighted 为 YES,点击结束传入的为 NO。似乎可以考虑在这里里一个 FLAG - 另外,在 cell 默认加载出来的时候也会调用
setHighlighted![:animated:](https://static.oschina.net/uploads/img/201611/21150330_FNSd.png) 方法,只不过传入的 highlighted 为 NO - 那么此处我已经确定可以用最后一个方法立一个 FLAG 了,也就是我在这里,只需要在
highlighted 传入为 YES 的时候让子视图禁止调用改变背景颜色的方法,然后在再次为 NO 的时候,恢复可以调用改变背景颜色。 - 这似乎是一个可行的方法,但是,不要忘了,函数调用栈中的方法,是越早调用的方法越在下面,这里
setHighlighted![:animated:](https://static.oschina.net/uploads/img/201611/21150330_FNSd.png) 方法的调用在最后一次设置背景颜色为透明之前,也就是说,如果我按照上面的方法做了,那我就只能阻止一次设置背景颜色为透明 - 我想到的解决方案,再立一个 FLAG,然后,在
highlighted 为 YES 的时候,让子视图禁止设置背景颜色,然后第一次设置为透明会被阻止,第二次的时候,判断 FLAG,通过 FLAG 再阻止下一次设置背景颜色为透明 禁止设置子视图背景颜色的具体代码如下: ![禁止 UILabel 设置背景颜色](https://static.oschina.net/uploads/img/201611/21150330_9Ahi.png) 这段代码使用了 runtime 在交换的设置背景颜色方法中,对通过 runtime 添加的 forbidSetBackgroundColor 属性进行判断,如果设置为 YES,则不能设置背景颜色。 接下来对 cell 的 setHighlighted![:animated:](https://static.oschina.net/uploads/img/201611/21150330_FNSd.png) 进行替换,在合适的时机,设置 UILabel 能否改变背景颜色即可 具体代码如下: ![替换 UITableViewCell 的 setHighlighted-animated- 方法](https://static.oschina.net/uploads/img/201611/21150330_wKlm.png) 接下来,我们该立 FLAG 了 ![通过 Flag 阻止最后一次设置背景颜色为透明的操作](https://static.oschina.net/uploads/img/201611/21150330_Y0LK.png) 我们添加了一个新的属性,当 highlighted 为 YES 的时候,forbidSetBackgroundColor 会变成 YES,此时,FLAG:shouldIgnoreSetClearBackgroundColorHandle 设置为 YES,下一次 forbidSetBackgroundColor 不起作用的时候,通过 shouldIgnoreSetClearBackgroundColorHandle 可以再次阻止背景色被设置成透明色的操作。 写完这些代码,我想我以后又可以偷懒了,不需要以后每次去关心 cell 点击改变子视图背景颜色的问题,只需要新增一个 .m 文件,不需要改动原先一句代码,就可以实现点击 cell 前后,子视图颜色不会改变,而且也保留了原生的点击效果 不足之处 这篇文章中所实现的仅仅是 UITableViewCell 在点击过程中 UILabel 的颜色不会改变,其他的子控件,诸如,UIButton,UIView 之类的视图,背景色仍然会改变,而且也不能解决多个视图层叠的背景色被清空的情况,于是,我写了一个分类: - 实现了所有带有非透明颜色的 UI 控件在 UITableViewCell 中,点击过程中不会改变背景颜色的效果
- 使用方法很简单,直接将分类拖入工程中,即可使用
- 欢迎大家使用和 Star,欢迎提出修改意见,更欢迎提出 bug
感谢 在这里要感谢两个网友 angelen10 和 MuYanQin 对我所写的代码的 bug 提出,欢迎更多人指出我的 bug。 结尾 - 最后附上 .m 文件所有的代码
- 使用方法很简单,在工程中新增一个 .m 文件,把下方的所有代码粘贴上去
- 不需要你调用和改变任何原先的代码,这段代码就会自动工作了,
- 如果你有什么疑问,或者发现了我代码中有什么不对的地方,欢迎评论和纠错
- 如果你想研究和阅读本篇文章测试所写的工程,可以到我的 GitHub: 上下载
-
#import <UIKit/UIKit.h> #import <objc/runtime.h> -
UILabel (RuntimeTest) -
(nonatomic, assign) BOOL forbidSetBackgroundColor; -
(nonatomic, assign) BOOL shouldIgnoreSetClearBackgroundColorHandle; -
-
@implementation UILabel (RuntimeTest) -
+ (void)load { -
SEL originalSelector = @selector(setBackgroundColor:); SEL swizzledSelector = @selector(ex_setBackgroundColor:); -
Method originalMethod = class_getInstanceMethod(self, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); -
BOOL success = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } -
- (void)ex_setBackgroundColor:(UIColor *)backgroundColor { -
if (self.forbidSetBackgroundColor){ self.shouldIgnoreSetClearBackgroundColorHandle = YES; } else { if (backgroundColor == [UIColor clearColor]) { if (self.shouldIgnoreSetClearBackgroundColorHandle) { self.shouldIgnoreSetClearBackgroundColorHandle = NO; } else { [self ex_setBackgroundColor:backgroundColor]; } } else { [self ex_setBackgroundColor:backgroundColor]; } } } -
- (BOOL)shouldIgnoreSetClearBackgroundColorHandle { -
id value = objc_getAssociatedObject(self, _cmd); if (value == nil) { objc_setAssociatedObject(self, _cmd, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return [objc_getAssociatedObject(self, _cmd) boolValue]; } -
- (void)setShouldIgnoreSetClearBackgroundColorHandle:(BOOL)shouldIgnoreSetClearBackgroundColorHandle { -
objc_setAssociatedObject(self, @selector(shouldIgnoreSetClearBackgroundColorHandle), @(shouldIgnoreSetClearBackgroundColorHandle), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -
- (BOOL)forbidSetBackgroundColor { -
id value = objc_getAssociatedObject(self, _cmd); if (value == nil) { objc_setAssociatedObject(self, _cmd, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return [objc_getAssociatedObject(self, _cmd) boolValue]; } -
- (void)setForbidSetBackgroundColor:(BOOL)forbidSetBackgroundColor { -
objc_setAssociatedObject(self, @selector(forbidSetBackgroundColor), @(forbidSetBackgroundColor), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -
@end -
@implementation UITableViewCell (RuntimeTest) -
+ (void)load { -
SEL originalSelector = @selector(setHighlighted:animated:); SEL swizzledSelector = @selector(ex_setHighlighted:animated:); -
Method originalMethod = class_getInstanceMethod(self, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); -
BOOL success = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } -
- (void)ex_setHighlighted:(BOOL)highlighted animated:(BOOL)animated { -
if (highlighted == YES) { for (UIView *subview in self.contentView.subviews) { if ([subview isKindOfClass:[UILabel class]]) { UILabel *label = (UILabel *)subview; label.forbidSetBackgroundColor = YES; } } } else { for (UIView *subview in self.contentView.subviews) { if ([subview isKindOfClass:[UILabel class]]) { UILabel *label = (UILabel *)subview; label.forbidSetBackgroundColor = NO; } } } [self ex_setHighlighted:highlighted animated:animated]; } -
-
@end 原文:http://bbs.520it.com/forum.php?mod=viewthread&tid=2781 |