// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSBubbleShapeView.h" #import "OWSBubbleView.h" #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) { // For stroking or filling. OWSBubbleShapeViewMode_Draw, OWSBubbleShapeViewMode_Shadow, OWSBubbleShapeViewMode_Clip, OWSBubbleShapeViewMode_InnerShadow, }; @interface OWSBubbleShapeView () @property (nonatomic) OWSBubbleShapeViewMode mode; @property (nonatomic) CAShapeLayer *shapeLayer; @property (nonatomic) CAShapeLayer *maskLayer; @property (nonatomic, nullable, weak) OWSBubbleView *bubbleView; @property (nonatomic) BOOL isConfigured; @end #pragma mark - @implementation OWSBubbleShapeView - (void)configure { self.opaque = NO; self.backgroundColor = [UIColor clearColor]; self.layoutMargins = UIEdgeInsetsZero; self.shapeLayer = [CAShapeLayer new]; [self.layer addSublayer:self.shapeLayer]; self.maskLayer = [CAShapeLayer new]; self.isConfigured = YES; [self updateLayers]; } - (instancetype)initDraw { self = [super initWithFrame:CGRectZero]; if (!self) { return self; } self.mode = OWSBubbleShapeViewMode_Draw; [self configure]; return self; } - (instancetype)initShadow { self = [super initWithFrame:CGRectZero]; if (!self) { return self; } self.mode = OWSBubbleShapeViewMode_Shadow; [self configure]; return self; } - (instancetype)initClip { self = [super initWithFrame:CGRectZero]; if (!self) { return self; } self.mode = OWSBubbleShapeViewMode_Clip; [self configure]; return self; } - (instancetype)initInnerShadowWithColor:(UIColor *)color radius:(CGFloat)radius opacity:(float)opacity { self = [super initWithFrame:CGRectZero]; if (!self) { return self; } self.mode = OWSBubbleShapeViewMode_InnerShadow; _innerShadowColor = color; _innerShadowRadius = radius; _innerShadowOpacity = opacity; [self configure]; return self; } - (void)setFillColor:(nullable UIColor *)fillColor { _fillColor = fillColor; [self updateLayers]; } - (void)setStrokeColor:(nullable UIColor *)strokeColor { _strokeColor = strokeColor; [self updateLayers]; } - (void)setStrokeThickness:(CGFloat)strokeThickness { _strokeThickness = strokeThickness; [self updateLayers]; } - (void)setInnerShadowColor:(nullable UIColor *)innerShadowColor { _innerShadowColor = innerShadowColor; [self updateLayers]; } - (void)setInnerShadowRadius:(CGFloat)innerShadowRadius { _innerShadowRadius = innerShadowRadius; [self updateLayers]; } - (void)setInnerShadowOpacity:(float)innerShadowOpacity { _innerShadowOpacity = innerShadowOpacity; [self updateLayers]; } - (void)setFrame:(CGRect)frame { BOOL didChange = !CGRectEqualToRect(self.frame, frame); [super setFrame:frame]; if (didChange) { [self updateLayers]; } } - (void)setBounds:(CGRect)bounds { BOOL didChange = !CGRectEqualToRect(self.bounds, bounds); [super setBounds:bounds]; if (didChange) { [self updateLayers]; } } - (void)setCenter:(CGPoint)center { [super setCenter:center]; [self updateLayers]; } - (void)setBubbleView:(nullable OWSBubbleView *)bubbleView { _bubbleView = bubbleView; [self updateLayers]; } - (void)updateLayers { if (!self.shapeLayer) { return; } if (!self.bubbleView) { return; } if (!self.isConfigured) { return; } // Prevent the layer from animating changes. [CATransaction begin]; [CATransaction setDisableActions:YES]; UIBezierPath *bezierPath = [UIBezierPath new]; // Add the bubble view's path to the local path. UIBezierPath *bubbleBezierPath = [self.bubbleView maskPath]; // We need to convert between coordinate systems using layers, not views. CGPoint bubbleOffset = [self.layer convertPoint:CGPointZero fromLayer:self.bubbleView.layer]; CGAffineTransform transform = CGAffineTransformMakeTranslation(bubbleOffset.x, bubbleOffset.y); [bubbleBezierPath applyTransform:transform]; [bezierPath appendPath:bubbleBezierPath]; switch (self.mode) { case OWSBubbleShapeViewMode_Draw: { UIBezierPath *boundsBezierPath = [UIBezierPath bezierPathWithRect:self.bounds]; [bezierPath appendPath:boundsBezierPath]; self.clipsToBounds = YES; if (self.strokeColor) { self.shapeLayer.strokeColor = self.strokeColor.CGColor; self.shapeLayer.lineWidth = self.strokeThickness; self.shapeLayer.zPosition = 100.f; } else { self.shapeLayer.strokeColor = nil; self.shapeLayer.lineWidth = 0.f; } if (self.fillColor) { self.shapeLayer.fillColor = self.fillColor.CGColor; } else { self.shapeLayer.fillColor = nil; } self.shapeLayer.path = bezierPath.CGPath; break; } case OWSBubbleShapeViewMode_Shadow: self.clipsToBounds = NO; if (self.fillColor) { self.shapeLayer.fillColor = self.fillColor.CGColor; } else { self.shapeLayer.fillColor = nil; } self.shapeLayer.path = bezierPath.CGPath; self.shapeLayer.frame = self.bounds; self.shapeLayer.masksToBounds = YES; break; case OWSBubbleShapeViewMode_Clip: self.maskLayer.path = bezierPath.CGPath; self.layer.mask = self.maskLayer; break; case OWSBubbleShapeViewMode_InnerShadow: { self.maskLayer.path = bezierPath.CGPath; self.layer.mask = self.maskLayer; // Inner shadow. // This should usually not be visible; it is used to distinguish // profile pics from the background if they are similar. self.shapeLayer.frame = self.bounds; self.shapeLayer.masksToBounds = YES; CGRect shadowBounds = self.bounds; UIBezierPath *shadowPath = [bezierPath copy]; // This can be any value large enough to cast a sufficiently large shadow. CGFloat shadowInset = -(self.innerShadowRadius * 4.f); [shadowPath appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(shadowBounds, shadowInset, shadowInset)]]; // This can be any color since the fill should be clipped. self.shapeLayer.fillColor = UIColor.blackColor.CGColor; self.shapeLayer.path = shadowPath.CGPath; self.shapeLayer.fillRule = kCAFillRuleEvenOdd; self.shapeLayer.shadowColor = self.innerShadowColor.CGColor; self.shapeLayer.shadowRadius = self.innerShadowRadius; self.shapeLayer.shadowOpacity = self.innerShadowOpacity; self.shapeLayer.shadowOffset = CGSizeZero; break; } } [CATransaction commit]; } @end NS_ASSUME_NONNULL_END