因?yàn)樽罱霭У羧?code>App黑白化的需求君丁,需要依據(jù)下發(fā)的配置對(duì)APP
的首頁(yè)或者整體進(jìn)行置為灰色,因此這里針對(duì)方案做一下總結(jié)砰蠢。
一. 方案一
最開(kāi)始想到的就是給App
添加一層灰色濾鏡,將App
所有的視圖通過(guò)濾鏡虱岂,都變?yōu)榛疑簿褪窃?code>window或者首頁(yè)的view
上添加這樣一種灰色濾鏡效果菠红,使得整個(gè)App
界面或者首頁(yè)變?yōu)榛疑?/p>
+ (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];
}
我們可以通過(guò)addGreyFilterToView
方法將灰色濾鏡放置到App
的對(duì)應(yīng)的視圖上第岖,比如window
或者首頁(yè)view
上,這樣就可以保證對(duì)應(yīng)視圖试溯,及其所有子視圖都為灰色蔑滓。
如下是將addGreyFilterToView
添加到App
對(duì)應(yīng)的window
上,來(lái)使得整個(gè)界面變化灰色遇绞。
展示效果:
該方法的主要原理是設(shè)置一個(gè)淺灰色的lightGrayColor
的顏色键袱,然后將該淺灰色的飽和度,應(yīng)用到將要顯示的視圖上摹闽,使得將要顯示的視圖蹄咖,顯示灰色。
飽和度是指色彩的鮮艷程度付鹿,也稱(chēng)色彩的純度澜汤。飽和度取決于該色中含色成分和消色成分(灰色)的比例蚜迅。含色成分越大,飽和度越大俊抵;消色成分越大谁不,飽和度越小。
但很可惜徽诲,這個(gè)方法在iOS12
以下的系統(tǒng)刹帕,不起作用。即使是iOS12
以上的系統(tǒng)也有部分會(huì)顯示直接的純灰色畫(huà)面
比如在我的12.5
的系統(tǒng)的iPhone6
上谎替,直接顯示灰色畫(huà)面:
因此如果項(xiàng)目只需要適配iOS13
以上的系統(tǒng)偷溺,該方法還是可行的,不然就需要做版本兼容院喜。
二.方案二
可以通過(guò)CAFilter
這個(gè)私有類(lèi)亡蓉,設(shè)置一個(gè)濾鏡,先將要顯示的視圖轉(zhuǎn)為會(huì)單色調(diào)(即黑白色),然后再將整個(gè)視圖的背景顏色設(shè)置為灰色喷舀,來(lái)達(dá)到這樣的置位灰色效果砍濒。
// 灰度濾鏡
+ (NSArray *)greyFilterArray {
//獲取RGBA顏色數(shù)值
CGFloat r,g,b,a;
[[UIColor lightGrayColor] getRed:&r green:&g blue:&b alpha:&a];
//創(chuàng)建濾鏡
id cls = NSClassFromString(@"CAFilter");
id filter = [cls filterWithName:@"colorMonochrome"];
//設(shè)置濾鏡參數(shù)
[filter setValue:@[@(r),@(g),@(b),@(a)] forKey:@"inputColor"];
[filter setValue:@(0) forKey:@"inputBias"];
[filter setValue:@(1) forKey:@"inputAmount"];
return [NSArray arrayWithObject:filter];
}
展示效果:
該方法優(yōu)點(diǎn)是不受系統(tǒng)限制,但缺點(diǎn)就是展示效果不像第一種通過(guò)飽和度來(lái)調(diào)整的自然硫麻,感覺(jué)像真的蓋了一層灰色的蒙層到App
上爸邢,而且因?yàn)槭褂玫乃接妙?lèi)CAFilter
,具有風(fēng)險(xiǎn)性拿愧。
三. 方案三
- 一開(kāi)始考慮能否參考安卓的思路杠河,遞歸去遍歷視圖及其相關(guān)子視圖,然后判斷視圖的類(lèi)型浇辜,對(duì)其進(jìn)行圖片券敌、顏色等進(jìn)行處理,但這里有個(gè)問(wèn)題就是如何確定遍歷的時(shí)機(jī)柳洋,一開(kāi)始是
hook
了UIView
相關(guān)的addSubview:
等方法待诅,然后在添加子視圖的時(shí)候,去遍歷處理所有子視圖熊镣。 - 但是比如說(shuō)
UIImageView
,添加到父視圖的時(shí)候卑雁,并沒(méi)有顯示圖片,只有網(wǎng)絡(luò)下載成功之后才設(shè)置圖片绪囱,因此你必須監(jiān)聽(tīng)UIImageView
設(shè)置圖片的方法测蹲,同樣對(duì)應(yīng)UILabel
等控件也是一樣,所以在添加子視圖的時(shí)候鬼吵,去遍歷處理所有子視圖明顯達(dá)不到要求扣甲。 - 因此這里采取了
hook
的相關(guān)操作,對(duì)UIColor
齿椅、UIImage
文捶、UIImageView
荷逞、WKWebView
等進(jìn)行hook
,然后再進(jìn)行處理粹排。
1. UIImage處理
- A. 取出圖片像素的顏色值种远,對(duì)每一個(gè)顏色值依據(jù)灰度算法計(jì)算出原來(lái)色值的的灰度值,然后重新生成灰色的圖片顽耳。
// 轉(zhuǎn)化灰度圖片
- (UIImage *)fjf_convertToGrayImage {
return [self fjf_convertToGrayImageWithRedRate:0.3 blueRate:0.59 greenRate:0.11];
}
// 轉(zhuǎn)化灰度圖片
- (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;
}
- 對(duì)
UIImage
相對(duì)應(yīng)的初始化方法進(jìn)行hook
坠敷,因?yàn)轫?xiàng)目里面使用混編,所以有用到UIImage
的類(lèi)方法進(jìn)行初始化射富,也有用到實(shí)例方法進(jìn)行初始化膝迎。
+ (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];
}
這里實(shí)例初始化方法,有一點(diǎn)需要注意胰耗,最后返回的時(shí)候必須調(diào)用實(shí)例對(duì)象的實(shí)例方法來(lái)返回一個(gè)UIImage
對(duì)象限次。
+ (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
通過(guò)hook
圖片的設(shè)置方法setImage
和initWithCoder
來(lái)對(duì)圖片進(jìn)行處理。
這里需要注意的就是對(duì)拉伸的圖片柴灯,動(dòng)效圖卖漫,還有xib
上圖片的處理。
A. 如果設(shè)置的圖片是動(dòng)效圖赠群,比如
gif
圖羊始,可以通過(guò)SDImageCoderHelper
的framesFromAnimatedImage
函數(shù)將gif
解析獲取對(duì)應(yīng)的圖片數(shù)組,然后對(duì)圖片數(shù)組里面的每一張圖進(jìn)行灰度化查描,直到圖片數(shù)組所有圖片都灰度化完成突委,再將灰度的圖片數(shù)組合成動(dòng)效圖。B. 如果是拉伸的圖片冬三,比如聊天消息的背景圖匀油,因?yàn)樵?code>UIImage的相關(guān)初始化方法中已經(jīng)處理過(guò),變成灰色的圖片勾笆,所以在
UIImageView
的setImage
方法里面不需要再對(duì)圖片進(jìn)行灰色處理敌蚜,否則就會(huì)失去拉伸的效果,這里可以通過(guò)判斷圖片是否為_UIResizableImage
來(lái)判斷是否為拉伸圖片。C.如果是普通圖片則進(jìn)行普通的灰度處理匠襟。
// 轉(zhuǎn)換為灰度圖標(biāo)
- (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
上的圖片钝侠,因?yàn)?code>Xib經(jīng)過(guò)編譯會(huì)變成Nib
该园,Nib
存儲(chǔ)了Xib
里面的各種信息的二進(jìn)制文件酸舍,Xcode
上的Xib
文件可以直接顯示圖片,是因?yàn)?code>Xcode支持訪問(wèn)項(xiàng)目中圖像和資源里初,所以是通過(guò)Xcode
去讀取和顯示的啃勉,而實(shí)際App
,是在調(diào)用[UINib nibWithNibName
等方法的時(shí)候双妨,將Nib
的數(shù)據(jù)以及關(guān)聯(lián)的資源讀取到內(nèi)存中,而Nib
去相關(guān)聯(lián)的圖片資源的時(shí)候淮阐,走的是更底層的系統(tǒng)方法叮阅,而不是UIImage
相關(guān)的圖片初始化方法,因此對(duì)于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
主要通過(guò)hook
相關(guān)的顏色初始化方法,然后依據(jù)顏色的RGB
值去算出對(duì)應(yīng)的灰度值状您,來(lái)顯示勒叠。
// 開(kāi)啟 黑白色
+ (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
是通過(guò)hook
初始化的initWithFrame:configuration:
方法,進(jìn)行js
腳本注入來(lái)實(shí)現(xiàn)灰色化.
- (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];
// 配置對(duì)象
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
configuration = wkWebConfig;
WKWebView *webView = [self fjf_initWithFrame:frame configuration:configuration];
return webView;
}
5. CAShapeLayer
CAShapeLayer
主要是通過(guò)hook
了setFillColor:
和 setStrokeColor
兩個(gè)方法來(lái)解決lottie
動(dòng)畫(huà)相關(guān)的灰色膏孟。
+ (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
之所以會(huì)用到NSData
是因?yàn)轫?xiàng)目里面地圖需要保持原來(lái)的顏色眯分,而地圖相關(guān)的圖片加載是通過(guò)UIImage
的initWithData
方法,因此無(wú)法判斷圖片的來(lái)源是否為地圖的圖片柒桑,所以通過(guò)hook
了NSData
的initWithContentsOfURL
和initWithContentsOfFile
方法來(lái)依據(jù)加載路徑弊决,來(lái)對(duì)地圖相關(guān)的圖片設(shè)置標(biāo)志位fjf_isMapImageData
是否為地圖圖片,如果是地圖圖片魁淳,在UIImage
的initWithData
取出該標(biāo)志位飘诗,判斷如果NSData
是地圖圖片數(shù)據(jù),就保持原來(lái)顏色先改。
這里是自身項(xiàng)目需要所以單獨(dú)處理疚察。
7. UIViewController
因?yàn)楹诎咨梢灾婚_(kāi)啟在首頁(yè),其他界面要保持原來(lái)的顏色仇奶,所以需要首頁(yè)跳轉(zhuǎn)到其他頁(yè)面的時(shí)候需要做判斷貌嫡,來(lái)保證只有首頁(yè)會(huì)被置為灰色。
- 這里對(duì)
UIViewController
的init
该溯、viewWillAppear:
進(jìn)行hook
岛抄,在init
方法中對(duì)當(dāng)前UIViewController
類(lèi)型行判斷,如果是首頁(yè)的VC
就置位灰色狈茉,如果是其他VC
就保持原來(lái)邏輯夫椭。
- 之所以是在
UIViewController
的init
方法判斷,是因?yàn)橛行?code>vc會(huì)先出初始化一些子視圖氯庆,然后再調(diào)用UIViewController
的viewDidLoad
,只有在init
方法蹭秋,才能保證當(dāng)前vc
上的所有子視圖都能保持原來(lái)顏色。
- 然后再
UIViewController
的viewWillAppear:
方法判斷是否回到首頁(yè)堤撵,如果回到首頁(yè)仁讨,就遞歸遍歷首頁(yè)的View
,對(duì)其進(jìn)行圖片实昨、顏色等進(jìn)行置灰處理洞豁,這樣做是為了避免,當(dāng)切到其他頁(yè)面的時(shí)候,首頁(yè)收到通知或者其他推送丈挟,更新了視圖刁卜,使得更新的視圖也能保持灰色。
四. 總結(jié)
Demo:https://github.com/fangjinfeng/MySampleCode
以上幾種方法各有優(yōu)劣曙咽,可以針對(duì)各自的需求進(jìn)行選擇蛔趴。