iOS哀掉日黑白化

因为最近做哀掉日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上,来使得整个界面变化灰色。

展示效果:

grey_filter_view.gif

该方法的主要原理是设置一个浅灰色的lightGrayColor的颜色,然后将该浅灰色的饱和度,应用到将要显示的视图上,使得将要显示的视图,显示灰色。

饱和度是指色彩的鲜艳程度,也称色彩的纯度。饱和度取决于该色中含色成分和消色成分(灰色)的比例。含色成分越大,饱和度越大;消色成分越大,饱和度越小。

但很可惜,这个方法在iOS12以下的系统,不起作用。即使是iOS12以上的系统也有部分会显示直接的纯灰色画面
比如在我的12.5的系统的iPhone6上,直接显示灰色画面:

IMG_0334.PNG
[图片上传中...(IMG_0334.PNG-5505ab-1656228410216-0)]

因此如果项目只需要适配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];
}

展示效果:

filter_grey_view.gif

该方法优点是不受系统限制,但缺点就是展示效果不像第一种通过饱和度来调整的自然,感觉像真的盖了一层灰色的蒙层到App上,而且因为使用的私用类CAFilter,具有风险性。

三. 方案三

  • 一开始考虑能否参考安卓的思路,递归去遍历视图及其相关子视图,然后判断视图的类型,对其进行图片、颜色等进行处理,但这里有个问题就是如何确定遍历的时机,一开始是hookUIView相关的addSubview:等方法,然后在添加子视图的时候,去遍历处理所有子视图。
  • 但是比如说UIImageView,添加到父视图的时候,并没有显示图片,只有网络下载成功之后才设置图片,因此你必须监听UIImageView设置图片的方法,同样对应UILabel等控件也是一样,所以在添加子视图的时候,去遍历处理所有子视图明显达不到要求。
  • 因此这里采取了hook的相关操作,对UIColorUIImageUIImageViewWKWebView等进行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图片的设置方法setImageinitWithCoder来对图片进行处理。

这里需要注意的就是对拉伸的图片,动效图,还有xib上图片的处理。

  • A. 如果设置的图片是动效图,比如gif图,可以通过SDImageCoderHelperframesFromAnimatedImage函数将gif解析获取对应的图片数组,然后对图片数组里面的每一张图进行灰度化,直到图片数组所有图片都灰度化完成,再将灰度的图片数组合成动效图。

  • B. 如果是拉伸的图片,比如聊天消息的背景图,因为在UIImage的相关初始化方法中已经处理过,变成灰色的图片,所以在UIImageViewsetImage方法里面不需要再对图片进行灰色处理,否则就会失去拉伸的效果,这里可以通过判断图片是否为_UIResizableImage来判断是否为拉伸图片。

  • C.如果是普通图片则进行普通的灰度处理。

// 转换为灰度图标
- (void)fjf_convertToGrayImageWithImage:(UIImage *)image {
    NSArray *animatedImageFrameArray = [SDImageCoderHelper framesFromAnimatedImage:image];
    if (animatedImageFrameArray.count > 1) {
        NSMutableArray *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经过编译会变成NibNib存储了Xib里面的各种信息的二进制文件,Xcode上的Xib文件可以直接显示图片,是因为Xcode支持访问项目中图像和资源,所以是通过Xcode去读取和显示的,而实际App,是在调用[UINib nibWithNibName等方法的时候,将Nib的数据以及关联的资源读取到内存中,而Nib去相关联的图片资源的时候,走的是更底层的系统方法,而不是UIImage相关的图片初始化方法,因此对于Xib上的图片的灰度处理,需要放在UIImageViewinitWithCoder方法上。
- (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主要是通过hooksetFillColor: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是因为项目里面地图需要保持原来的颜色,而地图相关的图片加载是通过UIImageinitWithData方法,因此无法判断图片的来源是否为地图的图片,所以通过hookNSDatainitWithContentsOfURLinitWithContentsOfFile方法来依据加载路径,来对地图相关的图片设置标志位fjf_isMapImageData是否为地图图片,如果是地图图片,在UIImageinitWithData取出该标志位,判断如果NSData是地图图片数据,就保持原来颜色。

这里是自身项目需要所以单独处理。

7. UIViewController

因为黑白色可以只开启在首页,其他界面要保持原来的颜色,所以需要首页跳转到其他页面的时候需要做判断,来保证只有首页会被置为灰色。

  • 这里对UIViewControllerinitviewWillAppear:进行hook,在init方法中对当前UIViewController类型行判断,如果是首页的VC就置位灰色,如果是其他VC就保持原来逻辑。
  • 之所以是在UIViewControllerinit方法判断,是因为有些vc会先出初始化一些子视图,然后再调用UIViewControllerviewDidLoad,只有在init方法,才能保证当前vc上的所有子视图都能保持原来颜色。
  • 然后再UIViewControllerviewWillAppear:方法判断是否回到首页,如果回到首页,就递归遍历首页的View,对其进行图片、颜色等进行置灰处理,这样做是为了避免,当切到其他页面的时候,首页收到通知或者其他推送,更新了视图,使得更新的视图也能保持灰色。

四. 总结

Demo:https://github.com/fangjinfeng/MySampleCode

以上几种方法各有优劣,可以针对各自的需求进行选择。

你可能感兴趣的:(iOS哀掉日黑白化)