Skip to content

Instantly share code, notes, and snippets.

@matthewryan
Forked from raphaelschaad/RSPlayPauseButton.h
Created March 24, 2014 10:28
Show Gist options
  • Save matthewryan/9737807 to your computer and use it in GitHub Desktop.
Save matthewryan/9737807 to your computer and use it in GitHub Desktop.
//
// RSPlayPauseButton.h
//
// Created by Raphael Schaad on 2014-03-22.
// This is free and unencumbered software released into the public domain.
//
#import <UIKit/UIKit.h>
//
// Displays a ⃝ with either the ► (play) or ❚❚ (pause) icon and nicely morphs between the two states.
// It sends an action message to a target when tapped like `UIButton`.
//
@interface RSPlayPauseButton : UIControl
// State
@property (nonatomic, assign, getter = isPaused) BOOL paused; // Default is `YES`
// Style
@property (nonatomic, strong) UIColor *color; // Default is 90% black
@end
//
// RSPlayPauseButton.m
//
// Created by Raphael Schaad on 2014-03-22.
// This is free and unencumbered software released into the public domain.
//
#import "RSPlayPauseButton.h"
#include <tgmath.h> // type generic math, yo: http://en.wikipedia.org/wiki/Tgmath.h#tgmath.h
static const CGFloat kScale = 1.0;
static const CGFloat kBorderSize = 32.0 * kScale;
static const CGFloat kBorderWidth = 3.0 * kScale;
static const CGFloat kSize = kBorderSize + kBorderWidth; // The total size is the border size + 2x half the border width.
static const CGFloat kPauseLineWidth = 4.0 * kScale;
static const CGFloat kPauseLineHeight = 15.0 * kScale;
static const CGFloat kPauseLinesSpace = 4.0 * kScale;
static const CGFloat kPlayTriangleOffsetX = 1.0 * kScale;
static const CGFloat kPlayTriangleTipOffsetX = 2.0 * kScale;
static const CGPoint p1 = {0.0, 0.0}; // line 1, top left
static const CGPoint p2 = {kPauseLineWidth, 0.0}; // line 1, top right
static const CGPoint p3 = {kPauseLineWidth, kPauseLineHeight}; // line 1, bottom right
static const CGPoint p4 = {0.0, kPauseLineHeight}; // line 1, bottom left
static const CGPoint p5 = {kPauseLineWidth + kPauseLinesSpace, 0.0}; // line 2, top left
static const CGPoint p6 = {kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth, 0.0}; // line 2, top right
static const CGPoint p7 = {kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth, kPauseLineHeight}; // line 2, bottom right
static const CGPoint p8 = {kPauseLineWidth + kPauseLinesSpace, kPauseLineHeight}; // line 2, bottom left
@interface RSPlayPauseButton ()
@property (nonatomic, strong) CAShapeLayer *borderShapeLayer;
@property (nonatomic, strong) CAShapeLayer *playPauseShapeLayer;
@property (nonatomic, strong, readonly) UIBezierPath *pauseBezierPath;
@property (nonatomic, strong, readonly) UIBezierPath *playBezierPath;
@end
@implementation RSPlayPauseButton
#pragma mark - Accessors
@synthesize pauseBezierPath = _pauseBezierPath;
- (UIBezierPath *)pauseBezierPath
{
if (!_pauseBezierPath) {
_pauseBezierPath = [UIBezierPath bezierPath];
// Subpath for 1. line
[_pauseBezierPath moveToPoint:p1];
[_pauseBezierPath addLineToPoint:p2];
[_pauseBezierPath addLineToPoint:p3];
[_pauseBezierPath addLineToPoint:p4];
[_pauseBezierPath closePath];
// Subpath for 2. line
[_pauseBezierPath moveToPoint:p5];
[_pauseBezierPath addLineToPoint:p6];
[_pauseBezierPath addLineToPoint:p7];
[_pauseBezierPath addLineToPoint:p8];
[_pauseBezierPath closePath];
}
return _pauseBezierPath;
}
@synthesize playBezierPath = _playBezierPath;
- (UIBezierPath *)playBezierPath
{
if (!_playBezierPath) {
_playBezierPath = [UIBezierPath bezierPath];
const CGFloat kPauseLinesHalfSpace = floor(kPauseLinesSpace / 2);
const CGFloat kPauseLineHalfHeight = floor(kPauseLineHeight / 2);
CGPoint _p1 = CGPointMake(p1.x + kPlayTriangleOffsetX, p1.y);
CGPoint _p2 = CGPointMake(p2.x + kPauseLinesHalfSpace, p2.y);
CGPoint _p3 = CGPointMake(p3.x + kPauseLinesHalfSpace, p3.y);
CGPoint _p4 = CGPointMake(p4.x + kPlayTriangleOffsetX, p4.y);
CGPoint _p5 = CGPointMake(p5.x - kPauseLinesHalfSpace, p5.y);
CGPoint _p6 = CGPointMake(p6.x + kPlayTriangleTipOffsetX, p6.y);
CGPoint _p7 = CGPointMake(p7.x + kPlayTriangleTipOffsetX, p7.y);
CGPoint _p8 = CGPointMake(p8.x - kPauseLinesHalfSpace, p8.y);
const CGFloat kPlayTriangleWidth = _p6.x - _p1.x;
_p2.y += kPauseLineHalfHeight * (_p2.x - kPlayTriangleOffsetX) / kPlayTriangleWidth;
_p3.y -= kPauseLineHalfHeight * (_p3.x - kPlayTriangleOffsetX) / kPlayTriangleWidth;
_p5.y += kPauseLineHalfHeight * (_p5.x - kPlayTriangleOffsetX) / kPlayTriangleWidth;
_p6.y = kPauseLineHalfHeight;
_p7.y = kPauseLineHalfHeight;
_p8.y -= kPauseLineHalfHeight * (_p8.x - kPlayTriangleOffsetX) / kPlayTriangleWidth;
[_playBezierPath moveToPoint:_p1];
[_playBezierPath addLineToPoint:_p2];
[_playBezierPath addLineToPoint:_p3];
[_playBezierPath addLineToPoint:_p4];
[_playBezierPath closePath];
[_playBezierPath moveToPoint:_p5];
[_playBezierPath addLineToPoint:_p6];
[_playBezierPath addLineToPoint:_p7];
[_playBezierPath addLineToPoint:_p8];
[_playBezierPath closePath];
}
return _playBezierPath;
}
#pragma mark - Life Cycle
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.paused = YES;
self.color = [UIColor colorWithWhite:0.1 alpha:1.0];
[self sizeToFit];
}
return self;
}
#pragma mark - UIView Method Overrides
#pragma mark Configuring the Resizing Behavior
- (CGSize)sizeThatFits:(CGSize)size
{
CGSize sizeThatFits = [super sizeThatFits:size];
// Ignore the current size/new size by super, and instead use our default size.
sizeThatFits = CGSizeMake(kSize, kSize);
return sizeThatFits;
}
#pragma mark Laying out Subviews
- (void)layoutSubviews
{
[super layoutSubviews];
if (!self.borderShapeLayer) {
self.borderShapeLayer = [[CAShapeLayer alloc] init];
// Adjust for line width
CGRect borderRect = CGRectInset(self.bounds, ceil(kBorderWidth / 2), ceil(kBorderWidth / 2));
self.borderShapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:borderRect].CGPath;
self.borderShapeLayer.lineWidth = kBorderWidth;
self.borderShapeLayer.strokeColor = self.color.CGColor;
self.borderShapeLayer.fillColor = [UIColor clearColor].CGColor;
[self.layer addSublayer:self.borderShapeLayer];
}
if (!self.playPauseShapeLayer) {
self.playPauseShapeLayer = [[CAShapeLayer alloc] init];
CGRect playPauseRect = CGRectZero;
playPauseRect.origin.x = floor(((self.bounds.size.width) - (kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth)) / 2);
playPauseRect.origin.y = floor(((self.bounds.size.height) - (kPauseLineHeight)) / 2);
playPauseRect.size.width = kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth + kPlayTriangleTipOffsetX;
playPauseRect.size.height = kPauseLineHeight;
self.playPauseShapeLayer.frame = playPauseRect;
self.playPauseShapeLayer.fillColor = self.color.CGColor;
UIBezierPath *path = self.isPaused ? self.playBezierPath : self.pauseBezierPath;
self.playPauseShapeLayer.path = path.CGPath;
[self.layer addSublayer:self.playPauseShapeLayer];
}
}
#pragma mark - UIControl Method Overrides
#pragma mark Preparing and Sending Action Messages
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
// Update the state
self.paused = !self.isPaused;
// Morph between the two states
UIBezierPath *fromPath = self.isPaused ? self.pauseBezierPath : self.playBezierPath;
UIBezierPath *toPath = self.isPaused ? self.playBezierPath : self.pauseBezierPath;
CABasicAnimation *morphAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
CAMediaTimingFunction *timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[morphAnimation setTimingFunction:timingFunction];
// Make the new state stick
[morphAnimation setRemovedOnCompletion:NO];
[morphAnimation setFillMode:kCAFillModeForwards];
morphAnimation.duration = 0.3;
morphAnimation.fromValue = (__bridge id)fromPath.CGPath;
morphAnimation.toValue = (__bridge id)toPath.CGPath;
[self.playPauseShapeLayer addAnimation:morphAnimation forKey:nil];
// Forward the action
[super sendAction:action to:target forEvent:event];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment