UIVisualEffectView 背后的实现

iOS 8 苹果为我们带来了原生的毛玻璃效果的支持,即 UIVisualEffectView

UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];  

但它提供的 API 非常有限,能改的样式属性只有两个 effect 以及不多的几个 style,模糊效果也基本是非黑即白,模糊程度也不可调节。但有时候不关心实现的设计师们会要求某些地方模糊小一点之类的,此时一般就换自己用高斯模糊来做了,这里问题就来了,真的做不了吗?

我们可以通过一些视图审查工具(如:Lookin)发现 UIVisualEffectView 背后其实由三个视图构成:

  • _UIVisualEffectBackdropView
  • _UIVisualEffectSubview
  • _UIVisualEffectContentView

其中,_UIVisualEffectBackdropView 是真正产生模糊效果的地方,_UIVisualEffectSubview 是调节黑白的地方。而 _UIVisualEffectBackdropViewlayerClass 为:

@interface UICABackdropLayer : CABackdropLayer
@end

它做的事情其实也很简单,就是将它下面被它拦住的视图内容复制一份。真正的模糊效果是由之前一篇文章提到的 CALayer 的 filters 做的。

了解了原理之后,我们可以自定义一个类似 UIVisualEffectView 的视图,并且可以调节模糊程度,效果如下图:

IB_DESIGNABLE  
@interface RTBackdropView : UIView
@property (nonatomic) IBInspectable CGFloat blurRadius;
@property (nonatomic) IBInspectable CGFloat saturation;
@end

@implementation RTBackdropView

+ (Class)layerClass
{
    return NSClassFromString(@"CABackdropLayer");
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        self.blurRadius = 30;
        self.saturation = 2;
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.blurRadius = 30;
        self.saturation = 2;
    }
    return self;
}

- (void)setBlurRadius:(CGFloat)blurRadius
{
    if (_blurRadius != blurRadius) {
        _blurRadius = blurRadius;
        [self _updateFilters];
    }
}

- (void)setSaturation:(CGFloat)saturation
{
    if (_saturation != saturation) {
        _saturation = saturation;
        [self _updateFilters];
    }
}

- (void)_updateFilters {
    self.layer.filters = @[
        ({
            CIFilter *sat = [NSClassFromString(@"CAFilter") filterWithName:@"colorSaturate"];
            [sat setValue:@(self.saturation) forKey:@"inputAmount"];
            [sat setValue:@YES forKey:@"inputNormalizeEdges"];
            sat;
        }),
        ({
            CIFilter *blur = [NSClassFromString(@"CAFilter") filterWithName:@"gaussianBlur"];
            blur.name = @"blur";    // 注意这个名字,后面有用!
            [blur setValue:@(self.blurRadius) forKey:@"inputRadius"];
            blur;
        }),
    ];
}

@end

这样就完了吗?我们还可以加动画呢!

    // 下面的 .blur 是 filter 的 name
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"filters.blur.inputRadius"];
    animation.fromValue = @0;
    animation.toValue = @20;
    animation.duration = 0.5;
    animation.autoreverses = YES;
    animation.removedOnCompletion = NO;
    animation.repeatCount = FLT_MAX;
    [view.layer addAnimation:animation forKey:@"Blur"];