MarqueeLabel.m 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517
  1. //
  2. // MarqueeLabel.m
  3. //
  4. // Created by Charles Powell on 1/31/11.
  5. // Copyright (c) 2011-2015 Charles Powell. All rights reserved.
  6. //
  7. #import "MarqueeLabel.h"
  8. #import <QuartzCore/QuartzCore.h>
  9. // Notification strings
  10. NSString *const kMarqueeLabelControllerRestartNotification = @"MarqueeLabelViewControllerRestart";
  11. NSString *const kMarqueeLabelShouldLabelizeNotification = @"MarqueeLabelShouldLabelizeNotification";
  12. NSString *const kMarqueeLabelShouldAnimateNotification = @"MarqueeLabelShouldAnimateNotification";
  13. NSString *const kMarqueeLabelAnimationCompletionBlock = @"MarqueeLabelAnimationCompletionBlock";
  14. // Animation completion block
  15. typedef void(^MLAnimationCompletionBlock)(BOOL finished);
  16. // iOS Version check for iOS 8.0.0
  17. #define SYSTEM_VERSION_IS_8_0_X ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"8.0"])
  18. // Helpers
  19. @interface UIView (MarqueeLabelHelpers)
  20. - (UIViewController *)firstAvailableViewController;
  21. - (id)traverseResponderChainForFirstViewController;
  22. @end
  23. @interface CAMediaTimingFunction (MarqueeLabelHelpers)
  24. - (NSArray *)controlPoints;
  25. - (CGFloat)durationPercentageForPositionPercentage:(CGFloat)positionPercentage withDuration:(NSTimeInterval)duration;
  26. @end
  27. @interface MarqueeLabel()
  28. @property (nonatomic, strong) UILabel *subLabel;
  29. @property (nonatomic, assign) NSTimeInterval animationDuration;
  30. @property (nonatomic, assign, readonly) BOOL labelShouldScroll;
  31. @property (nonatomic, weak) UITapGestureRecognizer *tapRecognizer;
  32. @property (nonatomic, assign) CGRect homeLabelFrame;
  33. @property (nonatomic, assign) CGFloat awayOffset;
  34. @property (nonatomic, assign, readwrite) BOOL isPaused;
  35. // Support
  36. @property (nonatomic, strong) NSArray *gradientColors;
  37. CGPoint MLOffsetCGPoint(CGPoint point, CGFloat offset);
  38. @end
  39. @implementation MarqueeLabel
  40. #pragma mark - Class Methods and handlers
  41. + (void)restartLabelsOfController:(UIViewController *)controller {
  42. [MarqueeLabel notifyController:controller
  43. withMessage:kMarqueeLabelControllerRestartNotification];
  44. }
  45. + (void)controllerViewWillAppear:(UIViewController *)controller {
  46. [MarqueeLabel restartLabelsOfController:controller];
  47. }
  48. + (void)controllerViewDidAppear:(UIViewController *)controller {
  49. [MarqueeLabel restartLabelsOfController:controller];
  50. }
  51. + (void)controllerViewAppearing:(UIViewController *)controller {
  52. [MarqueeLabel restartLabelsOfController:controller];
  53. }
  54. + (void)controllerLabelsShouldLabelize:(UIViewController *)controller {
  55. [MarqueeLabel notifyController:controller
  56. withMessage:kMarqueeLabelShouldLabelizeNotification];
  57. }
  58. + (void)controllerLabelsShouldAnimate:(UIViewController *)controller {
  59. [MarqueeLabel notifyController:controller
  60. withMessage:kMarqueeLabelShouldAnimateNotification];
  61. }
  62. + (void)notifyController:(UIViewController *)controller withMessage:(NSString *)message
  63. {
  64. if (controller && message) {
  65. [[NSNotificationCenter defaultCenter] postNotificationName:message
  66. object:nil
  67. userInfo:[NSDictionary dictionaryWithObject:controller
  68. forKey:@"controller"]];
  69. }
  70. }
  71. - (void)viewControllerShouldRestart:(NSNotification *)notification {
  72. UIViewController *controller = [[notification userInfo] objectForKey:@"controller"];
  73. if (controller == [self firstAvailableViewController]) {
  74. [self restartLabel];
  75. }
  76. }
  77. - (void)labelsShouldLabelize:(NSNotification *)notification {
  78. UIViewController *controller = [[notification userInfo] objectForKey:@"controller"];
  79. if (controller == [self firstAvailableViewController]) {
  80. self.labelize = YES;
  81. }
  82. }
  83. - (void)labelsShouldAnimate:(NSNotification *)notification {
  84. UIViewController *controller = [[notification userInfo] objectForKey:@"controller"];
  85. if (controller == [self firstAvailableViewController]) {
  86. self.labelize = NO;
  87. }
  88. }
  89. #pragma mark - Initialization and Label Config
  90. - (id)initWithFrame:(CGRect)frame {
  91. return [self initWithFrame:frame duration:7.0 andFadeLength:0.0];
  92. }
  93. - (id)initWithFrame:(CGRect)frame duration:(NSTimeInterval)aLengthOfScroll andFadeLength:(CGFloat)aFadeLength {
  94. self = [super initWithFrame:frame];
  95. if (self) {
  96. [self setupLabel];
  97. _scrollDuration = aLengthOfScroll;
  98. self.fadeLength = MIN(aFadeLength, frame.size.width/2);
  99. }
  100. return self;
  101. }
  102. - (id)initWithFrame:(CGRect)frame rate:(CGFloat)pixelsPerSec andFadeLength:(CGFloat)aFadeLength {
  103. self = [super initWithFrame:frame];
  104. if (self) {
  105. [self setupLabel];
  106. _rate = pixelsPerSec;
  107. self.fadeLength = MIN(aFadeLength, frame.size.width/2);
  108. }
  109. return self;
  110. }
  111. - (id)initWithCoder:(NSCoder *)aDecoder {
  112. self = [super initWithCoder:aDecoder];
  113. if (self) {
  114. [self setupLabel];
  115. if (self.scrollDuration == 0) {
  116. self.scrollDuration = 7.0;
  117. }
  118. }
  119. return self;
  120. }
  121. - (void)awakeFromNib {
  122. [super awakeFromNib];
  123. [self forwardPropertiesToSubLabel];
  124. }
  125. + (Class)layerClass {
  126. return [CAReplicatorLayer class];
  127. }
  128. - (CAReplicatorLayer *)repliLayer {
  129. return (CAReplicatorLayer *)self.layer;
  130. }
  131. - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
  132. // Do NOT call super, to prevent UILabel superclass from drawing into context
  133. // Label drawing is handled by sublabel and CAReplicatorLayer layer class
  134. // Draw only background color
  135. CGContextSetFillColorWithColor(ctx, self.backgroundColor.CGColor);
  136. CGContextFillRect(ctx, layer.bounds);
  137. }
  138. - (void)forwardPropertiesToSubLabel {
  139. /*
  140. Note that this method is currently ONLY called from awakeFromNib, i.e. when
  141. text properties are set via a Storyboard. As the Storyboard/IB doesn't currently
  142. support attributed strings, there's no need to "forward" the super attributedString value.
  143. */
  144. // Since we're a UILabel, we actually do implement all of UILabel's properties.
  145. // We don't care about these values, we just want to forward them on to our sublabel.
  146. NSArray *properties = @[@"baselineAdjustment", @"enabled", @"highlighted", @"highlightedTextColor",
  147. @"minimumFontSize", @"textAlignment",
  148. @"userInteractionEnabled", @"adjustsFontSizeToFitWidth",
  149. @"lineBreakMode", @"numberOfLines"];
  150. // Iterate through properties
  151. self.subLabel.text = super.text;
  152. self.subLabel.font = super.font;
  153. self.subLabel.textColor = super.textColor;
  154. self.subLabel.backgroundColor = (super.backgroundColor == nil ? [UIColor clearColor] : super.backgroundColor);
  155. self.subLabel.shadowColor = super.shadowColor;
  156. self.subLabel.shadowOffset = super.shadowOffset;
  157. for (NSString *property in properties) {
  158. id val = [super valueForKey:property];
  159. [self.subLabel setValue:val forKey:property];
  160. }
  161. }
  162. - (void)setupLabel {
  163. // Basic UILabel options override
  164. self.clipsToBounds = YES;
  165. self.numberOfLines = 1;
  166. // Create first sublabel
  167. self.subLabel = [[UILabel alloc] initWithFrame:self.bounds];
  168. self.subLabel.tag = 700;
  169. self.subLabel.layer.anchorPoint = CGPointMake(0.0f, 0.0f);
  170. [self addSubview:self.subLabel];
  171. // Setup default values
  172. _awayOffset = 0.0f;
  173. _animationCurve = UIViewAnimationOptionCurveLinear;
  174. _labelize = NO;
  175. _holdScrolling = NO;
  176. _tapToScroll = NO;
  177. _isPaused = NO;
  178. _fadeLength = 0.0f;
  179. _animationDelay = 1.0;
  180. _animationDuration = 0.0f;
  181. _leadingBuffer = 0.0f;
  182. _trailingBuffer = 0.0f;
  183. // Add notification observers
  184. // Custom class notifications
  185. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewControllerShouldRestart:) name:kMarqueeLabelControllerRestartNotification object:nil];
  186. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(labelsShouldLabelize:) name:kMarqueeLabelShouldLabelizeNotification object:nil];
  187. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(labelsShouldAnimate:) name:kMarqueeLabelShouldAnimateNotification object:nil];
  188. // UIApplication state notifications
  189. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restartLabel) name:UIApplicationDidBecomeActiveNotification object:nil];
  190. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shutdownLabel) name:UIApplicationDidEnterBackgroundNotification object:nil];
  191. }
  192. - (void)minimizeLabelFrameWithMaximumSize:(CGSize)maxSize adjustHeight:(BOOL)adjustHeight {
  193. if (self.subLabel.text != nil) {
  194. // Calculate text size
  195. if (CGSizeEqualToSize(maxSize, CGSizeZero)) {
  196. maxSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
  197. }
  198. CGSize minimumLabelSize = [self subLabelSize];
  199. // Adjust for fade length
  200. CGSize minimumSize = CGSizeMake(minimumLabelSize.width + (self.fadeLength * 2), minimumLabelSize.height);
  201. // Find minimum size of options
  202. minimumSize = CGSizeMake(MIN(minimumSize.width, maxSize.width), MIN(minimumSize.height, maxSize.height));
  203. // Apply to frame
  204. self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, minimumSize.width, (adjustHeight ? minimumSize.height : self.frame.size.height));
  205. }
  206. }
  207. -(void)didMoveToSuperview {
  208. [self updateSublabel];
  209. }
  210. #pragma mark - MarqueeLabel Heavy Lifting
  211. - (void)layoutSubviews
  212. {
  213. [super layoutSubviews];
  214. [self updateSublabel];
  215. }
  216. - (void)willMoveToWindow:(UIWindow *)newWindow {
  217. if (!newWindow) {
  218. [self shutdownLabel];
  219. }
  220. }
  221. - (void)didMoveToWindow {
  222. if (!self.window) {
  223. [self shutdownLabel];
  224. } else {
  225. [self updateSublabel];
  226. }
  227. }
  228. - (void)updateSublabel {
  229. [self updateSublabelAndBeginScroll:YES];
  230. }
  231. - (void)updateSublabelAndBeginScroll:(BOOL)beginScroll {
  232. if (!self.subLabel.text || !self.superview) {
  233. return;
  234. }
  235. // Calculate expected size
  236. CGSize expectedLabelSize = [self subLabelSize];
  237. // Invalidate intrinsic size
  238. [self invalidateIntrinsicContentSize];
  239. // Move to home
  240. [self returnLabelToOriginImmediately];
  241. // Configure gradient for the current condition
  242. [self applyGradientMaskForFadeLength:self.fadeLength animated:YES];
  243. // Check if label should scroll
  244. // Can be because: 1) text fits, or 2) labelization
  245. // The holdScrolling property does NOT affect this
  246. if (!self.labelShouldScroll) {
  247. // Set text alignment and break mode to act like normal label
  248. self.subLabel.textAlignment = [super textAlignment];
  249. self.subLabel.lineBreakMode = [super lineBreakMode];
  250. CGRect labelFrame, unusedFrame;
  251. switch (self.marqueeType) {
  252. case MLContinuousReverse:
  253. case MLRightLeft:
  254. CGRectDivide(self.bounds, &unusedFrame, &labelFrame, self.leadingBuffer, CGRectMaxXEdge);
  255. labelFrame = CGRectIntegral(labelFrame);
  256. break;
  257. default:
  258. labelFrame = CGRectIntegral(CGRectMake(self.leadingBuffer, 0.0f, self.bounds.size.width - self.leadingBuffer, self.bounds.size.height));
  259. break;
  260. }
  261. self.homeLabelFrame = labelFrame;
  262. self.awayOffset = 0.0f;
  263. // Remove an additional sublabels (for continuous types)
  264. self.repliLayer.instanceCount = 1;
  265. // Set sublabel frame calculated labelFrame
  266. self.subLabel.frame = labelFrame;
  267. return;
  268. }
  269. // Label DOES need to scroll
  270. [self.subLabel setLineBreakMode:NSLineBreakByClipping];
  271. // Spacing between primary and second sublabel must be at least equal to leadingBuffer, and at least equal to the fadeLength
  272. CGFloat minTrailing = MAX(MAX(self.leadingBuffer, self.trailingBuffer), self.fadeLength);
  273. switch (self.marqueeType) {
  274. case MLContinuous:
  275. case MLContinuousReverse:
  276. {
  277. if (self.marqueeType == MLContinuous) {
  278. self.homeLabelFrame = CGRectIntegral(CGRectMake(self.leadingBuffer, 0.0f, expectedLabelSize.width, self.bounds.size.height));
  279. self.awayOffset = -(self.homeLabelFrame.size.width + minTrailing);
  280. } else {
  281. self.homeLabelFrame = CGRectIntegral(CGRectMake(self.bounds.size.width - (expectedLabelSize.width + self.leadingBuffer), 0.0f, expectedLabelSize.width, self.bounds.size.height));
  282. self.awayOffset = (self.homeLabelFrame.size.width + minTrailing);
  283. }
  284. self.subLabel.frame = self.homeLabelFrame;
  285. // Configure replication
  286. self.repliLayer.instanceCount = 2;
  287. self.repliLayer.instanceTransform = CATransform3DMakeTranslation(-self.awayOffset, 0.0, 0.0);
  288. // Recompute the animation duration
  289. self.animationDuration = (self.rate != 0) ? ((NSTimeInterval) fabs(self.awayOffset) / self.rate) : (self.scrollDuration);
  290. break;
  291. }
  292. case MLRightLeft:
  293. {
  294. self.homeLabelFrame = CGRectIntegral(CGRectMake(self.bounds.size.width - (expectedLabelSize.width + self.leadingBuffer), 0.0f, expectedLabelSize.width, self.bounds.size.height));
  295. self.awayOffset = (expectedLabelSize.width + self.trailingBuffer + self.leadingBuffer) - self.bounds.size.width;
  296. // Calculate animation duration
  297. self.animationDuration = (self.rate != 0) ? (NSTimeInterval)fabs(self.awayOffset / self.rate) : (self.scrollDuration);
  298. // Set frame and text
  299. self.subLabel.frame = self.homeLabelFrame;
  300. // Remove any replication
  301. self.repliLayer.instanceCount = 1;
  302. // Enforce text alignment for this type
  303. self.subLabel.textAlignment = NSTextAlignmentRight;
  304. break;
  305. }
  306. case MLLeftRight:
  307. {
  308. self.homeLabelFrame = CGRectIntegral(CGRectMake(self.leadingBuffer, 0.0f, expectedLabelSize.width, expectedLabelSize.height));
  309. self.awayOffset = self.bounds.size.width - (expectedLabelSize.width + self.leadingBuffer + self.trailingBuffer);
  310. // Calculate animation duration
  311. self.animationDuration = (self.rate != 0) ? (NSTimeInterval)fabs(self.awayOffset / self.rate) : (self.scrollDuration);
  312. // Set frame
  313. self.subLabel.frame = self.homeLabelFrame;
  314. // Remove any replication
  315. self.repliLayer.instanceCount = 1;
  316. // Enforce text alignment for this type
  317. self.subLabel.textAlignment = NSTextAlignmentLeft;
  318. break;
  319. }
  320. default:
  321. {
  322. // Something strange has happened
  323. self.homeLabelFrame = CGRectZero;
  324. self.awayOffset = 0.0f;
  325. // Do not attempt to begin scroll
  326. return;
  327. break;
  328. }
  329. } //end of marqueeType switch
  330. if (!self.tapToScroll && !self.holdScrolling && beginScroll) {
  331. [self beginScroll];
  332. }
  333. }
  334. - (CGSize)subLabelSize {
  335. // Calculate expected size
  336. CGSize expectedLabelSize = CGSizeZero;
  337. CGSize maximumLabelSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
  338. // Get size of subLabel
  339. expectedLabelSize = [self.subLabel sizeThatFits:maximumLabelSize];
  340. // Sanitize width to 5461.0f (largest width a UILabel will draw on an iPhone 6S Plus)
  341. expectedLabelSize.width = MIN(expectedLabelSize.width, 5461.0f);
  342. // Adjust to own height (make text baseline match normal label)
  343. expectedLabelSize.height = self.bounds.size.height;
  344. return expectedLabelSize;
  345. }
  346. - (CGSize)sizeThatFits:(CGSize)size {
  347. CGSize fitSize = [self.subLabel sizeThatFits:size];
  348. fitSize.width += self.leadingBuffer;
  349. return fitSize;
  350. }
  351. #pragma mark - Animation Handlers
  352. - (BOOL)labelShouldScroll {
  353. BOOL stringLength = ([self.subLabel.text length] > 0);
  354. if (!stringLength) {
  355. return NO;
  356. }
  357. BOOL labelTooLarge = ([self subLabelSize].width + self.leadingBuffer > self.bounds.size.width);
  358. BOOL animationHasDuration = (self.scrollDuration > 0.0f || self.rate > 0.0f);
  359. return (!self.labelize && labelTooLarge && animationHasDuration);
  360. }
  361. - (BOOL)labelReadyForScroll {
  362. // Check if we have a superview
  363. if (!self.superview) {
  364. return NO;
  365. }
  366. if (!self.window) {
  367. return NO;
  368. }
  369. // Check if our view controller is ready
  370. UIViewController *viewController = [self firstAvailableViewController];
  371. if (!viewController.isViewLoaded) {
  372. return NO;
  373. }
  374. return YES;
  375. }
  376. - (void)beginScroll {
  377. [self beginScrollWithDelay:YES];
  378. }
  379. - (void)beginScrollWithDelay:(BOOL)delay {
  380. switch (self.marqueeType) {
  381. case MLContinuous:
  382. case MLContinuousReverse:
  383. [self scrollContinuousWithInterval:self.animationDuration after:(delay ? self.animationDelay : 0.0)];
  384. break;
  385. default:
  386. [self scrollAwayWithInterval:self.animationDuration];
  387. break;
  388. }
  389. }
  390. - (void)returnLabelToOriginImmediately {
  391. // Remove gradient animations
  392. [self.layer.mask removeAllAnimations];
  393. // Remove sublabel position animations
  394. [self.subLabel.layer removeAllAnimations];
  395. }
  396. - (void)scrollAwayWithInterval:(NSTimeInterval)interval {
  397. [self scrollAwayWithInterval:interval delay:YES];
  398. }
  399. - (void)scrollAwayWithInterval:(NSTimeInterval)interval delay:(BOOL)delay {
  400. [self scrollAwayWithInterval:interval delayAmount:(delay ? self.animationDelay : 0.0)];
  401. }
  402. - (void)scrollAwayWithInterval:(NSTimeInterval)interval delayAmount:(NSTimeInterval)delayAmount {
  403. // Check for conditions which would prevent scrolling
  404. if (![self labelReadyForScroll]) {
  405. return;
  406. }
  407. // Return labels to home (cancel any animations)
  408. [self returnLabelToOriginImmediately];
  409. // Call pre-animation method
  410. [self labelWillBeginScroll];
  411. // Animate
  412. [CATransaction begin];
  413. // Set Duration
  414. [CATransaction setAnimationDuration:(2.0 * (delayAmount + interval))];
  415. // Create animation for gradient, if needed
  416. if (self.fadeLength != 0.0f) {
  417. CAKeyframeAnimation *gradAnim = [self keyFrameAnimationForGradientFadeLength:self.fadeLength
  418. interval:interval
  419. delay:delayAmount];
  420. [self.layer.mask addAnimation:gradAnim forKey:@"gradient"];
  421. }
  422. MLAnimationCompletionBlock completionBlock = ^(BOOL finished) {
  423. if (!finished) {
  424. // Do not continue into the next loop
  425. return;
  426. }
  427. // Call returned home method
  428. [self labelReturnedToHome:YES];
  429. // Check to ensure that:
  430. // 1) We don't double fire if an animation already exists
  431. // 2) The instance is still attached to a window - this completion block is called for
  432. // many reasons, including if the animation is removed due to the view being removed
  433. // from the UIWindow (typically when the view controller is no longer the "top" view)
  434. if (self.window && ![self.subLabel.layer animationForKey:@"position"]) {
  435. // Begin again, if conditions met
  436. if (self.labelShouldScroll && !self.tapToScroll && !self.holdScrolling) {
  437. [self scrollAwayWithInterval:interval delayAmount:delayAmount];
  438. }
  439. }
  440. };
  441. // Create animation for position
  442. CGPoint homeOrigin = self.homeLabelFrame.origin;
  443. CGPoint awayOrigin = MLOffsetCGPoint(self.homeLabelFrame.origin, self.awayOffset);
  444. NSArray *values = @[[NSValue valueWithCGPoint:homeOrigin], // Initial location, home
  445. [NSValue valueWithCGPoint:homeOrigin], // Initial delay, at home
  446. [NSValue valueWithCGPoint:awayOrigin], // Animation to away
  447. [NSValue valueWithCGPoint:awayOrigin], // Delay at away
  448. [NSValue valueWithCGPoint:homeOrigin]]; // Animation to home
  449. CAKeyframeAnimation *awayAnim = [self keyFrameAnimationForProperty:@"position"
  450. values:values
  451. interval:interval
  452. delay:delayAmount];
  453. // Add completion block
  454. [awayAnim setValue:completionBlock forKey:kMarqueeLabelAnimationCompletionBlock];
  455. // Add animation
  456. [self.subLabel.layer addAnimation:awayAnim forKey:@"position"];
  457. [CATransaction commit];
  458. }
  459. - (void)scrollContinuousWithInterval:(NSTimeInterval)interval after:(NSTimeInterval)delayAmount {
  460. [self scrollContinuousWithInterval:interval after:delayAmount labelAnimation:nil gradientAnimation:nil];
  461. }
  462. - (void)scrollContinuousWithInterval:(NSTimeInterval)interval
  463. after:(NSTimeInterval)delayAmount
  464. labelAnimation:(CAKeyframeAnimation *)labelAnimation
  465. gradientAnimation:(CAKeyframeAnimation *)gradientAnimation {
  466. // Check for conditions which would prevent scrolling
  467. if (![self labelReadyForScroll]) {
  468. return;
  469. }
  470. // Return labels to home (cancel any animations)
  471. [self returnLabelToOriginImmediately];
  472. // Call pre-animation method
  473. [self labelWillBeginScroll];
  474. // Animate
  475. [CATransaction begin];
  476. // Set Duration
  477. [CATransaction setAnimationDuration:(delayAmount + interval)];
  478. // Create animation for gradient, if needed
  479. if (self.fadeLength != 0.0f) {
  480. if (!gradientAnimation) {
  481. gradientAnimation = [self keyFrameAnimationForGradientFadeLength:self.fadeLength
  482. interval:interval
  483. delay:delayAmount];
  484. }
  485. [self.layer.mask addAnimation:gradientAnimation forKey:@"gradient"];
  486. }
  487. // Create animation for sublabel positions, if needed
  488. if (!labelAnimation) {
  489. CGPoint homeOrigin = self.homeLabelFrame.origin;
  490. CGPoint awayOrigin = MLOffsetCGPoint(self.homeLabelFrame.origin, self.awayOffset);
  491. NSArray *values = @[[NSValue valueWithCGPoint:homeOrigin], // Initial location, home
  492. [NSValue valueWithCGPoint:homeOrigin], // Initial delay, at home
  493. [NSValue valueWithCGPoint:awayOrigin]]; // Animation to home
  494. labelAnimation = [self keyFrameAnimationForProperty:@"position"
  495. values:values
  496. interval:interval
  497. delay:delayAmount];
  498. }
  499. MLAnimationCompletionBlock completionBlock = ^(BOOL finished) {
  500. if (!finished) {
  501. // Do not continue into the next loop
  502. return;
  503. }
  504. // Call returned home method
  505. [self labelReturnedToHome:YES];
  506. // Check to ensure that:
  507. // 1) We don't double fire if an animation already exists
  508. // 2) The instance is still attached to a window - this completion block is called for
  509. // many reasons, including if the animation is removed due to the view being removed
  510. // from the UIWindow (typically when the view controller is no longer the "top" view)
  511. if (self.window && ![self.subLabel.layer animationForKey:@"position"]) {
  512. // Begin again, if conditions met
  513. if (self.labelShouldScroll && !self.tapToScroll && !self.holdScrolling) {
  514. [self scrollContinuousWithInterval:interval
  515. after:delayAmount
  516. labelAnimation:labelAnimation
  517. gradientAnimation:gradientAnimation];
  518. }
  519. }
  520. };
  521. // Attach completion block
  522. [labelAnimation setValue:completionBlock forKey:kMarqueeLabelAnimationCompletionBlock];
  523. // Add animation
  524. [self.subLabel.layer addAnimation:labelAnimation forKey:@"position"];
  525. [CATransaction commit];
  526. }
  527. - (void)applyGradientMaskForFadeLength:(CGFloat)fadeLength animated:(BOOL)animated {
  528. // Check for zero-length fade
  529. if (fadeLength <= 0.0f) {
  530. [self removeGradientMask];
  531. return;
  532. }
  533. CAGradientLayer *gradientMask = (CAGradientLayer *)self.layer.mask;
  534. [gradientMask removeAllAnimations];
  535. if (!gradientMask) {
  536. // Create CAGradientLayer if needed
  537. gradientMask = [CAGradientLayer layer];
  538. }
  539. // Set up colors
  540. NSObject *transparent = (NSObject *)[[UIColor clearColor] CGColor];
  541. NSObject *opaque = (NSObject *)[[UIColor blackColor] CGColor];
  542. gradientMask.bounds = self.layer.bounds;
  543. gradientMask.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  544. gradientMask.shouldRasterize = YES;
  545. gradientMask.rasterizationScale = [UIScreen mainScreen].scale;
  546. gradientMask.startPoint = CGPointMake(0.0f, 0.5f);
  547. gradientMask.endPoint = CGPointMake(1.0f, 0.5f);
  548. // Start with "no fade" colors and locations
  549. gradientMask.colors = @[opaque, opaque, opaque, opaque];
  550. gradientMask.locations = @[@(0.0f), @(0.0f), @(1.0f), @(1.0f)];
  551. // Set mask
  552. self.layer.mask = gradientMask;
  553. CGFloat leftFadeStop = fadeLength/self.bounds.size.width;
  554. CGFloat rightFadeStop = fadeLength/self.bounds.size.width;
  555. // Adjust stops based on fade length
  556. NSArray *adjustedLocations = @[@(0.0), @(leftFadeStop), @(1.0 - rightFadeStop), @(1.0)];
  557. // Determine colors for non-scrolling label (i.e. at home)
  558. NSArray *adjustedColors;
  559. BOOL trailingFadeNeeded = self.labelShouldScroll;
  560. switch (self.marqueeType) {
  561. case MLContinuousReverse:
  562. case MLRightLeft:
  563. adjustedColors = @[(trailingFadeNeeded ? transparent : opaque),
  564. opaque,
  565. opaque,
  566. opaque];
  567. break;
  568. default:
  569. // MLContinuous
  570. // MLLeftRight
  571. adjustedColors = @[opaque,
  572. opaque,
  573. opaque,
  574. (trailingFadeNeeded ? transparent : opaque)];
  575. break;
  576. }
  577. if (animated) {
  578. // Create animation for location change
  579. CABasicAnimation *locationAnimation = [CABasicAnimation animationWithKeyPath:@"locations"];
  580. locationAnimation.fromValue = gradientMask.locations;
  581. locationAnimation.toValue = adjustedLocations;
  582. locationAnimation.duration = 0.25;
  583. // Create animation for color change
  584. CABasicAnimation *colorAnimation = [CABasicAnimation animationWithKeyPath:@"colors"];
  585. colorAnimation.fromValue = gradientMask.colors;
  586. colorAnimation.toValue = adjustedColors;
  587. colorAnimation.duration = 0.25;
  588. CAAnimationGroup *group = [CAAnimationGroup animation];
  589. group.duration = 0.25;
  590. group.animations = @[locationAnimation, colorAnimation];
  591. [gradientMask addAnimation:group forKey:colorAnimation.keyPath];
  592. gradientMask.locations = adjustedLocations;
  593. gradientMask.colors = adjustedColors;
  594. } else {
  595. [CATransaction begin];
  596. [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
  597. gradientMask.locations = adjustedLocations;
  598. gradientMask.colors = adjustedColors;
  599. [CATransaction commit];
  600. }
  601. }
  602. - (void)removeGradientMask {
  603. self.layer.mask = nil;
  604. }
  605. - (CAKeyframeAnimation *)keyFrameAnimationForGradientFadeLength:(CGFloat)fadeLength
  606. interval:(NSTimeInterval)interval
  607. delay:(NSTimeInterval)delayAmount
  608. {
  609. // Setup
  610. NSArray *values = nil;
  611. NSArray *keyTimes = nil;
  612. NSTimeInterval totalDuration;
  613. NSObject *transp = (NSObject *)[[UIColor clearColor] CGColor];
  614. NSObject *opaque = (NSObject *)[[UIColor blackColor] CGColor];
  615. // Create new animation
  616. CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"colors"];
  617. // Get timing function
  618. CAMediaTimingFunction *timingFunction = [self timingFunctionForAnimationOptions:self.animationCurve];
  619. // Define keyTimes
  620. switch (self.marqueeType) {
  621. case MLLeftRight:
  622. case MLRightLeft:
  623. // Calculate total animation duration
  624. totalDuration = 2.0 * (delayAmount + interval);
  625. keyTimes = @[
  626. @(0.0), // 1) Initial gradient
  627. @(delayAmount/totalDuration), // 2) Begin of LE fade-in, just as scroll away starts
  628. @((delayAmount + 0.4)/totalDuration), // 3) End of LE fade in [LE fully faded]
  629. @((delayAmount + interval - 0.4)/totalDuration), // 4) Begin of TE fade out, just before scroll away finishes
  630. @((delayAmount + interval)/totalDuration), // 5) End of TE fade out [TE fade removed]
  631. @((delayAmount + interval + delayAmount)/totalDuration), // 6) Begin of TE fade back in, just as scroll home starts
  632. @((delayAmount + interval + delayAmount + 0.4)/totalDuration), // 7) End of TE fade back in [TE fully faded]
  633. @((totalDuration - 0.4)/totalDuration), // 8) Begin of LE fade out, just before scroll home finishes
  634. @(1.0)]; // 9) End of LE fade out, just as scroll home finishes
  635. break;
  636. case MLContinuousReverse:
  637. default:
  638. // Calculate total animation duration
  639. totalDuration = delayAmount + interval;
  640. // Find when the lead label will be totally offscreen
  641. CGFloat startFadeFraction = fabs((self.subLabel.bounds.size.width + self.leadingBuffer) / self.awayOffset);
  642. // Find when the animation will hit that point
  643. CGFloat startFadeTimeFraction = [timingFunction durationPercentageForPositionPercentage:startFadeFraction withDuration:totalDuration];
  644. NSTimeInterval startFadeTime = delayAmount + startFadeTimeFraction * interval;
  645. keyTimes = @[
  646. @(0.0), // Initial gradient
  647. @(delayAmount/totalDuration), // Begin of fade in
  648. @((delayAmount + 0.2)/totalDuration), // End of fade in, just as scroll away starts
  649. @((startFadeTime)/totalDuration), // Begin of fade out, just before scroll home completes
  650. @((startFadeTime + 0.1)/totalDuration), // End of fade out, as scroll home completes
  651. @(1.0) // Buffer final value (used on continuous types)
  652. ];
  653. break;
  654. }
  655. // Define gradient values
  656. switch (self.marqueeType) {
  657. case MLContinuousReverse:
  658. values = @[
  659. @[transp, opaque, opaque, opaque], // Initial gradient
  660. @[transp, opaque, opaque, opaque], // Begin of fade in
  661. @[transp, opaque, opaque, transp], // End of fade in, just as scroll away starts
  662. @[transp, opaque, opaque, transp], // Begin of fade out, just before scroll home completes
  663. @[transp, opaque, opaque, opaque], // End of fade out, as scroll home completes
  664. @[transp, opaque, opaque, opaque] // Final "home" value
  665. ];
  666. break;
  667. case MLRightLeft:
  668. values = @[
  669. @[transp, opaque, opaque, opaque], // 1)
  670. @[transp, opaque, opaque, opaque], // 2)
  671. @[transp, opaque, opaque, transp], // 3)
  672. @[transp, opaque, opaque, transp], // 4)
  673. @[opaque, opaque, opaque, transp], // 5)
  674. @[opaque, opaque, opaque, transp], // 6)
  675. @[transp, opaque, opaque, transp], // 7)
  676. @[transp, opaque, opaque, transp], // 8)
  677. @[transp, opaque, opaque, opaque] // 9)
  678. ];
  679. break;
  680. case MLContinuous:
  681. values = @[
  682. @[opaque, opaque, opaque, transp], // Initial gradient
  683. @[opaque, opaque, opaque, transp], // Begin of fade in
  684. @[transp, opaque, opaque, transp], // End of fade in, just as scroll away starts
  685. @[transp, opaque, opaque, transp], // Begin of fade out, just before scroll home completes
  686. @[opaque, opaque, opaque, transp], // End of fade out, as scroll home completes
  687. @[opaque, opaque, opaque, transp] // Final "home" value
  688. ];
  689. break;
  690. case MLLeftRight:
  691. default:
  692. values = @[
  693. @[opaque, opaque, opaque, transp], // 1)
  694. @[opaque, opaque, opaque, transp], // 2)
  695. @[transp, opaque, opaque, transp], // 3)
  696. @[transp, opaque, opaque, transp], // 4)
  697. @[transp, opaque, opaque, opaque], // 5)
  698. @[transp, opaque, opaque, opaque], // 6)
  699. @[transp, opaque, opaque, transp], // 7)
  700. @[transp, opaque, opaque, transp], // 8)
  701. @[opaque, opaque, opaque, transp] // 9)
  702. ];
  703. break;
  704. }
  705. animation.values = values;
  706. animation.keyTimes = keyTimes;
  707. animation.timingFunctions = @[timingFunction, timingFunction, timingFunction, timingFunction];
  708. return animation;
  709. }
  710. - (CAKeyframeAnimation *)keyFrameAnimationForProperty:(NSString *)property
  711. values:(NSArray *)values
  712. interval:(NSTimeInterval)interval
  713. delay:(NSTimeInterval)delayAmount
  714. {
  715. // Create new animation
  716. CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:property];
  717. // Get timing function
  718. CAMediaTimingFunction *timingFunction = [self timingFunctionForAnimationOptions:self.animationCurve];
  719. // Calculate times based on marqueeType
  720. NSTimeInterval totalDuration;
  721. switch (self.marqueeType) {
  722. case MLLeftRight:
  723. case MLRightLeft:
  724. NSAssert(values.count == 5, @"Incorrect number of values passed for MLLeftRight-type animation");
  725. totalDuration = 2.0 * (delayAmount + interval);
  726. // Set up keyTimes
  727. animation.keyTimes = @[@(0.0), // Initial location, home
  728. @(delayAmount/totalDuration), // Initial delay, at home
  729. @((delayAmount + interval)/totalDuration), // Animation to away
  730. @((delayAmount + interval + delayAmount)/totalDuration), // Delay at away
  731. @(1.0)]; // Animation to home
  732. animation.timingFunctions = @[timingFunction,
  733. timingFunction,
  734. timingFunction,
  735. timingFunction];
  736. break;
  737. // MLContinuous
  738. // MLContinuousReverse
  739. default:
  740. NSAssert(values.count == 3, @"Incorrect number of values passed for MLContinous-type animation");
  741. totalDuration = delayAmount + interval;
  742. // Set up keyTimes
  743. animation.keyTimes = @[@(0.0), // Initial location, home
  744. @(delayAmount/totalDuration), // Initial delay, at home
  745. @(1.0)]; // Animation to away
  746. animation.timingFunctions = @[timingFunction,
  747. timingFunction];
  748. break;
  749. }
  750. // Set values
  751. animation.values = values;
  752. animation.delegate = self;
  753. return animation;
  754. }
  755. - (CAMediaTimingFunction *)timingFunctionForAnimationOptions:(UIViewAnimationOptions)animationOptions {
  756. NSString *timingFunction;
  757. switch (animationOptions) {
  758. case UIViewAnimationOptionCurveEaseIn:
  759. timingFunction = kCAMediaTimingFunctionEaseIn;
  760. break;
  761. case UIViewAnimationOptionCurveEaseInOut:
  762. timingFunction = kCAMediaTimingFunctionEaseInEaseOut;
  763. break;
  764. case UIViewAnimationOptionCurveEaseOut:
  765. timingFunction = kCAMediaTimingFunctionEaseOut;
  766. break;
  767. default:
  768. timingFunction = kCAMediaTimingFunctionLinear;
  769. break;
  770. }
  771. return [CAMediaTimingFunction functionWithName:timingFunction];
  772. }
  773. - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
  774. MLAnimationCompletionBlock completionBlock = [anim valueForKey:kMarqueeLabelAnimationCompletionBlock];
  775. if (completionBlock) {
  776. completionBlock(flag);
  777. }
  778. }
  779. #pragma mark - Label Control
  780. - (void)restartLabel {
  781. // Shutdown the label
  782. [self shutdownLabel];
  783. // Restart scrolling if appropriate
  784. if (self.labelShouldScroll && !self.tapToScroll && !self.holdScrolling) {
  785. [self beginScroll];
  786. }
  787. }
  788. - (void)resetLabel {
  789. [self returnLabelToOriginImmediately];
  790. self.homeLabelFrame = CGRectNull;
  791. self.awayOffset = 0.0f;
  792. }
  793. - (void)shutdownLabel {
  794. // Bring label to home location
  795. [self returnLabelToOriginImmediately];
  796. // Apply gradient mask for home location
  797. [self applyGradientMaskForFadeLength:self.fadeLength animated:false];
  798. }
  799. -(void)pauseLabel
  800. {
  801. // Only pause if label is not already paused, and already in a scrolling animation
  802. if (!self.isPaused && self.awayFromHome) {
  803. // Pause sublabel position animation
  804. CFTimeInterval labelPauseTime = [self.subLabel.layer convertTime:CACurrentMediaTime() fromLayer:nil];
  805. self.subLabel.layer.speed = 0.0;
  806. self.subLabel.layer.timeOffset = labelPauseTime;
  807. // Pause gradient fade animation
  808. CFTimeInterval gradientPauseTime = [self.layer.mask convertTime:CACurrentMediaTime() fromLayer:nil];
  809. self.layer.mask.speed = 0.0;
  810. self.layer.mask.timeOffset = gradientPauseTime;
  811. self.isPaused = YES;
  812. }
  813. }
  814. -(void)unpauseLabel
  815. {
  816. if (self.isPaused) {
  817. // Unpause sublabel position animation
  818. CFTimeInterval labelPausedTime = self.subLabel.layer.timeOffset;
  819. self.subLabel.layer.speed = 1.0;
  820. self.subLabel.layer.timeOffset = 0.0;
  821. self.subLabel.layer.beginTime = 0.0;
  822. self.subLabel.layer.beginTime = [self.subLabel.layer convertTime:CACurrentMediaTime() fromLayer:nil] - labelPausedTime;
  823. // Unpause gradient fade animation
  824. CFTimeInterval gradientPauseTime = self.layer.mask.timeOffset;
  825. self.layer.mask.speed = 1.0;
  826. self.layer.mask.timeOffset = 0.0;
  827. self.layer.mask.beginTime = 0.0;
  828. self.layer.mask.beginTime = [self.layer.mask convertTime:CACurrentMediaTime() fromLayer:nil] - gradientPauseTime;
  829. self.isPaused = NO;
  830. }
  831. }
  832. - (void)labelWasTapped:(UITapGestureRecognizer *)recognizer {
  833. if (self.labelShouldScroll && !self.awayFromHome) {
  834. [self beginScrollWithDelay:NO];
  835. }
  836. }
  837. - (void)triggerScrollStart {
  838. if (self.labelShouldScroll && !self.awayFromHome) {
  839. [self beginScroll];
  840. }
  841. }
  842. - (void)labelWillBeginScroll {
  843. // Default implementation does nothing
  844. return;
  845. }
  846. - (void)labelReturnedToHome:(BOOL)finished {
  847. // Default implementation does nothing
  848. return;
  849. }
  850. #pragma mark - Modified UIView Methods/Getters/Setters
  851. - (void)setFrame:(CGRect)frame {
  852. [super setFrame:frame];
  853. // Check if device is running iOS 8.0.X
  854. if(SYSTEM_VERSION_IS_8_0_X) {
  855. // If so, force update because layoutSubviews is not called
  856. [self updateSublabel];
  857. }
  858. }
  859. - (void)setBounds:(CGRect)bounds {
  860. [super setBounds:bounds];
  861. // Check if device is running iOS 8.0.X
  862. if(SYSTEM_VERSION_IS_8_0_X) {
  863. // If so, force update because layoutSubviews is not called
  864. [self updateSublabel];
  865. }
  866. }
  867. #pragma mark - Modified UILabel Methods/Getters/Setters
  868. - (UIView *)viewForBaselineLayout {
  869. // Use subLabel view for handling baseline layouts
  870. return self.subLabel;
  871. }
  872. - (UIView *)viewForLastBaselineLayout {
  873. // Use subLabel view for handling baseline layouts
  874. return self.subLabel;
  875. }
  876. - (UIView *)viewForFirstBaselineLayout {
  877. // Use subLabel view for handling baseline layouts
  878. return self.subLabel;
  879. }
  880. - (NSString *)text {
  881. return self.subLabel.text;
  882. }
  883. - (void)setText:(NSString *)text {
  884. if ([text isEqualToString:self.subLabel.text]) {
  885. return;
  886. }
  887. self.subLabel.text = text;
  888. super.text = text;
  889. [self updateSublabel];
  890. }
  891. - (NSAttributedString *)attributedText {
  892. return self.subLabel.attributedText;
  893. }
  894. - (void)setAttributedText:(NSAttributedString *)attributedText {
  895. if ([attributedText isEqualToAttributedString:self.subLabel.attributedText]) {
  896. return;
  897. }
  898. self.subLabel.attributedText = attributedText;
  899. super.attributedText = attributedText;
  900. [self updateSublabel];
  901. }
  902. - (UIFont *)font {
  903. return self.subLabel.font;
  904. }
  905. - (void)setFont:(UIFont *)font {
  906. if ([font isEqual:self.subLabel.font]) {
  907. return;
  908. }
  909. self.subLabel.font = font;
  910. super.font = font;
  911. [self updateSublabel];
  912. }
  913. - (UIColor *)textColor {
  914. return self.subLabel.textColor;
  915. }
  916. - (void)setTextColor:(UIColor *)textColor {
  917. self.subLabel.textColor = textColor;
  918. super.textColor = textColor;
  919. }
  920. - (UIColor *)backgroundColor {
  921. return self.subLabel.backgroundColor;
  922. }
  923. - (void)setBackgroundColor:(UIColor *)backgroundColor {
  924. self.subLabel.backgroundColor = backgroundColor;
  925. super.backgroundColor = backgroundColor;
  926. }
  927. - (UIColor *)shadowColor {
  928. return self.subLabel.shadowColor;
  929. }
  930. - (void)setShadowColor:(UIColor *)shadowColor {
  931. self.subLabel.shadowColor = shadowColor;
  932. super.shadowColor = shadowColor;
  933. }
  934. - (CGSize)shadowOffset {
  935. return self.subLabel.shadowOffset;
  936. }
  937. - (void)setShadowOffset:(CGSize)shadowOffset {
  938. self.subLabel.shadowOffset = shadowOffset;
  939. super.shadowOffset = shadowOffset;
  940. }
  941. - (UIColor *)highlightedTextColor {
  942. return self.subLabel.highlightedTextColor;
  943. }
  944. - (void)setHighlightedTextColor:(UIColor *)highlightedTextColor {
  945. self.subLabel.highlightedTextColor = highlightedTextColor;
  946. super.highlightedTextColor = highlightedTextColor;
  947. }
  948. - (BOOL)isHighlighted {
  949. return self.subLabel.isHighlighted;
  950. }
  951. - (void)setHighlighted:(BOOL)highlighted {
  952. self.subLabel.highlighted = highlighted;
  953. super.highlighted = highlighted;
  954. }
  955. - (BOOL)isEnabled {
  956. return self.subLabel.isEnabled;
  957. }
  958. - (void)setEnabled:(BOOL)enabled {
  959. self.subLabel.enabled = enabled;
  960. super.enabled = enabled;
  961. }
  962. - (void)setNumberOfLines:(NSInteger)numberOfLines {
  963. // By the nature of MarqueeLabel, this is 1
  964. [super setNumberOfLines:1];
  965. }
  966. - (void)setAdjustsFontSizeToFitWidth:(BOOL)adjustsFontSizeToFitWidth {
  967. // By the nature of MarqueeLabel, this is NO
  968. [super setAdjustsFontSizeToFitWidth:NO];
  969. }
  970. - (void)setMinimumFontSize:(CGFloat)minimumFontSize {
  971. [super setMinimumFontSize:0.0];
  972. }
  973. - (UIBaselineAdjustment)baselineAdjustment {
  974. return self.subLabel.baselineAdjustment;
  975. }
  976. - (void)setBaselineAdjustment:(UIBaselineAdjustment)baselineAdjustment {
  977. self.subLabel.baselineAdjustment = baselineAdjustment;
  978. super.baselineAdjustment = baselineAdjustment;
  979. }
  980. - (CGSize)intrinsicContentSize {
  981. CGSize contentSize = self.subLabel.intrinsicContentSize;
  982. contentSize.width += self.leadingBuffer;
  983. return contentSize;
  984. }
  985. - (void)setAdjustsLetterSpacingToFitWidth:(BOOL)adjustsLetterSpacingToFitWidth {
  986. // By the nature of MarqueeLabel, this is NO
  987. [super setAdjustsLetterSpacingToFitWidth:NO];
  988. }
  989. - (void)setMinimumScaleFactor:(CGFloat)minimumScaleFactor {
  990. [super setMinimumScaleFactor:0.0f];
  991. }
  992. #pragma mark - Custom Getters and Setters
  993. - (void)setRate:(CGFloat)rate {
  994. if (_rate == rate) {
  995. return;
  996. }
  997. _scrollDuration = 0.0f;
  998. _rate = rate;
  999. [self updateSublabel];
  1000. }
  1001. - (void)setScrollDuration:(CGFloat)lengthOfScroll {
  1002. if (_scrollDuration == lengthOfScroll) {
  1003. return;
  1004. }
  1005. _rate = 0.0f;
  1006. _scrollDuration = lengthOfScroll;
  1007. [self updateSublabel];
  1008. }
  1009. - (void)setAnimationCurve:(UIViewAnimationOptions)animationCurve {
  1010. if (_animationCurve == animationCurve) {
  1011. return;
  1012. }
  1013. NSUInteger allowableOptions = UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionCurveLinear;
  1014. if ((allowableOptions & animationCurve) == animationCurve) {
  1015. _animationCurve = animationCurve;
  1016. }
  1017. }
  1018. - (void)setLeadingBuffer:(CGFloat)leadingBuffer {
  1019. if (_leadingBuffer == leadingBuffer) {
  1020. return;
  1021. }
  1022. // Do not allow negative values
  1023. _leadingBuffer = fabs(leadingBuffer);
  1024. [self updateSublabel];
  1025. }
  1026. - (void)setTrailingBuffer:(CGFloat)trailingBuffer {
  1027. if (_trailingBuffer == trailingBuffer) {
  1028. return;
  1029. }
  1030. // Do not allow negative values
  1031. _trailingBuffer = fabs(trailingBuffer);
  1032. [self updateSublabel];
  1033. }
  1034. - (void)setContinuousMarqueeExtraBuffer:(CGFloat)continuousMarqueeExtraBuffer {
  1035. [self setTrailingBuffer:continuousMarqueeExtraBuffer];
  1036. }
  1037. - (CGFloat)continuousMarqueeExtraBuffer {
  1038. return self.trailingBuffer;
  1039. }
  1040. - (void)setFadeLength:(CGFloat)fadeLength {
  1041. if (_fadeLength == fadeLength) {
  1042. return;
  1043. }
  1044. _fadeLength = fadeLength;
  1045. [self updateSublabel];
  1046. }
  1047. - (void)setTapToScroll:(BOOL)tapToScroll {
  1048. if (_tapToScroll == tapToScroll) {
  1049. return;
  1050. }
  1051. _tapToScroll = tapToScroll;
  1052. if (_tapToScroll) {
  1053. UITapGestureRecognizer *newTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(labelWasTapped:)];
  1054. [self addGestureRecognizer:newTapRecognizer];
  1055. self.tapRecognizer = newTapRecognizer;
  1056. self.userInteractionEnabled = YES;
  1057. } else {
  1058. [self removeGestureRecognizer:self.tapRecognizer];
  1059. self.tapRecognizer = nil;
  1060. self.userInteractionEnabled = NO;
  1061. }
  1062. }
  1063. - (void)setMarqueeType:(MarqueeType)marqueeType {
  1064. if (marqueeType == _marqueeType) {
  1065. return;
  1066. }
  1067. _marqueeType = marqueeType;
  1068. [self updateSublabel];
  1069. }
  1070. - (void)setLabelize:(BOOL)labelize {
  1071. if (_labelize == labelize) {
  1072. return;
  1073. }
  1074. _labelize = labelize;
  1075. [self updateSublabelAndBeginScroll:YES];
  1076. }
  1077. - (void)setHoldScrolling:(BOOL)holdScrolling {
  1078. if (_holdScrolling == holdScrolling) {
  1079. return;
  1080. }
  1081. _holdScrolling = holdScrolling;
  1082. if (!holdScrolling && !(self.awayFromHome || self.labelize || self.tapToScroll) && self.labelShouldScroll) {
  1083. [self beginScroll];
  1084. }
  1085. }
  1086. - (BOOL)awayFromHome {
  1087. CALayer *presentationLayer = self.subLabel.layer.presentationLayer;
  1088. if (!presentationLayer) {
  1089. return NO;
  1090. }
  1091. return !(presentationLayer.position.x == self.homeLabelFrame.origin.x);
  1092. }
  1093. #pragma mark - Support
  1094. - (NSArray *)gradientColors {
  1095. if (!_gradientColors) {
  1096. NSObject *transparent = (NSObject *)[[UIColor clearColor] CGColor];
  1097. NSObject *opaque = (NSObject *)[[UIColor blackColor] CGColor];
  1098. _gradientColors = [NSArray arrayWithObjects: transparent, opaque, opaque, transparent, nil];
  1099. }
  1100. return _gradientColors;
  1101. }
  1102. #pragma mark -
  1103. - (void)dealloc {
  1104. [[NSNotificationCenter defaultCenter] removeObserver:self];
  1105. }
  1106. @end
  1107. #pragma mark - Helpers
  1108. CGPoint MLOffsetCGPoint(CGPoint point, CGFloat offset) {
  1109. return CGPointMake(point.x + offset, point.y);
  1110. }
  1111. @implementation UIView (MarqueeLabelHelpers)
  1112. // Thanks to Phil M
  1113. // http://stackoverflow.com/questions/1340434/get-to-uiviewcontroller-from-uiview-on-iphone
  1114. - (id)firstAvailableViewController
  1115. {
  1116. // convenience function for casting and to "mask" the recursive function
  1117. return [self traverseResponderChainForFirstViewController];
  1118. }
  1119. - (id)traverseResponderChainForFirstViewController
  1120. {
  1121. id nextResponder = [self nextResponder];
  1122. if ([nextResponder isKindOfClass:[UIViewController class]]) {
  1123. return nextResponder;
  1124. } else if ([nextResponder isKindOfClass:[UIView class]]) {
  1125. return [nextResponder traverseResponderChainForFirstViewController];
  1126. } else {
  1127. return nil;
  1128. }
  1129. }
  1130. @end
  1131. @implementation CAMediaTimingFunction (MarqueeLabelHelpers)
  1132. - (CGFloat)durationPercentageForPositionPercentage:(CGFloat)positionPercentage withDuration:(NSTimeInterval)duration
  1133. {
  1134. // Finds the animation duration percentage that corresponds with the given animation "position" percentage.
  1135. // Utilizes Newton's Method to solve for the parametric Bezier curve that is used by CAMediaAnimation.
  1136. NSArray *controlPoints = [self controlPoints];
  1137. CGFloat epsilon = 1.0f / (100.0f * duration);
  1138. // Find the t value that gives the position percentage we want
  1139. CGFloat t_found = [self solveTForY:positionPercentage
  1140. withEpsilon:epsilon
  1141. controlPoints:controlPoints];
  1142. // With that t, find the corresponding animation percentage
  1143. CGFloat durationPercentage = [self XforCurveAt:t_found withControlPoints:controlPoints];
  1144. return durationPercentage;
  1145. }
  1146. - (CGFloat)solveTForY:(CGFloat)y_0 withEpsilon:(CGFloat)epsilon controlPoints:(NSArray *)controlPoints
  1147. {
  1148. // Use Newton's Method: http://en.wikipedia.org/wiki/Newton's_method
  1149. // For first guess, use t = y (i.e. if curve were linear)
  1150. CGFloat t0 = y_0;
  1151. CGFloat t1 = y_0;
  1152. CGFloat f0, df0;
  1153. for (int i = 0; i < 15; i++) {
  1154. // Base this iteration of t1 calculated from last iteration
  1155. t0 = t1;
  1156. // Calculate f(t0)
  1157. f0 = [self YforCurveAt:t0 withControlPoints:controlPoints] - y_0;
  1158. // Check if this is close (enough)
  1159. if (fabs(f0) < epsilon) {
  1160. // Done!
  1161. return t0;
  1162. }
  1163. // Else continue Newton's Method
  1164. df0 = [self derivativeYValueForCurveAt:t0 withControlPoints:controlPoints];
  1165. // Check if derivative is small or zero ( http://en.wikipedia.org/wiki/Newton's_method#Failure_analysis )
  1166. if (fabs(df0) < 1e-6) {
  1167. NSLog(@"MarqueeLabel: Newton's Method failure, small/zero derivative!");
  1168. break;
  1169. }
  1170. // Else recalculate t1
  1171. t1 = t0 - f0/df0;
  1172. }
  1173. NSLog(@"MarqueeLabel: Failed to find t for Y input!");
  1174. return t0;
  1175. }
  1176. - (CGFloat)YforCurveAt:(CGFloat)t withControlPoints:(NSArray *)controlPoints
  1177. {
  1178. CGPoint P0 = [controlPoints[0] CGPointValue];
  1179. CGPoint P1 = [controlPoints[1] CGPointValue];
  1180. CGPoint P2 = [controlPoints[2] CGPointValue];
  1181. CGPoint P3 = [controlPoints[3] CGPointValue];
  1182. // Per http://en.wikipedia.org/wiki/Bezier_curve#Cubic_B.C3.A9zier_curves
  1183. return powf((1 - t),3) * P0.y +
  1184. 3.0f * powf(1 - t, 2) * t * P1.y +
  1185. 3.0f * (1 - t) * powf(t, 2) * P2.y +
  1186. powf(t, 3) * P3.y;
  1187. }
  1188. - (CGFloat)XforCurveAt:(CGFloat)t withControlPoints:(NSArray *)controlPoints
  1189. {
  1190. CGPoint P0 = [controlPoints[0] CGPointValue];
  1191. CGPoint P1 = [controlPoints[1] CGPointValue];
  1192. CGPoint P2 = [controlPoints[2] CGPointValue];
  1193. CGPoint P3 = [controlPoints[3] CGPointValue];
  1194. // Per http://en.wikipedia.org/wiki/Bezier_curve#Cubic_B.C3.A9zier_curves
  1195. return powf((1 - t),3) * P0.x +
  1196. 3.0f * powf(1 - t, 2) * t * P1.x +
  1197. 3.0f * (1 - t) * powf(t, 2) * P2.x +
  1198. powf(t, 3) * P3.x;
  1199. }
  1200. - (CGFloat)derivativeYValueForCurveAt:(CGFloat)t withControlPoints:(NSArray *)controlPoints
  1201. {
  1202. CGPoint P0 = [controlPoints[0] CGPointValue];
  1203. CGPoint P1 = [controlPoints[1] CGPointValue];
  1204. CGPoint P2 = [controlPoints[2] CGPointValue];
  1205. CGPoint P3 = [controlPoints[3] CGPointValue];
  1206. return powf(t, 2) * (-3.0f * P0.y - 9.0f * P1.y - 9.0f * P2.y + 3.0f * P3.y) +
  1207. t * (6.0f * P0.y + 6.0f * P2.y) +
  1208. (-3.0f * P0.y + 3.0f * P1.y);
  1209. }
  1210. - (NSArray *)controlPoints
  1211. {
  1212. float point[2];
  1213. NSMutableArray *pointArray = [NSMutableArray array];
  1214. for (int i = 0; i <= 3; i++) {
  1215. [self getControlPointAtIndex:i values:point];
  1216. [pointArray addObject:[NSValue valueWithCGPoint:CGPointMake(point[0], point[1])]];
  1217. }
  1218. return [NSArray arrayWithArray:pointArray];
  1219. }
  1220. @end