口袋优选

MarqueeLabel.m 59KB

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