//
|
// ASValuePopUpView.m
|
// ValueTrackingSlider
|
//
|
// Created by Alan Skipp on 27/03/2014.
|
// Copyright (c) 2014 Alan Skipp. All rights reserved.
|
//
|
|
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// This UIView subclass is used internally by ASValueTrackingSlider
|
// The public API is declared in ASValueTrackingSlider.h
|
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
#import "ASValuePopUpView.h"
|
|
const float ARROW_LENGTH = 13.0;
|
const float MIN_POPUPVIEW_WIDTH = 45.0;
|
const float MIN_POPUPVIEW_HEIGHT = 27.0;
|
const float POPUPVIEW_WIDTH_PAD = 1.15;
|
const float POPUPVIEW_HEIGHT_PAD = 1.1;
|
|
NSString *const FillColorAnimation = @"fillColor";
|
|
@implementation ASValuePopUpView
|
{
|
NSMutableAttributedString *_attributedString;
|
CAShapeLayer *_backgroundLayer;
|
CATextLayer *_textLayer;
|
CGSize _oldSize;
|
CGFloat _arrowCenterOffset;
|
}
|
|
#pragma mark - public
|
|
- (id)initWithFrame:(CGRect)frame
|
{
|
self = [super initWithFrame:frame];
|
if (self) {
|
self.layer.anchorPoint = CGPointMake(0.5, 1);
|
|
self.userInteractionEnabled = NO;
|
_backgroundLayer = [CAShapeLayer layer];
|
_backgroundLayer.anchorPoint = CGPointMake(0, 0);
|
|
_textLayer = [CATextLayer layer];
|
_textLayer.alignmentMode = kCAAlignmentCenter;
|
_textLayer.anchorPoint = CGPointMake(0, 0);
|
_textLayer.contentsScale = [UIScreen mainScreen].scale;
|
_textLayer.actions = @{@"bounds" : [NSNull null], // prevent implicit animation of bounds
|
@"position" : [NSNull null]};// and position
|
|
[self.layer addSublayer:_backgroundLayer];
|
[self.layer addSublayer:_textLayer];
|
|
_attributedString = [[NSMutableAttributedString alloc] initWithString:@" " attributes:nil];
|
}
|
return self;
|
}
|
|
- (void)setCornerRadius:(CGFloat)radius
|
{
|
if (_cornerRadius == radius) return;
|
_cornerRadius = radius;
|
[self drawPath];
|
}
|
|
- (UIColor *)color
|
{
|
return [UIColor colorWithCGColor:[_backgroundLayer.presentationLayer fillColor]];
|
}
|
|
- (void)setColor:(UIColor *)color
|
{
|
[_backgroundLayer removeAnimationForKey:FillColorAnimation];
|
_backgroundLayer.fillColor = color.CGColor;
|
}
|
|
- (UIColor *)opaqueColor
|
{
|
return opaqueUIColorFromCGColor([_backgroundLayer.presentationLayer fillColor] ?: _backgroundLayer.fillColor);
|
}
|
|
- (void)setTextColor:(UIColor *)color
|
{
|
[_attributedString addAttribute:NSForegroundColorAttributeName
|
value:(id)color.CGColor
|
range:NSMakeRange(0, [_attributedString length])];
|
}
|
|
- (void)setFont:(UIFont *)font
|
{
|
[_attributedString addAttribute:NSFontAttributeName
|
value:font
|
range:NSMakeRange(0, [_attributedString length])];
|
}
|
|
- (void)setString:(NSString *)string
|
{
|
[[_attributedString mutableString] setString:string];
|
_textLayer.string = _attributedString;
|
LOG_INFO(@"当前数值:%@",_attributedString);
|
}
|
|
// set up an animation, but prevent it from running automatically
|
// the animation progress will be adjusted manually
|
- (void)setAnimatedColors:(NSArray *)animatedColors withKeyTimes:(NSArray *)keyTimes
|
{
|
NSMutableArray *cgColors = [NSMutableArray array];
|
for (UIColor *col in animatedColors) {
|
[cgColors addObject:(id)col.CGColor];
|
}
|
|
CAKeyframeAnimation *colorAnim = [CAKeyframeAnimation animationWithKeyPath:FillColorAnimation];
|
colorAnim.keyTimes = keyTimes;
|
colorAnim.values = cgColors;
|
colorAnim.fillMode = kCAFillModeBoth;
|
colorAnim.duration = 1.0;
|
colorAnim.delegate = self;
|
|
// As the interpolated color values from the presentationLayer are needed immediately
|
// the animation must be allowed to start to initialize _backgroundLayer's presentationLayer
|
// hence the speed is set to min value - then set to zero in 'animationDidStart:' delegate method
|
_backgroundLayer.speed = FLT_MIN;
|
_backgroundLayer.timeOffset = 0.0;
|
|
[_backgroundLayer addAnimation:colorAnim forKey:FillColorAnimation];
|
}
|
|
- (void)setAnimationOffset:(CGFloat)offset
|
{
|
_backgroundLayer.timeOffset = offset;
|
}
|
|
- (void)setArrowCenterOffset:(CGFloat)offset
|
{
|
if (_arrowCenterOffset == offset) return; // only redraw if the offset has changed
|
_arrowCenterOffset = offset;
|
|
// the arrow tip should be the origin of any scale animations
|
// to achieve this, position the anchorPoint at the tip of the arrow
|
CGRect f = self.layer.frame;
|
self.layer.anchorPoint = CGPointMake(0.5+(offset/self.bounds.size.width), 1);
|
self.layer.frame = f; // changing anchor repositions layer, so must reset frame afterwards
|
[self drawPath];
|
}
|
|
- (CGSize)popUpSizeForString:(NSString *)string
|
{
|
[[_attributedString mutableString] setString:string];
|
CGFloat w, h;
|
w = ceilf(MAX([_attributedString size].width, MIN_POPUPVIEW_WIDTH) * POPUPVIEW_WIDTH_PAD);
|
h = ceilf(MAX([_attributedString size].height, MIN_POPUPVIEW_HEIGHT) * POPUPVIEW_HEIGHT_PAD + ARROW_LENGTH);
|
return CGSizeMake(w, h);
|
}
|
|
- (void)show
|
{
|
[CATransaction begin]; {
|
// start the transform animation from its current value if it's already running
|
NSValue *fromValue = [self.layer animationForKey:@"transform"] ? [self.layer.presentationLayer valueForKey:@"transform"] : [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.5, 0.5, 1)];
|
|
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
|
scaleAnim.fromValue = fromValue;
|
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
|
[scaleAnim setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.8 :2.5 :0.35 :0.5]];
|
scaleAnim.removedOnCompletion = NO;
|
scaleAnim.fillMode = kCAFillModeForwards;
|
scaleAnim.duration = 0.4;
|
[self.layer addAnimation:scaleAnim forKey:@"transform"];
|
|
CABasicAnimation* fadeInAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
|
fadeInAnim.fromValue = [self.layer.presentationLayer valueForKey:@"opacity"];
|
fadeInAnim.duration = 0.1;
|
fadeInAnim.toValue = @1.0;
|
[self.layer addAnimation:fadeInAnim forKey:@"opacity"];
|
|
self.layer.opacity = 1.0;
|
} [CATransaction commit];
|
}
|
|
- (void)hide
|
{
|
[CATransaction begin]; {
|
[CATransaction setCompletionBlock:^{
|
// remove the transform animation if the animation finished and wasn't interrupted
|
if (self.layer.opacity == 0.0) [self.layer removeAnimationForKey:@"transform"];
|
[self.delegate popUpViewDidHide];
|
}];
|
|
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
|
scaleAnim.fromValue = [self.layer.presentationLayer valueForKey:@"transform"];
|
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.5, 0.5, 1)];
|
scaleAnim.duration = 0.6;
|
scaleAnim.removedOnCompletion = NO;
|
scaleAnim.fillMode = kCAFillModeForwards;
|
[scaleAnim setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.1 :-2 :0.3 :3]];
|
[self.layer addAnimation:scaleAnim forKey:@"transform"];
|
|
CABasicAnimation* fadeOutAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
|
fadeOutAnim.fromValue = [self.layer.presentationLayer valueForKey:@"opacity"];
|
fadeOutAnim.toValue = @0.0;
|
fadeOutAnim.duration = 0.8;
|
[self.layer addAnimation:fadeOutAnim forKey:@"opacity"];
|
self.layer.opacity = 0.0;
|
} [CATransaction commit];
|
}
|
|
#pragma mark - CAAnimation delegate
|
|
// set the speed to zero to freeze the animation and set the offset to the correct value
|
// the animation can now be updated manually by explicity setting its 'timeOffset'
|
- (void)animationDidStart:(CAAnimation *)animation
|
{
|
_backgroundLayer.speed = 0.0;
|
_backgroundLayer.timeOffset = [self.delegate currentValueOffset];
|
[self.delegate colorAnimationDidStart];
|
}
|
|
#pragma mark - private
|
|
- (void)drawPath
|
{
|
// Create rounded rect
|
CGRect roundedRect = self.bounds;
|
roundedRect.size.height -= ARROW_LENGTH;
|
UIBezierPath *roundedRectPath = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:_cornerRadius];
|
|
// Create arrow path
|
CGFloat maxX = CGRectGetMaxX(roundedRect); // prevent arrow from extending beyond this point
|
CGFloat arrowTipX = CGRectGetMidX(self.bounds) + _arrowCenterOffset;
|
CGPoint tip = CGPointMake(arrowTipX, CGRectGetMaxY(self.bounds));
|
|
CGFloat arrowLength = CGRectGetHeight(roundedRect)/2.0;
|
CGFloat x = arrowLength * tan(45.0 * M_PI/180); // x = half the length of the base of the arrow
|
|
UIBezierPath *arrowPath = [UIBezierPath bezierPath];
|
[arrowPath moveToPoint:tip];
|
[arrowPath addLineToPoint:CGPointMake(MAX(arrowTipX - x, 0), CGRectGetMaxY(roundedRect) - arrowLength)];
|
[arrowPath addLineToPoint:CGPointMake(MIN(arrowTipX + x, maxX), CGRectGetMaxY(roundedRect) - arrowLength)];
|
[arrowPath closePath];
|
|
// combine arrow path and rounded rect
|
[roundedRectPath appendPath:arrowPath];
|
|
_backgroundLayer.path = roundedRectPath.CGPath;
|
}
|
|
- (void)layoutSubviews
|
{
|
[super layoutSubviews];
|
|
if (CGSizeEqualToSize(self.bounds.size, _oldSize)) return; // return if view size hasn't changed
|
|
_oldSize = self.bounds.size;
|
_backgroundLayer.bounds = self.bounds;
|
|
CGFloat textHeight = 25/2;//[_textLayer.string frame].size.height;
|
CGRect textRect = CGRectMake(self.bounds.origin.x,
|
(self.bounds.size.height-ARROW_LENGTH-textHeight)/2,
|
self.bounds.size.width, textHeight);
|
_textLayer.frame = CGRectIntegral(textRect);
|
[self drawPath];
|
}
|
|
static UIColor* opaqueUIColorFromCGColor(CGColorRef col)
|
{
|
const CGFloat *components = CGColorGetComponents(col);
|
UIColor *color;
|
if (CGColorGetNumberOfComponents(col) == 2) {
|
color = [UIColor colorWithWhite:components[0] alpha:1.0];
|
} else {
|
color = [UIColor colorWithRed:components[0] green:components[1] blue:components[2] alpha:1.0];
|
}
|
return color;
|
}
|
|
|
|
@end
|