CBAutoScrollLabel.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. //
  2. // CBAutoScrollLabel.m
  3. // CBAutoScrollLabel
  4. //
  5. // Created by Brian Stormont on 10/21/09.
  6. // Updated by Christopher Bess on 2/5/12
  7. //
  8. // Copyright 2009 Stormy Productions.
  9. //
  10. // Permission is granted to use this code free of charge for any project.
  11. //
  12. #import "CBAutoScrollLabel.h"
  13. #import <QuartzCore/QuartzCore.h>
  14. #define kLabelCount 2
  15. #define kDefaultFadeLength 7.f
  16. // pixel buffer space between scrolling label
  17. #define kDefaultLabelBufferSpace 20
  18. #define kDefaultPixelsPerSecond 30
  19. #define kDefaultPauseTime 1.5f
  20. // shortcut method for NSArray iterations
  21. static void each_object(NSArray *objects, void (^block)(id object)) {
  22. for (id obj in objects) {
  23. block(obj);
  24. }
  25. }
  26. // shortcut to change each label attribute value
  27. #define EACH_LABEL(ATTR, VALUE) each_object(self.labels, ^(UILabel *label) { label.ATTR = VALUE; });
  28. @interface CBAutoScrollLabel ()
  29. @property (nonatomic, strong) NSArray<UILabel *> *labels;
  30. @property (nonatomic, strong, readonly) UILabel *mainLabel;
  31. @property (nonatomic, strong) UIScrollView *scrollView;
  32. @end
  33. @implementation CBAutoScrollLabel
  34. - (id)initWithCoder:(NSCoder *)aDecoder {
  35. if ((self = [super initWithCoder:aDecoder])) {
  36. [self commonInit];
  37. }
  38. return self;
  39. }
  40. - (id)initWithFrame:(CGRect)frame {
  41. if ((self = [super initWithFrame:frame])) {
  42. [self commonInit];
  43. }
  44. return self;
  45. }
  46. - (void)commonInit {
  47. // create the labels
  48. NSMutableSet<UILabel *> *labelSet = [[NSMutableSet alloc] initWithCapacity:kLabelCount];
  49. for (int index = 0; index < kLabelCount; ++index) {
  50. UILabel *label = [[UILabel alloc] init];
  51. label.backgroundColor = [UIColor clearColor];
  52. label.autoresizingMask = self.autoresizingMask;
  53. // store labels
  54. [self.scrollView addSubview:label];
  55. [labelSet addObject:label];
  56. }
  57. self.labels = [labelSet.allObjects copy];
  58. // default values
  59. _scrollDirection = CBAutoScrollDirectionLeft;
  60. _scrollSpeed = kDefaultPixelsPerSecond;
  61. self.pauseInterval = kDefaultPauseTime;
  62. self.labelSpacing = kDefaultLabelBufferSpace;
  63. self.textAlignment = NSTextAlignmentLeft;
  64. self.animationOptions = UIViewAnimationOptionCurveLinear;
  65. self.scrollView.showsVerticalScrollIndicator = NO;
  66. self.scrollView.showsHorizontalScrollIndicator = NO;
  67. self.scrollView.scrollEnabled = NO;
  68. self.userInteractionEnabled = NO;
  69. self.backgroundColor = [UIColor clearColor];
  70. self.clipsToBounds = YES;
  71. self.fadeLength = kDefaultFadeLength;
  72. }
  73. - (void)dealloc {
  74. [NSObject cancelPreviousPerformRequestsWithTarget:self];
  75. [[NSNotificationCenter defaultCenter] removeObserver:self];
  76. }
  77. - (void)setFrame:(CGRect)frame {
  78. [super setFrame:frame];
  79. [self didChangeFrame];
  80. }
  81. // For autolayout
  82. - (void)setBounds:(CGRect)bounds {
  83. [super setBounds:bounds];
  84. [self didChangeFrame];
  85. }
  86. - (void)didMoveToWindow {
  87. [super didMoveToWindow];
  88. if (self.window) {
  89. [self scrollLabelIfNeeded];
  90. }
  91. }
  92. #pragma mark - Properties
  93. - (UIScrollView *)scrollView {
  94. if (_scrollView == nil) {
  95. _scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
  96. _scrollView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
  97. _scrollView.backgroundColor = [UIColor clearColor];
  98. [self addSubview:_scrollView];
  99. }
  100. return _scrollView;
  101. }
  102. - (void)setFadeLength:(CGFloat)fadeLength {
  103. if (_fadeLength != fadeLength) {
  104. _fadeLength = fadeLength;
  105. [self refreshLabels];
  106. [self applyGradientMaskForFadeLength:fadeLength enableFade:NO];
  107. }
  108. }
  109. - (UILabel *)mainLabel {
  110. return [self.labels firstObject];
  111. }
  112. - (void)setText:(NSString *)theText {
  113. [self setText:theText refreshLabels:YES];
  114. }
  115. - (void)setText:(NSString *)theText refreshLabels:(BOOL)refresh {
  116. // ignore identical text changes
  117. if ([theText isEqualToString:self.text])
  118. return;
  119. EACH_LABEL(text, theText)
  120. if (refresh)
  121. [self refreshLabels];
  122. }
  123. - (NSString *)text {
  124. return self.mainLabel.text;
  125. }
  126. - (void)setAttributedText:(NSAttributedString *)theText {
  127. [self setAttributedText:theText refreshLabels:YES];
  128. }
  129. - (void)setAttributedText:(NSAttributedString *)theText refreshLabels:(BOOL)refresh {
  130. // ignore identical text changes
  131. if ([theText.string isEqualToString:self.attributedText.string])
  132. return;
  133. EACH_LABEL(attributedText, theText)
  134. if (refresh)
  135. [self refreshLabels];
  136. }
  137. - (NSAttributedString *)attributedText {
  138. return self.mainLabel.attributedText;
  139. }
  140. - (void)setTextColor:(UIColor *)color {
  141. EACH_LABEL(textColor, color)
  142. }
  143. - (UIColor *)textColor {
  144. return self.mainLabel.textColor;
  145. }
  146. - (void)setFont:(UIFont *)font {
  147. if (self.mainLabel.font == font)
  148. return;
  149. EACH_LABEL(font, font)
  150. [self refreshLabels];
  151. [self invalidateIntrinsicContentSize];
  152. }
  153. - (UIFont *)font {
  154. return self.mainLabel.font;
  155. }
  156. - (void)setScrollSpeed:(float)speed {
  157. _scrollSpeed = speed;
  158. [self scrollLabelIfNeeded];
  159. }
  160. - (void)setScrollDirection:(CBAutoScrollDirection)direction {
  161. _scrollDirection = direction;
  162. [self scrollLabelIfNeeded];
  163. }
  164. - (void)setShadowColor:(UIColor *)color {
  165. EACH_LABEL(shadowColor, color)
  166. }
  167. - (UIColor *)shadowColor {
  168. return self.mainLabel.shadowColor;
  169. }
  170. - (void)setShadowOffset:(CGSize)offset {
  171. EACH_LABEL(shadowOffset, offset)
  172. }
  173. - (CGSize)shadowOffset {
  174. return self.mainLabel.shadowOffset;
  175. }
  176. #pragma mark - Autolayout
  177. - (CGSize)intrinsicContentSize {
  178. return CGSizeMake(0, [self.mainLabel intrinsicContentSize].height);
  179. }
  180. #pragma mark - Misc
  181. - (void)observeApplicationNotifications {
  182. [[NSNotificationCenter defaultCenter] removeObserver:self];
  183. // restart scrolling when the app has been activated
  184. [[NSNotificationCenter defaultCenter] addObserver:self
  185. selector:@selector(scrollLabelIfNeeded)
  186. name:UIApplicationWillEnterForegroundNotification
  187. object:nil];
  188. [[NSNotificationCenter defaultCenter] addObserver:self
  189. selector:@selector(scrollLabelIfNeeded)
  190. name:UIApplicationDidBecomeActiveNotification
  191. object:nil];
  192. #ifndef TARGET_OS_TV
  193. // refresh labels when interface orientation is changed
  194. [[NSNotificationCenter defaultCenter] addObserver:self
  195. selector:@selector(onUIApplicationDidChangeStatusBarOrientationNotification:)
  196. name:UIApplicationDidChangeStatusBarOrientationNotification
  197. object:nil];
  198. #endif
  199. }
  200. - (void)enableShadow {
  201. _scrolling = YES;
  202. [self applyGradientMaskForFadeLength:self.fadeLength enableFade:YES];
  203. }
  204. - (void)scrollLabelIfNeeded {
  205. if (!self.text.length)
  206. return;
  207. CGFloat labelWidth = CGRectGetWidth(self.mainLabel.bounds);
  208. if (labelWidth <= CGRectGetWidth(self.bounds))
  209. return;
  210. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(scrollLabelIfNeeded) object:nil];
  211. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(enableShadow) object:nil];
  212. [self.scrollView.layer removeAllAnimations];
  213. BOOL doScrollLeft = (self.scrollDirection == CBAutoScrollDirectionLeft);
  214. self.scrollView.contentOffset = (doScrollLeft ? CGPointZero : CGPointMake(labelWidth + self.labelSpacing, 0));
  215. // Add the left shadow after delay
  216. [self performSelector:@selector(enableShadow) withObject:nil afterDelay:self.pauseInterval];
  217. // animate the scrolling
  218. NSTimeInterval duration = labelWidth / self.scrollSpeed;
  219. [UIView animateWithDuration:duration delay:self.pauseInterval options:self.animationOptions | UIViewAnimationOptionAllowUserInteraction animations:^{
  220. // adjust offset
  221. self.scrollView.contentOffset = (doScrollLeft ? CGPointMake(labelWidth + self.labelSpacing, 0) : CGPointZero);
  222. } completion:^(BOOL finished) {
  223. self->_scrolling = NO;
  224. // remove the left shadow
  225. [self applyGradientMaskForFadeLength:self.fadeLength enableFade:NO];
  226. // setup pause delay/loop
  227. if (finished) {
  228. [self performSelector:@selector(scrollLabelIfNeeded) withObject:nil];
  229. }
  230. }];
  231. }
  232. - (void)refreshLabels {
  233. __block float offset = 0;
  234. each_object(self.labels, ^(UILabel *label) {
  235. [label sizeToFit];
  236. CGRect frame = label.frame;
  237. frame.origin = CGPointMake(offset, 0);
  238. frame.size.height = CGRectGetHeight(self.bounds);
  239. label.frame = frame;
  240. // Recenter label vertically within the scroll view
  241. label.center = CGPointMake(label.center.x, roundf(self.center.y - CGRectGetMinY(self.frame)));
  242. offset += CGRectGetWidth(label.bounds) + self.labelSpacing;
  243. });
  244. self.scrollView.contentOffset = CGPointZero;
  245. [self.scrollView.layer removeAllAnimations];
  246. // if the label is bigger than the space allocated, then it should scroll
  247. if (CGRectGetWidth(self.mainLabel.bounds) > CGRectGetWidth(self.bounds)) {
  248. CGSize size;
  249. size.width = CGRectGetWidth(self.mainLabel.bounds) + CGRectGetWidth(self.bounds) + self.labelSpacing;
  250. size.height = CGRectGetHeight(self.bounds);
  251. self.scrollView.contentSize = size;
  252. EACH_LABEL(hidden, NO)
  253. [self applyGradientMaskForFadeLength:self.fadeLength enableFade:self.scrolling];
  254. [self scrollLabelIfNeeded];
  255. } else {
  256. // Hide the other labels
  257. EACH_LABEL(hidden, (self.mainLabel != label))
  258. // adjust the scroll view and main label
  259. self.scrollView.contentSize = self.bounds.size;
  260. self.mainLabel.frame = self.bounds;
  261. self.mainLabel.hidden = NO;
  262. self.mainLabel.textAlignment = self.textAlignment;
  263. // cleanup animation
  264. [self.scrollView.layer removeAllAnimations];
  265. [self applyGradientMaskForFadeLength:0 enableFade:NO];
  266. }
  267. }
  268. // bounds or frame has been changed
  269. - (void)didChangeFrame {
  270. [self refreshLabels];
  271. [self applyGradientMaskForFadeLength:self.fadeLength enableFade:self.scrolling];
  272. }
  273. #pragma mark - Gradient
  274. // ref: https://github.com/cbpowell/MarqueeLabel
  275. - (void)applyGradientMaskForFadeLength:(CGFloat)fadeLength enableFade:(BOOL)fade {
  276. CGFloat labelWidth = CGRectGetWidth(self.mainLabel.bounds);
  277. if (labelWidth <= CGRectGetWidth(self.bounds))
  278. fadeLength = 0;
  279. if (fadeLength) {
  280. // Recreate gradient mask with new fade length
  281. CAGradientLayer *gradientMask = [CAGradientLayer layer];
  282. gradientMask.bounds = self.layer.bounds;
  283. gradientMask.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  284. gradientMask.shouldRasterize = YES;
  285. gradientMask.rasterizationScale = [UIScreen mainScreen].scale;
  286. gradientMask.startPoint = CGPointMake(0, CGRectGetMidY(self.frame));
  287. gradientMask.endPoint = CGPointMake(1, CGRectGetMidY(self.frame));
  288. // setup fade mask colors and location
  289. id transparent = (id)[UIColor clearColor].CGColor;
  290. id opaque = (id)[UIColor blackColor].CGColor;
  291. gradientMask.colors = @[transparent, opaque, opaque, transparent];
  292. // calcluate fade
  293. CGFloat fadePoint = fadeLength / CGRectGetWidth(self.bounds);
  294. NSNumber *leftFadePoint = @(fadePoint);
  295. NSNumber *rightFadePoint = @(1 - fadePoint);
  296. if (!fade) switch (self.scrollDirection) {
  297. case CBAutoScrollDirectionLeft:
  298. leftFadePoint = @0;
  299. break;
  300. case CBAutoScrollDirectionRight:
  301. leftFadePoint = @0;
  302. rightFadePoint = @1;
  303. break;
  304. }
  305. // apply calculations to mask
  306. gradientMask.locations = @[@0, leftFadePoint, rightFadePoint, @1];
  307. // don't animate the mask change
  308. [CATransaction begin];
  309. [CATransaction setDisableActions:YES];
  310. self.layer.mask = gradientMask;
  311. [CATransaction commit];
  312. } else {
  313. // Remove gradient mask for 0.0f length fade length
  314. self.layer.mask = nil;
  315. }
  316. }
  317. #pragma mark - Notifications
  318. - (void)onUIApplicationDidChangeStatusBarOrientationNotification:(NSNotification *)notification {
  319. // delay to have it re-calculate on next runloop
  320. [self performSelector:@selector(refreshLabels) withObject:nil afterDelay:.1f];
  321. [self performSelector:@selector(scrollLabelIfNeeded) withObject:nil afterDelay:.1f];
  322. }
  323. @end