Skip to content

Instantly share code, notes, and snippets.

@conradev
Created March 4, 2016 03:26
Show Gist options
  • Save conradev/2d6c02c3792b8a61d29d to your computer and use it in GitHub Desktop.
Save conradev/2d6c02c3792b8a61d29d to your computer and use it in GitHub Desktop.
Vibrant buttons in Workflow
Copyright (c) 2014-2016 DeskConnect, Inc. (http://deskconnect.com/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
//
// WFVibrantButton.h
// WorkflowUI
//
// Created by Conrad Kramer on 7/28/14.
// Copyright (c) 2014 DeskConnect. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface WFVibrantButton : UIControl
@property (nonatomic, weak, readonly) UILabel *titleLabel;
@property (nonatomic, assign, getter=isAnimating) BOOL animating;
- (instancetype)initWithBlurEffect:(nullable UIBlurEffect *)effect;
@end
NS_ASSUME_NONNULL_END
//
// WFVibrantButton.m
// WorkflowUI
//
// Created by Conrad Kramer on 7/28/14.
// Copyright (c) 2014 DeskConnect. All rights reserved.
//
#import "WFVibrantButton.h"
#import <CoreText/CoreText.h>
NS_ASSUME_NONNULL_BEGIN
@interface WFVibrantButton ()
@property (nonatomic, readonly, weak) UIImageView *outlineView;
@property (nonatomic, readonly, strong) CAShapeLayer *outlineLayer;
@property (nonatomic, copy) UIBezierPath *outlinePath;
@property (nonatomic, copy) UIBezierPath *animatingPath;
@end
static CGFloat const WFVibrantButtonMargin = 6.0f;
static CGFloat const WFVibrantButtonCornerRadius = 4.0f;
static NSString * const WFVibrantButtonAnimatingKey = @"WFVibrantButtonAnimating";
static NSString * const WFVibrantButtonClipKey = @"WFVibrantButtonClip";
@implementation WFVibrantButton
- (instancetype)initWithBlurEffect:(nullable UIBlurEffect *)effect {
self = [super initWithFrame:CGRectZero];
if (!self)
return nil;
self.backgroundColor = [UIColor clearColor];
UIView *contentView = self;
if (effect) {
UIVisualEffectView *vibrantView = [[UIVisualEffectView alloc] initWithEffect:[UIVibrancyEffect effectForBlurEffect:effect]];
vibrantView.userInteractionEnabled = NO;
vibrantView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
vibrantView.frame = self.bounds;
[self addSubview:vibrantView];
contentView = vibrantView.contentView;
}
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.userInteractionEnabled = NO;
titleLabel.font = [UIFont boldSystemFontOfSize:14.0f];
[contentView addSubview:titleLabel];
_titleLabel = titleLabel;
CAShapeLayer *outlineLayer = [CAShapeLayer layer];
outlineLayer.lineWidth = 1.0f;
outlineLayer.strokeColor = [[UIColor blackColor] CGColor];
outlineLayer.fillColor = [[UIColor clearColor] CGColor];
_outlineLayer = outlineLayer;
UIImageView *outlineView = [[UIImageView alloc] init];
outlineView.userInteractionEnabled = NO;
outlineView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
outlineView.frame = self.bounds;
outlineView.layer.mask = outlineLayer;
[contentView insertSubview:outlineView belowSubview:titleLabel];
_outlineView = outlineView;
[self tintColorDidChange];
return self;
}
- (void)tintColorDidChange {
[super tintColorDidChange];
CGRect bounds = (CGRect){CGPointZero, CGSizeMake(1, 1)};
UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0.0f);
[self.tintColor setFill];
[[UIBezierPath bezierPathWithRect:bounds] fill];
self.outlineView.image = [UIGraphicsGetImageFromCurrentImageContext() imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIGraphicsEndImageContext();
self.titleLabel.textColor = self.tintColor;
}
- (void)setHighlighted:(BOOL)highlighted {
BOOL update = (self.highlighted != highlighted);
[super setHighlighted:highlighted];
if (update) {
if (highlighted) {
CGRect bounds = self.outlineView.layer.bounds;
UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, self.outlineLayer.path);
CGContextDrawPath(context, kCGPathFillStroke);
CGContextSetBlendMode(context, kCGBlendModeClear);
[self.titleLabel.attributedText drawInRect:self.titleLabel.frame];
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = bounds;
maskLayer.contents = (__bridge id)CGBitmapContextCreateImage(context);
self.outlineView.layer.mask = maskLayer;
self.titleLabel.hidden = YES;
UIGraphicsEndImageContext();
} else {
self.outlineView.layer.mask = self.outlineLayer;
self.titleLabel.hidden = NO;
}
}
self.outlineLayer.fillColor = (highlighted ? [[UIColor blackColor] CGColor] : [[UIColor clearColor] CGColor]);
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, UIEdgeInsetsMake(1, 1, 1, 1));
CGFloat diameter = CGRectGetHeight(bounds);
CGRect animatingRect = CGRectMake(CGRectGetWidth(bounds) - diameter, CGRectGetMinY(bounds), diameter, diameter);
CGPoint center = CGPointMake(CGRectGetMidX(animatingRect), CGRectGetMidY(animatingRect));
self.animatingPath = [UIBezierPath bezierPathWithRoundedRect:animatingRect cornerRadius:(diameter / 2.0f)];
self.outlinePath = [UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:WFVibrantButtonCornerRadius];
self.outlineLayer.anchorPoint = CGPointMake(center.x / CGRectGetWidth(self.bounds), center.y / CGRectGetHeight(self.bounds));
self.outlineLayer.frame = self.bounds;
self.outlineLayer.path = (self.animating ? [self.animatingPath CGPath] : [self.outlinePath CGPath]);
[self.titleLabel sizeToFit];
self.titleLabel.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
}
- (CGSize)intrinsicContentSize {
CGSize size = self.titleLabel.intrinsicContentSize;
size.height += 2.0f * WFVibrantButtonMargin;
size.width += 4.0f * WFVibrantButtonMargin;
return size;
}
- (void)setAnimating:(BOOL)animating {
if ([self.outlineLayer animationForKey:WFVibrantButtonClipKey])
return;
_animating = animating;
[self tintColorDidChange];
CABasicAnimation *shrinkAnimation = [CABasicAnimation animationWithKeyPath:NSStringFromSelector(@selector(path))];
shrinkAnimation.toValue = (__bridge id)(animating ? [self.animatingPath CGPath] : [self.outlinePath CGPath]);
shrinkAnimation.delegate = self;
shrinkAnimation.duration = 0.25f;
shrinkAnimation.fillMode = kCAFillModeForwards;
shrinkAnimation.removedOnCompletion = NO;
[self.outlineLayer addAnimation:shrinkAnimation forKey:WFVibrantButtonAnimatingKey];
CABasicAnimation *clipAnimation = [CABasicAnimation animationWithKeyPath:NSStringFromSelector(@selector(strokeStart))];
clipAnimation.toValue = @(animating ? 0.075f : 0.0f);
clipAnimation.delegate = self;
clipAnimation.duration = shrinkAnimation.duration;
clipAnimation.fillMode = kCAFillModeForwards;
clipAnimation.removedOnCompletion = NO;
[self.outlineLayer addAnimation:clipAnimation forKey:WFVibrantButtonClipKey];
[UIView animateWithDuration:clipAnimation.duration animations:^{
self.titleLabel.alpha = (animating ? 0.0f : 1.0f);
}];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if ([anim isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *basicAnimation = (CABasicAnimation *)anim;
if ([basicAnimation.keyPath isEqualToString:NSStringFromSelector(@selector(path))]) {
[self.outlineLayer setValue:basicAnimation.toValue forKey:basicAnimation.keyPath];
if (self.animating) {
CABasicAnimation *spinAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
spinAnimation.fromValue = @0.0f;
spinAnimation.toValue = @(2*M_PI);
spinAnimation.duration = 1.0f;
spinAnimation.repeatCount = HUGE_VALF;
[self.outlineLayer addAnimation:spinAnimation forKey:WFVibrantButtonAnimatingKey];
} else {
[self.outlineLayer removeAnimationForKey:WFVibrantButtonAnimatingKey];
}
} else if ([basicAnimation.keyPath isEqualToString:NSStringFromSelector(@selector(strokeStart))]) {
[self.outlineLayer setValue:basicAnimation.toValue forKey:basicAnimation.keyPath];
[self.outlineLayer removeAnimationForKey:WFVibrantButtonClipKey];
}
}
}
#pragma mark - Accessibility
- (BOOL)isAccessibilityElement {
return YES;
}
- (UIAccessibilityTraits)accessibilityTraits {
return (UIAccessibilityTraitButton | (self.enabled ? 0 : UIAccessibilityTraitNotEnabled));
}
- (nullable NSString *)accessibilityLabel {
return self.titleLabel.text;
}
@end
NS_ASSUME_NONNULL_END
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment