blob: bc9cf647d2dc3bd7a5163e3705228a2558bd97f8 [file] [log] [blame] [edit]
// Copyright 2015-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCNumericValueLabel.h"
static const CGFloat kAnchorPointY = (CGFloat)1.0;
static const CGFloat kBezierSmoothingFactor = (CGFloat)0.0625;
static const CGFloat kLabelInsetSize = 6;
@implementation MDCNumericValueLabel {
CAShapeLayer *_marker;
UILabel *_label;
}
/**
Inits a new numeric value label. Note that we expect the frame to be taller than it is wide, in
order to correctly display the "ice cream cone" shape.
*/
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
_marker = [CAShapeLayer layer];
_marker.fillRule = kCAFillRuleNonZero;
[self.layer addSublayer:_marker];
_label = [[UILabel alloc] init];
_label.textAlignment = NSTextAlignmentCenter;
_label.textColor = [UIColor whiteColor]; // Default text color, override by setting textColor
_label.adjustsFontSizeToFitWidth = YES;
_label.minimumScaleFactor = (CGFloat)0.7;
[self addSubview:_label];
// So that scaling happens in relation to slightly below the thumb track. Also has the nice
// effect of letting us set the view's "center" to be on the track, but have the view actually
// appear above the thumb track.
self.layer.anchorPoint = CGPointMake((CGFloat)0.5, kAnchorPointY);
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.bounds.size.width;
CGFloat radius = width / 2;
CGFloat height = self.bounds.size.height;
CGFloat bezierSmoothingPixels = height * kBezierSmoothingFactor;
CGMutablePathRef path = CGPathCreateMutable();
// We're drawing a shape that looks something like this:
// __
// ( )
// \/
// Calculate what point on the circle the lines on the bottom should touch. If you're interested
// in the Math, we found these formulas like this:
// 1. x^2 + y^2 = r defines the circle
// 2. y = H - mx defines a family of lines from (0,H) where H is the height of the whole shape
// 3. Now we try to find m such that the system of equations has only one (x,y) solution that
// satisfies both equations. This amounts to finding the line from (0,H) to the circle that
// only touches the circle one time.
// 4. Substitute y = H - mx into first equation. x^2 + (H-mx)^2 = r^2
// 5. Simplify and set to 0. (1 + m^2)x^2 + (-2Hm)x + (H^2 - r^2) = 0
// 6. We now have a quadratic equation of the form ax^2 + bx + c = 0. Such equations have only
// one solution if and only if the discriminant d = b^2 - 4ac = 0
// 7. Set discriminant to 0 and solve for m. 0 = (-2Hm)^2 - 4(1 + m^2)(H^2 - r^2)
// 8. m = sqrt((H^2 - r^2)/(r^2))
// 9. Now use quadratic formula to solve for x. x = (r * sqrt(H^2 - r^2)) / H
// 10. Plug into original equation to get y. y = r^2 / H
CGFloat x = (radius * sqrtf((float)(height * height - radius * radius))) / height;
CGFloat y = radius * radius / height;
// Calculate the angles at which the left and right lines touch the circle
CGFloat angleDelta = atanf((float)(y / x));
CGFloat startAngle = (float)M_PI - angleDelta;
CGFloat endAngle = angleDelta;
CGPathMoveToPoint(path, NULL, radius, height);
// Draws line from bottom to left side of circle, curving slightly to smooth the shape
CGPathAddCurveToPoint(path, NULL, radius, height, radius - x, radius + y + bezierSmoothingPixels,
radius - x, radius + y);
// Draw the part of the circle that we need
CGPathAddArc(path, NULL, radius, radius, radius, startAngle, endAngle, NO);
// Curve back down from the right side of the circle to the bottom of the shape
CGPathAddCurveToPoint(path, NULL, radius + x, radius + y + bezierSmoothingPixels, radius, height,
radius, height);
CGPathCloseSubpath(path);
_marker.path = path;
CGPathRelease(path);
// Place the label as well
_label.frame = CGRectInset(CGRectMake(0, 0, width, width), kLabelInsetSize, kLabelInsetSize);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGSize labelSize = [_label sizeThatFits:size];
return CGSizeMake(labelSize.width + kLabelInsetSize, labelSize.height + kLabelInsetSize);
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
_backgroundColor = backgroundColor;
_marker.fillColor = _backgroundColor.CGColor;
}
- (UIColor *)textColor {
return _label.textColor;
}
- (void)setTextColor:(UIColor *)textColor {
_label.textColor = textColor;
}
- (CGFloat)fontSize {
return _label.font.pointSize;
}
- (NSString *)text {
return _label.text;
}
- (void)setText:(NSString *)text {
_label.text = text;
}
- (UIFont *)font {
return _label.font;
}
- (void)setFont:(UIFont *)font {
_label.font = font;
}
- (BOOL)adjustsFontForContentSizeCategory {
return _label.adjustsFontForContentSizeCategory;
}
- (void)setAdjustsFontForContentSizeCategory:(BOOL)adjustsFontForContentSizeCategory {
_label.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory;
}
@end