因为最近做哀掉日App
黑白化的需求,需要依据下发的配置对APP
的首页或者整体进行置为灰色,因此这里针对方案做一下总结。
一. 方案一
最开始想到的就是给App
添加一层灰色滤镜,将App
所有的视图通过滤镜,都变为灰色,也就是在window
或者首页的view
上添加这样一种灰色滤镜效果,使得整个App
界面或者首页变为灰色。
+ (void)addGreyFilterToView:(UIView *)view {
UIView *coverView = [[UIView alloc] initWithFrame:view.bounds];
coverView.userInteractionEnabled = NO;
coverView.tag = kFJFGreyFilterTag;
coverView.backgroundColor = [UIColor lightGrayColor];
coverView.layer.compositingFilter = @"saturationBlendMode";
coverView.layer.zPosition = FLT_MAX;
[view addSubview:coverView];
}
+ (void)removeGreyFilterToView:(UIView *)view {
UIView *greyView = [view viewWithTag:kFJFGreyFilterTag];
[greyView removeFromSuperview];
}
我们可以通过addGreyFilterToView
方法将灰色滤镜放置到App
的对应的视图上,比如window
或者首页view
上,这样就可以保证对应视图,及其所有子视图都为灰色。
如下是将addGreyFilterToView
添加到App
对应的window
上,来使得整个界面变化灰色。
展示效果:
该方法的主要原理是设置一个浅灰色的lightGrayColor
的颜色,然后将该浅灰色的饱和度,应用到将要显示的视图上,使得将要显示的视图,显示灰色。
饱和度是指色彩的鲜艳程度,也称色彩的纯度。饱和度取决于该色中含色成分和消色成分(灰色)的比例。含色成分越大,饱和度越大;消色成分越大,饱和度越小。
但很可惜,这个方法在iOS12
以下的系统,不起作用。即使是iOS12
以上的系统也有部分会显示直接的纯灰色画面
比如在我的12.5
的系统的iPhone6
上,直接显示灰色画面:
因此如果项目只需要适配iOS13
以上的系统,该方法还是可行的,不然就需要做版本兼容。
二.方案二
可以通过CAFilter
这个私有类,设置一个滤镜,先将要显示的视图转为会单色调(即黑白色),然后再将整个视图的背景颜色设置为灰色,来达到这样的置位灰色效果。
// 灰度滤镜
+ (NSArray *)greyFilterArray {
//获取RGBA颜色数值
CGFloat r,g,b,a;
[[UIColor lightGrayColor] getRed:&r green:&g blue:&b alpha:&a];
//创建滤镜
id cls = NSClassFromString(@"CAFilter");
id filter = [cls filterWithName:@"colorMonochrome"];
//设置滤镜参数
[filter setValue:@[@(r),@(g),@(b),@(a)] forKey:@"inputColor"];
[filter setValue:@(0) forKey:@"inputBias"];
[filter setValue:@(1) forKey:@"inputAmount"];
return [NSArray arrayWithObject:filter];
}
展示效果:
该方法优点是不受系统限制,但缺点就是展示效果不像第一种通过饱和度来调整的自然,感觉像真的盖了一层灰色的蒙层到App
上,而且因为使用的私用类CAFilter
,具有风险性。
三. 方案三
- 一开始考虑能否参考安卓的思路,递归去遍历视图及其相关子视图,然后判断视图的类型,对其进行图片、颜色等进行处理,但这里有个问题就是如何确定遍历的时机,一开始是
hook
了UIView
相关的addSubview:
等方法,然后在添加子视图的时候,去遍历处理所有子视图。 - 但是比如说
UIImageView
,添加到父视图的时候,并没有显示图片,只有网络下载成功之后才设置图片,因此你必须监听UIImageView
设置图片的方法,同样对应UILabel
等控件也是一样,所以在添加子视图的时候,去遍历处理所有子视图明显达不到要求。 - 因此这里采取了
hook
的相关操作,对UIColor
、UIImage
、UIImageView
、WKWebView
等进行hook
,然后再进行处理。
1. UIImage处理
- A. 取出图片像素的颜色值,对每一个颜色值依据灰度算法计算出原来色值的的灰度值,然后重新生成灰色的图片。
// 转化灰度图片
- (UIImage *)fjf_convertToGrayImage {
return [self fjf_convertToGrayImageWithRedRate:0.3 blueRate:0.59 greenRate:0.11];
}
// 转化灰度图片
- (UIImage *)fjf_convertToGrayImageWithRedRate:(CGFloat)redRate
blueRate:(CGFloat)blueRate
greenRate:(CGFloat)greenRate {
const int RED = 1;
const int GREEN = 2;
const int BLUE = 3;
// Create image rectangle with current image width/height
CGRect imageRect = CGRectMake(0,0, self.size.width* self.scale, self.size.height* self.scale);
int width = imageRect.size.width;
int height = imageRect.size.height;
// the pixels will be painted to this array
uint32_t *pixels = (uint32_t*) malloc(width * height *sizeof(uint32_t));
// clear the pixels so any transparency is preserved
memset(pixels,0, width * height *sizeof(uint32_t));
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// create a context with RGBA pixels
CGContextRef context = CGBitmapContextCreate(pixels, width, height,8, width *sizeof(uint32_t), colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedLast);
// paint the bitmap to our context which will fill in the pixels array
CGContextDrawImage(context,CGRectMake(0,0, width, height), [self CGImage]);
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
uint8_t *rgbaPixel = (uint8_t*) &pixels[y * width + x];
// convert to grayscale using recommended method: http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
uint32_t gray = redRate * rgbaPixel[RED] + greenRate * rgbaPixel[GREEN] + blueRate * rgbaPixel[BLUE];
// set the pixels to gray
rgbaPixel[RED] = gray;
rgbaPixel[GREEN] = gray;
rgbaPixel[BLUE] = gray;
}
}
// create a new CGImageRef from our context with the modified pixels
CGImageRef imageRef = CGBitmapContextCreateImage(context);
// we're done with the context, color space, and pixels
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
free(pixels);
// make a new UIImage to return
UIImage *resultUIImage = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:UIImageOrientationUp];
// we're done with image now too
CGImageRelease(imageRef);
return resultUIImage;
}
- 对
UIImage
相对应的初始化方法进行hook
,因为项目里面使用混编,所以有用到UIImage
的类方法进行初始化,也有用到实例方法进行初始化。
+ (void)fjf_startGreyStyle {
//交换方法
NSError *error = NULL;
[UIImage fjf_swizzleMethod:@selector(initWithData:)
withMethod:@selector(fjf_initWithData:)
error:&error];
[UIImage fjf_swizzleMethod:@selector(initWithData:scale:)
withMethod:@selector(fjf_initWithData:scale:)
error:&error];
[UIImage fjf_swizzleMethod:@selector(initWithContentsOfFile:)
withMethod:@selector(fjf_initWithContentsOfFile:)
error:&error];
[UIImage fjf_swizzleClassMethod:@selector(imageNamed:)
withClassMethod:@selector(fjf_imageNamed:)
error:&error];
[UIImage fjf_swizzleClassMethod:@selector(imageNamed:inBundle:compatibleWithTraitCollection:)
withClassMethod:@selector(fjf_imageNamed:inBundle:compatibleWithTraitCollection:)
error:&error];
}
这里实例初始化方法,有一点需要注意,最后返回的时候必须调用实例对象的实例方法来返回一个UIImage
对象。
+ (UIImage *)fjf_imageNamed:(NSString *)name {
UIImage *image = [self fjf_imageNamed:name];
return [UIImage fjf_converToGrayImageWithImage:image];
}
+ (nullable UIImage *)fjf_imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection {
UIImage *image = [self fjf_imageNamed:name inBundle:bundle compatibleWithTraitCollection:traitCollection];
return [UIImage fjf_converToGrayImageWithImage:image];
}
- (instancetype)fjf_initWithContentsOfFile:(NSString *)path {
UIImage *greyImage = [[UIImage alloc] fjf_initWithContentsOfFile:path];
greyImage = [UIImage fjf_converToGrayImageWithImage:greyImage];
return [self initWithCGImage:greyImage.CGImage];
}
- (UIImage *)fjf_initWithData:(NSData *)data {
UIImage *greyImage = [[UIImage alloc] fjf_initWithData:data];
greyImage = [UIImage fjf_converToGrayImageWithImage:greyImage];
return [self initWithCGImage:greyImage.CGImage];
}
- (UIImage *)fjf_initWithData:(NSData *)data scale:(CGFloat)scale {
UIImage *greyImage = [[UIImage alloc] fjf_initWithData:data scale:scale];
greyImage = [UIImage fjf_converToGrayImageWithImage:greyImage];
return [self initWithCGImage:greyImage.CGImage];
}
2.UIImageView
UIImageView
通过hook
图片的设置方法setImage
和initWithCoder
来对图片进行处理。
这里需要注意的就是对拉伸的图片,动效图,还有xib
上图片的处理。
A. 如果设置的图片是动效图,比如
gif
图,可以通过SDImageCoderHelper
的framesFromAnimatedImage
函数将gif
解析获取对应的图片数组,然后对图片数组里面的每一张图进行灰度化,直到图片数组所有图片都灰度化完成,再将灰度的图片数组合成动效图。B. 如果是拉伸的图片,比如聊天消息的背景图,因为在
UIImage
的相关初始化方法中已经处理过,变成灰色的图片,所以在UIImageView
的setImage
方法里面不需要再对图片进行灰色处理,否则就会失去拉伸的效果,这里可以通过判断图片是否为_UIResizableImage
来判断是否为拉伸图片。C.如果是普通图片则进行普通的灰度处理。
// 转换为灰度图标
- (void)fjf_convertToGrayImageWithImage:(UIImage *)image {
NSArray<SDImageFrame *> *animatedImageFrameArray = [SDImageCoderHelper framesFromAnimatedImage:image];
if (animatedImageFrameArray.count > 1) {
NSMutableArray<SDImageFrame *> *tmpThumbImageFrameMarray = [NSMutableArray array];
[animatedImageFrameArray enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UIImage *targetImage = [obj.image fjf_convertToGrayImage];
SDImageFrame *thumbFrame = [SDImageFrame frameWithImage:targetImage duration:obj.duration];
[tmpThumbImageFrameMarray addObject:thumbFrame];
}];
UIImage *greyAnimatedImage = [SDImageCoderHelper animatedImageWithFrames:tmpThumbImageFrameMarray];
[self fjf_setImage:greyAnimatedImage];
} else if([image isKindOfClass:NSClassFromString(@"_UIResizableImage")]){
[self fjf_setImage:image];
} else {
UIImage *im = [image fjf_convertToGrayImage];
[self fjf_setImage:im];
}
}
- D.如果是放在
Xib
上的图片,因为Xib
经过编译会变成Nib
,Nib
存储了Xib
里面的各种信息的二进制文件,Xcode
上的Xib
文件可以直接显示图片,是因为Xcode
支持访问项目中图像和资源,所以是通过Xcode
去读取和显示的,而实际App
,是在调用[UINib nibWithNibName
等方法的时候,将Nib
的数据以及关联的资源读取到内存中,而Nib
去相关联的图片资源的时候,走的是更底层的系统方法,而不是UIImage
相关的图片初始化方法,因此对于Xib
上的图片的灰度处理,需要放在UIImageView
的initWithCoder
方法上。
- (nullable instancetype)fjf_initWithCoder:(NSCoder *)coder {
UIImageView *tmpImgageView = [self fjf_initWithCoder:coder];
[self fjf_convertToGrayImageWithImage:tmpImgageView.image];
return tmpImgageView;
}
3. UIColor
UIColor
主要通过hook
相关的颜色初始化方法,然后依据颜色的RGB
值去算出对应的灰度值,来显示。
// 开启 黑白色
+ (void)fjf_startGreyStyle {
NSError *error = NULL;
[UIColor fjf_swizzleClassMethod:@selector(redColor)
withClassMethod:@selector(fjf_redColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(greenColor)
withClassMethod:@selector(fjf_greenColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(blueColor)
withClassMethod:@selector(fjf_blueColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(cyanColor)
withClassMethod:@selector(fjf_cyanColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(yellowColor)
withClassMethod:@selector(fjf_yellowColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(magentaColor)
withClassMethod:@selector(fjf_magentaColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(orangeColor)
withClassMethod:@selector(fjf_orangeColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(purpleColor)
withClassMethod:@selector(fjf_purpleColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(brownColor)
withClassMethod:@selector(fjf_brownColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(systemBlueColor)
withClassMethod:@selector(fjf_systemBlueColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(systemGreenColor)
withClassMethod:@selector(fjf_systemGreenColor)
error:&error];
[UIColor fjf_swizzleClassMethod:@selector(colorWithRed:green:blue:alpha:)
withClassMethod:@selector(fjf_colorWithRed:green:blue:alpha:)
error:&error];
[UIColor fjf_swizzleMethod:@selector(initWithRed:green:blue:alpha:)
withMethod:@selector(fjf_initWithRed:green:blue:alpha:)
error:&error];
}
4. WKWebView
WKWebView
是通过hook
初始化的initWithFrame:configuration:
方法,进行js
脚本注入来实现灰色化.
- (instancetype)fjf_initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration {
// js脚本
NSString *jScript = @"var filter = '-webkit-filter:grayscale(100%);-moz-filter:grayscale(100%); -ms-filter:grayscale(100%); -o-filter:grayscale(100%) filter:grayscale(100%);';document.getElementsByTagName('html')[0].style.filter = 'grayscale(100%)';";
// 注入
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
// 配置对象
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
configuration = wkWebConfig;
WKWebView *webView = [self fjf_initWithFrame:frame configuration:configuration];
return webView;
}
5. CAShapeLayer
CAShapeLayer
主要是通过hook
了setFillColor:
和 setStrokeColor
两个方法来解决lottie
动画相关的灰色。
+ (void)fjf_startGreyStyle {
NSError *error = NULL;
[CAShapeLayer fjf_swizzleMethod:@selector(setFillColor:)
withMethod:@selector(fjf_setFillColor:)
error:&error];
[CAShapeLayer fjf_swizzleMethod:@selector(setStrokeColor:)
withMethod:@selector(fjf_setStrokeColor:)
error:&error];
}
- (void)fjf_setStrokeColor:(CGColorRef)color {
UIColor *greyColor = [UIColor fjf_generateGrayColorWithOriginalColor:[UIColor colorWithCGColor:color]];
[self fjf_setStrokeColor:greyColor.CGColor];
}
- (void)fjf_setFillColor:(CGColorRef)color {
UIColor *greyColor = [UIColor fjf_generateGrayColorWithOriginalColor:[UIColor colorWithCGColor:color]];
[self fjf_setFillColor:greyColor.CGColor];
}
6. NSData
之所以会用到NSData
是因为项目里面地图需要保持原来的颜色,而地图相关的图片加载是通过UIImage
的initWithData
方法,因此无法判断图片的来源是否为地图的图片,所以通过hook
了NSData
的initWithContentsOfURL
和initWithContentsOfFile
方法来依据加载路径,来对地图相关的图片设置标志位fjf_isMapImageData
是否为地图图片,如果是地图图片,在UIImage
的initWithData
取出该标志位,判断如果NSData
是地图图片数据,就保持原来颜色。
这里是自身项目需要所以单独处理。
7. UIViewController
因为黑白色可以只开启在首页,其他界面要保持原来的颜色,所以需要首页跳转到其他页面的时候需要做判断,来保证只有首页会被置为灰色。
- 这里对
UIViewController
的init
、viewWillAppear:
进行hook
,在init
方法中对当前UIViewController
类型行判断,如果是首页的VC
就置位灰色,如果是其他VC
就保持原来逻辑。
- 之所以是在
UIViewController
的init
方法判断,是因为有些vc
会先出初始化一些子视图,然后再调用UIViewController
的viewDidLoad
,只有在init
方法,才能保证当前vc
上的所有子视图都能保持原来颜色。
- 然后再
UIViewController
的viewWillAppear:
方法判断是否回到首页,如果回到首页,就递归遍历首页的View
,对其进行图片、颜色等进行置灰处理,这样做是为了避免,当切到其他页面的时候,首页收到通知或者其他推送,更新了视图,使得更新的视图也能保持灰色。
四. 总结
Demo:https://github.com/fangjinfeng/MySampleCode
以上几种方法各有优劣,可以针对各自的需求进行选择。