iOS哀掉日黑白化

作者: 果哥爸 | 来源:发表于2022-06-28 00:52 被阅读0次

    因为最近做哀掉日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 = [self fjf_initWithContentsOfFile:path];
        if (![path containsString:@"MASCTX.bundle"]) {
            greyImage = [UIImage fjf_converToGrayImageWithImage:greyImage];
        }
        return [self initWithCGImage:greyImage.CGImage];
    }
    
    - (UIImage *)fjf_initWithData:(NSData *)data {
        UIImage *greyImage = [[UIImage new] fjf_initWithData:data];
        return [self initWithCGImage:greyImage.CGImage];
    }
    
    - (UIImage *)fjf_initWithData:(NSData *)data scale:(CGFloat)scale {
        UIImage *greyImage = [[UIImage new] fjf_initWithData:data scale:scale];
        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<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经过编译会变成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哀掉日黑白化

        本文链接:https://www.haomeiwen.com/subject/ssljrrtx.html