You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
419 lines
12 KiB
419 lines
12 KiB
// |
|
// CBAutoScrollLabel.m |
|
// CBAutoScrollLabel |
|
// |
|
// Created by Brian Stormont on 10/21/09. |
|
// Updated by Christopher Bess on 2/5/12 |
|
// |
|
// Copyright 2009 Stormy Productions. |
|
// |
|
// Permission is granted to use this code free of charge for any project. |
|
// |
|
|
|
#import "CBAutoScrollLabel.h" |
|
#import <QuartzCore/QuartzCore.h> |
|
|
|
#define kLabelCount 2 |
|
#define kDefaultFadeLength 7.f |
|
// pixel buffer space between scrolling label |
|
#define kDefaultLabelBufferSpace 20 |
|
#define kDefaultPixelsPerSecond 30 |
|
#define kDefaultPauseTime 1.5f |
|
|
|
// shortcut method for NSArray iterations |
|
static void each_object(NSArray *objects, void (^block)(id object)) { |
|
for (id obj in objects) { |
|
block(obj); |
|
} |
|
} |
|
|
|
// shortcut to change each label attribute value |
|
#define EACH_LABEL(ATTR, VALUE) each_object(self.labels, ^(UILabel *label) { label.ATTR = VALUE; }); |
|
|
|
@interface CBAutoScrollLabel () |
|
|
|
@property (nonatomic, strong) NSArray *labels; |
|
@property (nonatomic, strong, readonly) UILabel *mainLabel; |
|
@property (nonatomic, strong) UIScrollView *scrollView; |
|
|
|
@end |
|
|
|
@implementation CBAutoScrollLabel |
|
|
|
- (id)initWithCoder:(NSCoder *)aDecoder { |
|
if ((self = [super initWithCoder:aDecoder])) { |
|
[self commonInit]; |
|
} |
|
return self; |
|
} |
|
|
|
- (id)initWithFrame:(CGRect)frame { |
|
if ((self = [super initWithFrame:frame])) { |
|
[self commonInit]; |
|
} |
|
return self; |
|
} |
|
|
|
- (void)commonInit { |
|
// create the labels |
|
NSMutableSet *labelSet = [[NSMutableSet alloc] initWithCapacity:kLabelCount]; |
|
|
|
for (int index = 0; index < kLabelCount; ++index) { |
|
UILabel *label = [[UILabel alloc] init]; |
|
label.backgroundColor = [UIColor clearColor]; |
|
label.autoresizingMask = self.autoresizingMask; |
|
|
|
// store labels |
|
[self.scrollView addSubview:label]; |
|
[labelSet addObject:label]; |
|
} |
|
|
|
self.labels = [labelSet.allObjects copy]; |
|
|
|
// default values |
|
_scrollDirection = CBAutoScrollDirectionLeft; |
|
_scrollSpeed = kDefaultPixelsPerSecond; |
|
self.pauseInterval = kDefaultPauseTime; |
|
self.labelSpacing = kDefaultLabelBufferSpace; |
|
self.textAlignment = NSTextAlignmentLeft; |
|
self.animationOptions = UIViewAnimationOptionCurveLinear; |
|
self.scrollView.showsVerticalScrollIndicator = NO; |
|
self.scrollView.showsHorizontalScrollIndicator = NO; |
|
self.scrollView.scrollEnabled = NO; |
|
self.userInteractionEnabled = NO; |
|
self.backgroundColor = [UIColor clearColor]; |
|
self.clipsToBounds = YES; |
|
self.fadeLength = kDefaultFadeLength; |
|
} |
|
|
|
- (void)dealloc { |
|
[NSObject cancelPreviousPerformRequestsWithTarget:self]; |
|
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
|
} |
|
|
|
- (void)setFrame:(CGRect)frame { |
|
[super setFrame:frame]; |
|
|
|
[self didChangeFrame]; |
|
} |
|
|
|
// For autolayout |
|
- (void)setBounds:(CGRect)bounds { |
|
[super setBounds:bounds]; |
|
|
|
[self didChangeFrame]; |
|
} |
|
|
|
- (void)didMoveToWindow { |
|
[super didMoveToWindow]; |
|
|
|
if (self.window) { |
|
[self scrollLabelIfNeeded]; |
|
} |
|
} |
|
|
|
#pragma mark - Properties |
|
|
|
- (UIScrollView *)scrollView { |
|
if (_scrollView == nil) { |
|
_scrollView = [[UIScrollView alloc] initWithFrame:self.bounds]; |
|
_scrollView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); |
|
_scrollView.backgroundColor = [UIColor clearColor]; |
|
|
|
[self addSubview:_scrollView]; |
|
} |
|
return _scrollView; |
|
} |
|
|
|
- (void)setFadeLength:(CGFloat)fadeLength { |
|
if (_fadeLength != fadeLength) { |
|
_fadeLength = fadeLength; |
|
|
|
[self refreshLabels]; |
|
[self applyGradientMaskForFadeLength:fadeLength enableFade:NO]; |
|
} |
|
} |
|
|
|
- (UILabel *)mainLabel { |
|
return self.labels[0]; |
|
} |
|
|
|
- (void)setText:(NSString *)theText { |
|
[self setText:theText refreshLabels:YES]; |
|
} |
|
|
|
- (void)setText:(NSString *)theText refreshLabels:(BOOL)refresh { |
|
// ignore identical text changes |
|
if ([theText isEqualToString:self.text]) |
|
return; |
|
|
|
EACH_LABEL(text, theText) |
|
|
|
if (refresh) |
|
[self refreshLabels]; |
|
} |
|
|
|
- (NSString *)text { |
|
return self.mainLabel.text; |
|
} |
|
|
|
- (void)setAttributedText:(NSAttributedString *)theText { |
|
[self setAttributedText:theText refreshLabels:YES]; |
|
} |
|
|
|
- (void)setAttributedText:(NSAttributedString *)theText refreshLabels:(BOOL)refresh { |
|
// ignore identical text changes |
|
if ([theText.string isEqualToString:self.attributedText.string]) |
|
return; |
|
|
|
EACH_LABEL(attributedText, theText) |
|
|
|
if (refresh) |
|
[self refreshLabels]; |
|
} |
|
|
|
- (NSAttributedString *)attributedText { |
|
return self.mainLabel.attributedText; |
|
} |
|
|
|
- (void)setTextColor:(UIColor *)color { |
|
EACH_LABEL(textColor, color) |
|
} |
|
|
|
- (UIColor *)textColor { |
|
return self.mainLabel.textColor; |
|
} |
|
|
|
- (void)setFont:(UIFont *)font { |
|
if (self.mainLabel.font == font) |
|
return; |
|
|
|
EACH_LABEL(font, font) |
|
|
|
[self refreshLabels]; |
|
[self invalidateIntrinsicContentSize]; |
|
} |
|
|
|
- (UIFont *)font { |
|
return self.mainLabel.font; |
|
} |
|
|
|
- (void)setScrollSpeed:(float)speed { |
|
_scrollSpeed = speed; |
|
|
|
[self scrollLabelIfNeeded]; |
|
} |
|
|
|
- (void)setScrollDirection:(CBAutoScrollDirection)direction { |
|
_scrollDirection = direction; |
|
|
|
[self scrollLabelIfNeeded]; |
|
} |
|
|
|
- (void)setShadowColor:(UIColor *)color { |
|
EACH_LABEL(shadowColor, color) |
|
} |
|
|
|
- (UIColor *)shadowColor { |
|
return self.mainLabel.shadowColor; |
|
} |
|
|
|
- (void)setShadowOffset:(CGSize)offset { |
|
EACH_LABEL(shadowOffset, offset) |
|
} |
|
|
|
- (CGSize)shadowOffset { |
|
return self.mainLabel.shadowOffset; |
|
} |
|
|
|
#pragma mark - Autolayout |
|
|
|
- (CGSize)intrinsicContentSize { |
|
return CGSizeMake(0, [self.mainLabel intrinsicContentSize].height); |
|
} |
|
|
|
#pragma mark - Misc |
|
|
|
- (void)observeApplicationNotifications { |
|
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
|
|
|
// restart scrolling when the app has been activated |
|
[[NSNotificationCenter defaultCenter] addObserver:self |
|
selector:@selector(scrollLabelIfNeeded) |
|
name:UIApplicationWillEnterForegroundNotification |
|
object:nil]; |
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self |
|
selector:@selector(scrollLabelIfNeeded) |
|
name:UIApplicationDidBecomeActiveNotification |
|
object:nil]; |
|
|
|
#ifndef TARGET_OS_TV |
|
// refresh labels when interface orientation is changed |
|
[[NSNotificationCenter defaultCenter] addObserver:self |
|
selector:@selector(onUIApplicationDidChangeStatusBarOrientationNotification:) |
|
name:UIApplicationDidChangeStatusBarOrientationNotification |
|
object:nil]; |
|
#endif |
|
|
|
} |
|
|
|
- (void)enableShadow { |
|
_scrolling = YES; |
|
[self applyGradientMaskForFadeLength:self.fadeLength enableFade:YES]; |
|
} |
|
|
|
- (void)scrollLabelIfNeeded { |
|
if (!self.text.length) |
|
return; |
|
|
|
CGFloat labelWidth = CGRectGetWidth(self.mainLabel.bounds); |
|
if (labelWidth <= CGRectGetWidth(self.bounds)) |
|
return; |
|
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(scrollLabelIfNeeded) object:nil]; |
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(enableShadow) object:nil]; |
|
|
|
[self.scrollView.layer removeAllAnimations]; |
|
|
|
BOOL doScrollLeft = (self.scrollDirection == CBAutoScrollDirectionLeft); |
|
self.scrollView.contentOffset = (doScrollLeft ? CGPointZero : CGPointMake(labelWidth + self.labelSpacing, 0)); |
|
|
|
// Add the left shadow after delay |
|
[self performSelector:@selector(enableShadow) withObject:nil afterDelay:self.pauseInterval]; |
|
|
|
// animate the scrolling |
|
NSTimeInterval duration = labelWidth / self.scrollSpeed; |
|
[UIView animateWithDuration:duration delay:self.pauseInterval options:self.animationOptions | UIViewAnimationOptionAllowUserInteraction animations:^{ |
|
// adjust offset |
|
self.scrollView.contentOffset = (doScrollLeft ? CGPointMake(labelWidth + self.labelSpacing, 0) : CGPointZero); |
|
} completion:^(BOOL finished) { |
|
_scrolling = NO; |
|
// remove the left shadow |
|
[self applyGradientMaskForFadeLength:self.fadeLength enableFade:NO]; |
|
// setup pause delay/loop |
|
if (finished) { |
|
[self performSelector:@selector(scrollLabelIfNeeded) withObject:nil]; |
|
} |
|
}]; |
|
} |
|
|
|
- (void)refreshLabels { |
|
__block float offset = 0; |
|
each_object(self.labels, ^(UILabel *label) { |
|
[label sizeToFit]; |
|
|
|
CGRect frame = label.frame; |
|
frame.origin = CGPointMake(offset, 0); |
|
frame.size.height = CGRectGetHeight(self.bounds); |
|
label.frame = frame; |
|
|
|
// Recenter label vertically within the scroll view |
|
label.center = CGPointMake(label.center.x, roundf(self.center.y - CGRectGetMinY(self.frame))); |
|
|
|
offset += CGRectGetWidth(label.bounds) + self.labelSpacing; |
|
}); |
|
|
|
self.scrollView.contentOffset = CGPointZero; |
|
[self.scrollView.layer removeAllAnimations]; |
|
|
|
// if the label is bigger than the space allocated, then it should scroll |
|
if (CGRectGetWidth(self.mainLabel.bounds) > CGRectGetWidth(self.bounds)) { |
|
CGSize size; |
|
size.width = CGRectGetWidth(self.mainLabel.bounds) + CGRectGetWidth(self.bounds) + self.labelSpacing; |
|
size.height = CGRectGetHeight(self.bounds); |
|
self.scrollView.contentSize = size; |
|
|
|
EACH_LABEL(hidden, NO) |
|
|
|
[self applyGradientMaskForFadeLength:self.fadeLength enableFade:self.scrolling]; |
|
|
|
[self scrollLabelIfNeeded]; |
|
} else { |
|
// Hide the other labels |
|
EACH_LABEL(hidden, (self.mainLabel != label)) |
|
|
|
// adjust the scroll view and main label |
|
self.scrollView.contentSize = self.bounds.size; |
|
self.mainLabel.frame = self.bounds; |
|
self.mainLabel.hidden = NO; |
|
self.mainLabel.textAlignment = self.textAlignment; |
|
|
|
// cleanup animation |
|
[self.scrollView.layer removeAllAnimations]; |
|
|
|
[self applyGradientMaskForFadeLength:0 enableFade:NO]; |
|
} |
|
} |
|
|
|
// bounds or frame has been changed |
|
- (void)didChangeFrame { |
|
[self refreshLabels]; |
|
[self applyGradientMaskForFadeLength:self.fadeLength enableFade:self.scrolling]; |
|
} |
|
|
|
#pragma mark - Gradient |
|
|
|
// ref: https://github.com/cbpowell/MarqueeLabel |
|
- (void)applyGradientMaskForFadeLength:(CGFloat)fadeLength enableFade:(BOOL)fade { |
|
CGFloat labelWidth = CGRectGetWidth(self.mainLabel.bounds); |
|
|
|
if (labelWidth <= CGRectGetWidth(self.bounds)) |
|
fadeLength = 0; |
|
|
|
if (fadeLength) { |
|
// Recreate gradient mask with new fade length |
|
CAGradientLayer *gradientMask = [CAGradientLayer layer]; |
|
|
|
gradientMask.bounds = self.layer.bounds; |
|
gradientMask.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); |
|
|
|
gradientMask.shouldRasterize = YES; |
|
gradientMask.rasterizationScale = [UIScreen mainScreen].scale; |
|
|
|
gradientMask.startPoint = CGPointMake(0, CGRectGetMidY(self.frame)); |
|
gradientMask.endPoint = CGPointMake(1, CGRectGetMidY(self.frame)); |
|
|
|
// setup fade mask colors and location |
|
id transparent = (id)[UIColor clearColor].CGColor; |
|
id opaque = (id)[UIColor blackColor].CGColor; |
|
gradientMask.colors = @[transparent, opaque, opaque, transparent]; |
|
|
|
// calcluate fade |
|
CGFloat fadePoint = fadeLength / CGRectGetWidth(self.bounds); |
|
NSNumber *leftFadePoint = @(fadePoint); |
|
NSNumber *rightFadePoint = @(1 - fadePoint); |
|
if (!fade) switch (self.scrollDirection) { |
|
case CBAutoScrollDirectionLeft: |
|
leftFadePoint = @0; |
|
break; |
|
|
|
case CBAutoScrollDirectionRight: |
|
leftFadePoint = @0; |
|
rightFadePoint = @1; |
|
break; |
|
} |
|
|
|
// apply calculations to mask |
|
gradientMask.locations = @[@0, leftFadePoint, rightFadePoint, @1]; |
|
|
|
// don't animate the mask change |
|
[CATransaction begin]; |
|
[CATransaction setDisableActions:YES]; |
|
self.layer.mask = gradientMask; |
|
[CATransaction commit]; |
|
} else { |
|
// Remove gradient mask for 0.0f length fade length |
|
self.layer.mask = nil; |
|
} |
|
} |
|
|
|
#pragma mark - Notifications |
|
|
|
- (void)onUIApplicationDidChangeStatusBarOrientationNotification:(NSNotification *)notification { |
|
// delay to have it re-calculate on next runloop |
|
[self performSelector:@selector(refreshLabels) withObject:nil afterDelay:.1f]; |
|
[self performSelector:@selector(scrollLabelIfNeeded) withObject:nil afterDelay:.1f]; |
|
} |
|
|
|
@end
|
|
|