From bf3f5dd1417685cb3450a4fceb44995e7f5d22ae Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 10 Jan 2018 17:51:05 -0500 Subject: [PATCH] Respond to CR. --- Signal/src/views/MarqueeLabel.swift | 521 ++++++++++++++-------------- 1 file changed, 269 insertions(+), 252 deletions(-) diff --git a/Signal/src/views/MarqueeLabel.swift b/Signal/src/views/MarqueeLabel.swift index 13b4508f7..5b082c61a 100644 --- a/Signal/src/views/MarqueeLabel.swift +++ b/Signal/src/views/MarqueeLabel.swift @@ -1,5 +1,10 @@ +// Grabbed from: https://github.com/cbpowell/MarqueeLabel-Swift/blob/cd331f3cfc3f9d7114ffa5aa4f243f1d5eda9d0d/Classes/MarqueeLabel.swift +// License: MIT License // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// MarqueeLabel.swift +// +// Created by Charles Powell on 8/6/14. +// Copyright (c) 2015 Charles Powell. All rights reserved. // import UIKit @@ -8,7 +13,7 @@ import QuartzCore @IBDesignable open class MarqueeLabel: UILabel, CAAnimationDelegate { - + /** An enum that defines the types of `MarqueeLabel` scrolling @@ -27,11 +32,11 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { case continuous case continuousReverse } - + // // MARK: - Public properties // - + /** Defines the direction and method in which the `MarqueeLabel` instance scrolls. `MarqueeLabel` supports six default types of scrolling: `Left`, `LeftRight`, `Right`, `RightLeft`, `Continuous`, and `ContinuousReverse`. @@ -56,7 +61,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { updateAndScroll() } } - + /** An optional custom scroll "sequence", defined by an array of `ScrollStep` or `FadeStep` instances. A sequence defines a single scroll/animation loop, which will continue to be automatically repeated like the default types. @@ -80,7 +85,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { - SeeAlso: FadeStep */ open var scrollSequence: Array? - + /** Specifies the animation curve used in the scrolling motion of the labels. Allowable options: @@ -93,7 +98,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { Defaults to `UIViewAnimationOptionCurveEaseInOut`. */ open var animationCurve: UIViewAnimationCurve = .linear - + /** A boolean property that sets whether the `MarqueeLabel` should behave like a normal `UILabel`. @@ -119,7 +124,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + /** A boolean property that sets whether the `MarqueeLabel` should hold (prevent) automatic label scrolling. @@ -142,7 +147,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + /** A boolean property that sets whether the `MarqueeLabel` should only begin a scroll when tapped. @@ -170,7 +175,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + /** A read-only boolean property that indicates if the label's scroll animation has been paused. @@ -180,7 +185,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { open var isPaused: Bool { return (sublabel.layer.speed == 0.0) } - + /** A boolean property that indicates if the label is currently away from the home location. @@ -190,10 +195,10 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { if let presentationLayer = sublabel.layer.presentation() { return !(presentationLayer.position.x == homeLabelFrame.origin.x) } - + return false } - + /** The `MarqueeLabel` scrolling speed may be defined by one of two ways: - Rate(CGFloat): The speed is defined by a rate of motion, in units of points per second. @@ -204,7 +209,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { public enum SpeedLimit { case rate(CGFloat) case duration(CGFloat) - + var value: CGFloat { switch self { case .rate(let rate): @@ -214,7 +219,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + /** Defines the speed of the `MarqueeLabel` scrolling animation. @@ -234,7 +239,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + @available(*, deprecated : 2.6, message : "Use speed property instead") @IBInspectable open var scrollDuration: CGFloat { get { @@ -247,7 +252,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { speed = .duration(newValue) } } - + @available(*, deprecated : 2.6, message : "Use speed property instead") @IBInspectable open var scrollRate: CGFloat { get { @@ -260,7 +265,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { speed = .rate(newValue) } } - + /** A buffer (offset) between the leading edge of the label text and the label frame. @@ -284,7 +289,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + /** A buffer (offset) between the trailing edge of the label text and the label frame. @@ -308,7 +313,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + /** The length of transparency fade at the left and right edges of the frame. @@ -326,23 +331,25 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + + /** The length of delay in seconds that the label pauses at the completion of a scroll. */ @IBInspectable open var animationDelay: CGFloat = 1.0 - + + /** The read-only duration of the scroll animation (not including delay). The value of this property is calculated from the value set to the `speed` property. If a .duration value is used to set the label animation speed, this value will be equivalent. */ private(set) public var animationDuration: CGFloat = 0.0 - + // // MARK: - Class Functions and Helpers // - + /** Convenience method to restart all `MarqueeLabel` instances that have the specified view controller in their next responder chain. @@ -361,7 +368,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { open class func restartLabelsOfController(_ controller: UIViewController) { MarqueeLabel.notifyController(controller, message: .Restart) } - + /** Convenience method to restart all `MarqueeLabel` instances that have the specified view controller in their next responder chain. @@ -374,7 +381,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { open class func controllerViewWillAppear(_ controller: UIViewController) { MarqueeLabel.restartLabelsOfController(controller) } - + /** Convenience method to restart all `MarqueeLabel` instances that have the specified view controller in their next responder chain. @@ -387,7 +394,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { open class func controllerViewDidAppear(_ controller: UIViewController) { MarqueeLabel.restartLabelsOfController(controller) } - + /** Labelizes all `MarqueeLabel` instances that have the specified view controller in their next responder chain. @@ -399,7 +406,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { open class func controllerLabelsLabelize(_ controller: UIViewController) { MarqueeLabel.notifyController(controller, message: .Labelize) } - + /** De-labelizes all `MarqueeLabel` instances that have the specified view controller in their next responder chain. @@ -412,10 +419,11 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { MarqueeLabel.notifyController(controller, message: .Animate) } + // // MARK: - Initialization // - + /** Returns a newly initialized `MarqueeLabel` instance with the specified scroll rate and edge transparency fade length. @@ -431,7 +439,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { super.init(frame: frame) setup() } - + /** Returns a newly initialized `MarqueeLabel` instance with the specified scroll rate and edge transparency fade length. @@ -447,12 +455,12 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { super.init(frame: frame) setup() } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + /** Returns a newly initialized `MarqueeLabel` instance. @@ -464,7 +472,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { convenience public override init(frame: CGRect) { self.init(frame: frame, duration:7.0, fadeLength:0.0) } - + private func setup() { // Create sublabel sublabel = UILabel(frame: self.bounds) @@ -473,11 +481,11 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { // Add sublabel addSubview(sublabel) - + // Configure self super.clipsToBounds = true super.numberOfLines = 1 - + // Add notification observers // Custom class notifications NotificationCenter.default.addObserver(self, selector: #selector(MarqueeLabel.restartForViewController(_:)), name: NSNotification.Name(rawValue: MarqueeKeys.Restart.rawValue), object: nil) @@ -487,32 +495,32 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { NotificationCenter.default.addObserver(self, selector: #selector(MarqueeLabel.restartLabel), name: NSNotification.Name.OWSApplicationDidBecomeActive, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MarqueeLabel.shutdownLabel), name: NSNotification.Name.OWSApplicationDidEnterBackground, object: nil) } - + override open func awakeFromNib() { super.awakeFromNib() forwardPropertiesToSublabel() } - + @available(iOS 8.0, *) override open func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() forwardPropertiesToSublabel() } - + private func forwardPropertiesToSublabel() { /* Note that this method is currently ONLY called from awakeFromNib, i.e. when text properties are set via a Storyboard. As the Storyboard/IB doesn't currently support attributed strings, there's no need to "forward" the super attributedString value. */ - + // Since we're a UILabel, we actually do implement all of UILabel's properties. // We don't care about these values, we just want to forward them on to our sublabel. let properties = ["baselineAdjustment", "enabled", "highlighted", "highlightedTextColor", "minimumFontSize", "shadowOffset", "textAlignment", "userInteractionEnabled", "adjustsFontSizeToFitWidth", "lineBreakMode", "numberOfLines", "contentMode"] - + // Iterate through properties sublabel.text = super.text sublabel.font = super.font @@ -525,14 +533,14 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { sublabel.setValue(value, forKeyPath: prop) } } - + // // MARK: - MarqueeLabel Heavy Lifting // override open func layoutSubviews() { super.layoutSubviews() - + updateAndScroll(true) } @@ -541,7 +549,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { shutdownLabel() } } - + override open func didMoveToWindow() { if self.window == nil { shutdownLabel() @@ -549,33 +557,33 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { updateAndScroll() } } - + private func updateAndScroll() { updateAndScroll(true) } - + private func updateAndScroll(_ shouldBeginScroll: Bool) { // Check if scrolling can occur if !labelReadyForScroll() { return } - + // Calculate expected size let expectedLabelSize = sublabelSize() - + // Invalidate intrinsic size invalidateIntrinsicContentSize() - + // Move label to home returnLabelToHome() - + // Check if label should scroll // Note that the holdScrolling propery does not affect this if !labelShouldScroll() { // Set text alignment and break mode to act like a normal label sublabel.textAlignment = super.textAlignment sublabel.lineBreakMode = super.lineBreakMode - + let labelFrame: CGRect switch type { case .continuousReverse, .rightLeft: @@ -583,24 +591,24 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { default: labelFrame = CGRect(x: leadingBuffer, y: 0.0, width: bounds.size.width - leadingBuffer, height: bounds.size.height).integral } - + homeLabelFrame = labelFrame awayOffset = 0.0 - + // Remove an additional sublabels (for continuous types) - repliLayer?.instanceCount = 1 - + repliLayer?.instanceCount = 1; + // Set the sublabel frame to calculated labelFrame sublabel.frame = labelFrame - + // Remove fade, as by definition none is needed in this case removeGradientMask() - + return } - + // Label DOES need to scroll - + // Recompute the animation duration animationDuration = { switch self.speed { @@ -610,13 +618,13 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { return duration } }() - + // Spacing between primary and second sublabel must be at least equal to leadingBuffer, and at least equal to the fadeLength let minTrailing = max(max(leadingBuffer, trailingBuffer), fadeLength) - + // Determine positions and generate scroll steps let sequence: [MarqueeStep] - + switch type { case .continuous, .continuousReverse: if (type == .continuous) { @@ -626,7 +634,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { homeLabelFrame = CGRect(x: bounds.size.width - (expectedLabelSize.width + leadingBuffer), y: 0.0, width: expectedLabelSize.width, height: bounds.size.height).integral awayOffset = (homeLabelFrame.size.width + minTrailing) } - + // Find when the lead label will be totally offscreen let offsetDistance = awayOffset let offscreenAmount = homeLabelFrame.size.width @@ -634,7 +642,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { // Find when the animation will hit that point let startFadeTimeFraction = timingFunctionForAnimationCurve(animationCurve).durationPercentageForPositionPercentage(startFadeFraction, duration: (animationDelay + animationDuration)) let startFadeTime = startFadeTimeFraction * animationDuration - + sequence = scrollSequence ?? [ ScrollStep(timeStep: 0.0, position: .home, edgeFades: .trailing), // Starting point, at home, with trailing fade ScrollStep(timeStep: animationDelay, position: .home, edgeFades: .trailing), // Delay at home, maintaining fade state @@ -644,14 +652,14 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { ScrollStep(timeStep: animationDuration, timingFunction: animationCurve, // Ending point (back at home), with animationCurve transition, with trailing fade position: .away, edgeFades: .trailing) ] - + // Set frame and text sublabel.frame = homeLabelFrame - + // Configure replication repliLayer?.instanceCount = 2 repliLayer?.instanceTransform = CATransform3DMakeTranslation(-awayOffset, 0.0, 0.0) - + case .leftRight, .left, .rightLeft, .right: if (type == .leftRight || type == .left) { homeLabelFrame = CGRect(x: leadingBuffer, y: 0.0, width: expectedLabelSize.width, height: bounds.size.height).integral @@ -666,10 +674,10 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } // Set frame and text sublabel.frame = homeLabelFrame - + // Remove any replication repliLayer?.instanceCount = 1 - + if (type == .leftRight || type == .rightLeft) { sequence = scrollSequence ?? [ ScrollStep(timeStep: 0.0, position: .home, edgeFades: .trailing), // Starting point, at home, with trailing fade @@ -693,25 +701,27 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { ScrollStep(timeStep: animationDuration, timingFunction: animationCurve, // Away position, using animationCurve transition, with only leading edge faded in position: .away, edgeFades: .leading), ScrollStep(timeStep: 60*60*24*365.0, // "Delay" at away, for huge time to effectie stay at away permanently - position: .away, edgeFades: .leading) + position: .away, edgeFades: .leading), ] } } - + + + // Configure gradient for current condition applyGradientMask(fadeLength, animated: !self.labelize) - + if !tapToScroll && !holdScrolling && shouldBeginScroll { beginScroll(sequence) } } - + private func sublabelSize() -> CGSize { // Bound the expected size let maximumLabelSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) // Calculate the expected size var expectedLabelSize = sublabel.sizeThatFits(maximumLabelSize) - + #if os(tvOS) // Sanitize width to 16384.0 (largest width a UILabel will draw on tvOS) expectedLabelSize.width = min(expectedLabelSize.width, 16384.0) @@ -724,45 +734,45 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { expectedLabelSize.height = bounds.size.height return expectedLabelSize } - + override open func sizeThatFits(_ size: CGSize) -> CGSize { var fitSize = sublabel.sizeThatFits(size) fitSize.width += leadingBuffer return fitSize } - + // // MARK: - Animation Handling // - + open func labelShouldScroll() -> Bool { // Check for nil string if sublabel.text == nil { return false } - + // Check for empty string if sublabel.text!.isEmpty { return false } - + // Check if the label string fits let labelTooLarge = (sublabelSize().width + leadingBuffer) > self.bounds.size.width + CGFloat.ulpOfOne let animationHasDuration = speed.value > 0.0 return (!labelize && labelTooLarge && animationHasDuration) } - + private func labelReadyForScroll() -> Bool { // Check if we have a superview if superview == nil { return false } - + // Check if we are attached to a window if window == nil { return false } - + // Check if our view controller is ready let viewController = firstAvailableViewController() if viewController != nil { @@ -770,28 +780,28 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { return false } } - + return true } - + private func returnLabelToHome() { // Remove any gradient animation maskLayer?.removeAllAnimations() - + // Remove all sublabel position animations sublabel.layer.removeAllAnimations() - + // Remove completion block scrollCompletionBlock = nil } - + private func beginScroll(_ sequence: [MarqueeStep]) { let scroller = generateScrollAnimation(sequence) let fader = generateGradientAnimation(sequence, totalDuration: scroller.duration) - + scroll(scroller, fader: fader) } - + private func scroll(_ scroller: MLAnimation, fader: MLAnimation?) { // Check for conditions which would prevent scrolling if !labelReadyForScroll() { @@ -799,14 +809,14 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } // Convert fader to var var fader = fader - + // Call pre-animation hook labelWillBeginScroll() - + // Start animation transactions CATransaction.begin() CATransaction.setAnimationDuration(TimeInterval(scroller.duration)) - + // Create gradient animation, if needed let gradientAnimation: CAKeyframeAnimation? // Check for IBDesignable @@ -817,14 +827,14 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { maskLayer?.colors = finalColors } maskLayer?.removeAnimation(forKey: "setupFade") - + // Generate animation if needed if let previousAnimation = fader?.anim { gradientAnimation = previousAnimation } else { gradientAnimation = nil } - + // Apply fade animation maskLayer?.add(gradientAnimation!, forKey: "gradient") } else { @@ -832,22 +842,22 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { fader = nil } #else - fader = nil + fader = nil; #endif - - scrollCompletionBlock = { [weak self] (finished: Bool) -> Void in + + scrollCompletionBlock = { [weak self] (finished: Bool) -> () in guard finished else { // Do not continue into the next loop return } - + guard (self != nil) else { return } - + // Call returned home function self!.labelReturnedToHome(true) - + // Check to ensure that: // 1) We don't double fire if an animation already exists // 2) The instance is still attached to a window - this completion block is called for @@ -856,45 +866,45 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { guard self!.window != nil else { return } - + guard self!.sublabel.layer.animation(forKey: "position") == nil else { return } - + // Begin again, if conditions met if (self!.labelShouldScroll() && !self!.tapToScroll && !self!.holdScrolling) { // Perform completion callback self!.scroll(scroller, fader: fader) } } - + // Perform scroll animation scroller.anim.setValue(true, forKey: MarqueeKeys.CompletionClosure.rawValue) scroller.anim.delegate = self sublabel.layer.add(scroller.anim, forKey: "position") - + CATransaction.commit() } - + private func generateScrollAnimation(_ sequence: [MarqueeStep]) -> MLAnimation { // Create scroller, which defines the animation to perform let homeOrigin = homeLabelFrame.origin let awayOrigin = offsetCGPoint(homeLabelFrame.origin, offset: awayOffset) - + let scrollSteps = sequence.filter({ $0 is ScrollStep }) as! [ScrollStep] let totalDuration = scrollSteps.reduce(0.0) { $0 + $1.timeStep } - + // Build scroll data var totalTime: CGFloat = 0.0 var scrollKeyTimes = [NSNumber]() var scrollKeyValues = [NSValue]() var scrollTimingFunctions = [CAMediaTimingFunction]() - + for (offset, step) in scrollSteps.enumerated() { // Scroll Times totalTime += step.timeStep scrollKeyTimes.append(NSNumber(value:Float(totalTime/totalDuration))) - + // Scroll Values let scrollPosition: CGPoint switch step.position { @@ -906,23 +916,23 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { scrollPosition = offsetCGPoint(homeOrigin, offset: awayOffset*frac) } scrollKeyValues.append(NSValue(cgPoint:scrollPosition)) - + // Scroll Timing Functions // Only need n-1 timing functions, so discard the first value as it's unused if offset == 0 { continue } scrollTimingFunctions.append(timingFunctionForAnimationCurve(step.timingFunction)) } - + // Create animation let animation = CAKeyframeAnimation(keyPath: "position") // Set values animation.keyTimes = scrollKeyTimes animation.values = scrollKeyValues animation.timingFunctions = scrollTimingFunctions - + return (anim: animation, duration: totalDuration) } - + private func generateGradientAnimation(_ sequence: [MarqueeStep], totalDuration: CGFloat) -> MLAnimation { // Setup var totalTime: CGFloat = 0.0 @@ -932,26 +942,26 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { var fadeTimingFunctions = [CAMediaTimingFunction]() let transp = UIColor.clear.cgColor let opaque = UIColor.black.cgColor - + // Filter to get only scroll steps and valid precedent/subsequent fade steps let fadeSteps = sequence.enumerated().filter { (arg: (offset: Int, element: MarqueeStep)) -> Bool in let (offset, element) = arg - + // Include all Scroll Steps if element is ScrollStep { return true } - + // Include all Fade Steps that have a directly preceding or subsequent Scroll Step // Exception: Fade Step cannot be first step if offset == 0 { return false } - + // Subsequent step if 1) positive/zero time step and 2) follows a Scroll Step let subsequent = element.timeStep >= 0 && (sequence[max(0, offset - 1)] is ScrollStep) // Precedent step if 1) negative time step and 2) precedes a Scroll Step let precedent = element.timeStep < 0 && (sequence[min(sequence.count - 1, offset + 1)] is ScrollStep) - + return (precedent || subsequent) } - + for (offset, step) in fadeSteps { // Fade times if (step is ScrollStep) { @@ -967,7 +977,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } fadeKeyTimes.append(NSNumber(value:Float(stepTime/totalDuration))) - + // Fade Values let values: [CGColor] let leading = step.edgeFades.contains(.leading) ? transp : opaque @@ -979,37 +989,37 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { values = [trailing, opaque, opaque, leading] } fadeKeyValues.append(values) - + // Fade Timing Function // Only need n-1 timing functions, so discard the first value as it's unused if offset == 0 { continue } fadeTimingFunctions.append(timingFunctionForAnimationCurve(step.timingFunction)) } - + // Create new animation let animation = CAKeyframeAnimation(keyPath: "colors") - + animation.values = fadeKeyValues animation.keyTimes = fadeKeyTimes animation.timingFunctions = fadeTimingFunctions - - return (anim: animation, duration: max(totalTime, totalDuration)) + + return (anim: animation, duration: max(totalTime,totalDuration)) } - + private func applyGradientMask(_ fadeLength: CGFloat, animated: Bool, firstStep: MarqueeStep? = nil) { // Remove any in-flight animations maskLayer?.removeAllAnimations() - + // Check for zero-length fade if (fadeLength <= 0.0) { removeGradientMask() return } - + // Configure gradient mask without implicit animations CATransaction.begin() CATransaction.setDisableActions(true) - + // Determine if gradient mask needs to be created let gradientMask: CAGradientLayer if let currentMask = self.maskLayer { @@ -1023,7 +1033,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { gradientMask.startPoint = CGPoint(x:0.0, y:0.5) gradientMask.endPoint = CGPoint(x:1.0, y:0.5) } - + // Check if there is a mask to layer size mismatch if gradientMask.bounds != self.layer.bounds { // Adjust stops based on fade length @@ -1031,41 +1041,41 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { let rightFadeStop = 1.0 - fadeLength/self.bounds.size.width gradientMask.locations = [0.0, leftFadeStop, rightFadeStop, 1.0].map { NSNumber(value: Float($0)) } } - + gradientMask.bounds = self.layer.bounds gradientMask.position = CGPoint(x:self.bounds.midX, y:self.bounds.midY) - + // Set up colors let transparent = UIColor.clear.cgColor let opaque = UIColor.black.cgColor - + // Set mask self.layer.mask = gradientMask - + // Determine colors for non-scrolling label (i.e. at home) let adjustedColors: [CGColor] let trailingFadeNeeded = self.labelShouldScroll() - + switch (type) { case .continuousReverse, .rightLeft: adjustedColors = [(trailingFadeNeeded ? transparent : opaque), opaque, opaque, opaque] - + // .Continuous, .LeftRight default: adjustedColors = [opaque, opaque, opaque, (trailingFadeNeeded ? transparent : opaque)] } - + // Check for IBDesignable #if TARGET_INTERFACE_BUILDER gradientMask.colors = adjustedColors CATransaction.commit() return #endif - + if (animated) { // Finish transaction CATransaction.commit() - + // Create animation for color change let colorAnimation = GradientSetupAnimation(keyPath: "colors") colorAnimation.fromValue = gradientMask.colors @@ -1079,14 +1089,14 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { CATransaction.commit() } } - + private func removeGradientMask() { self.layer.mask = nil } - + private func timingFunctionForAnimationCurve(_ curve: UIViewAnimationCurve) -> CAMediaTimingFunction { let timingFunction: String? - + switch curve { case .easeIn: timingFunction = kCAMediaTimingFunctionEaseIn @@ -1097,10 +1107,10 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { default: timingFunction = kCAMediaTimingFunctionLinear } - + return CAMediaTimingFunction(name: timingFunction!) } - + private func transactionDurationType(_ labelType: MarqueeType, interval: CGFloat, delay: CGFloat) -> TimeInterval { switch (labelType) { case .leftRight, .rightLeft: @@ -1109,7 +1119,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { return TimeInterval(delay + interval) } } - + public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { if let setupAnim = anim as? GradientSetupAnimation { if let finalColors = setupAnim.toValue as? [CGColor] { @@ -1121,52 +1131,53 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { scrollCompletionBlock?(flag) } } - + + // // MARK: - Private details // - + private var sublabel = UILabel() - + fileprivate var homeLabelFrame = CGRect.zero fileprivate var awayOffset: CGFloat = 0.0 - + override open class var layerClass: AnyClass { return CAReplicatorLayer.self } - + fileprivate weak var repliLayer: CAReplicatorLayer? { return self.layer as? CAReplicatorLayer } - + fileprivate weak var maskLayer: CAGradientLayer? { return self.layer.mask as! CAGradientLayer? } - + fileprivate var scrollCompletionBlock: MLAnimationCompletionBlock? - + override open func draw(_ layer: CALayer, in ctx: CGContext) { // Do NOT call super, to prevent UILabel superclass from drawing into context // Label drawing is handled by sublabel and CAReplicatorLayer layer class - + // Draw only background color if let bgColor = backgroundColor { - ctx.setFillColor(bgColor.cgColor) - ctx.fill(layer.bounds) + ctx.setFillColor(bgColor.cgColor); + ctx.fill(layer.bounds); } } - + fileprivate enum MarqueeKeys: String { case Restart = "MLViewControllerRestart" case Labelize = "MLShouldLabelize" case Animate = "MLShouldAnimate" case CompletionClosure = "MLAnimationCompletion" } - + class fileprivate func notifyController(_ controller: UIViewController, message: MarqueeKeys) { NotificationCenter.default.post(name: Notification.Name(rawValue: message.rawValue), object: nil, userInfo: ["controller" : controller]) } - + @objc public func restartForViewController(_ notification: Notification) { if let controller = (notification as NSNotification).userInfo?["controller"] as? UIViewController { if controller === self.firstAvailableViewController() { @@ -1174,7 +1185,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + @objc public func labelizeForController(_ notification: Notification) { if let controller = (notification as NSNotification).userInfo?["controller"] as? UIViewController { if controller === self.firstAvailableViewController() { @@ -1182,7 +1193,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + @objc public func animateForController(_ notification: Notification) { if let controller = (notification as NSNotification).userInfo?["controller"] as? UIViewController { if controller === self.firstAvailableViewController() { @@ -1190,11 +1201,12 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { } } } - + + // // MARK: - Label Control // - + /** Overrides any non-size condition which is preventing the receiver from automatically scrolling, and begins a scroll animation. @@ -1214,7 +1226,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { updateAndScroll() } } - + /** Immediately resets the label to the home position, cancelling any in-flight scroll animation, and restarts the scroll animation if the appropriate conditions are met. @@ -1229,7 +1241,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { updateAndScroll() } } - + /** Resets the label text, recalculating the scroll animation. @@ -1244,7 +1256,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { homeLabelFrame = CGRect.null awayOffset = 0.0 } - + /** Immediately resets the label to the home position, cancelling any in-flight scroll animation. @@ -1261,7 +1273,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { // Apply gradient mask for home location applyGradientMask(fadeLength, animated: false) } - + /** Pauses the text scrolling animation, at any point during an in-progress animation. @@ -1275,18 +1287,18 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { guard (!isPaused && awayFromHome) else { return } - + // Pause sublabel position animations let labelPauseTime = sublabel.layer.convertTime(CACurrentMediaTime(), from: nil) sublabel.layer.speed = 0.0 sublabel.layer.timeOffset = labelPauseTime - + // Pause gradient fade animation let gradientPauseTime = maskLayer?.convertTime(CACurrentMediaTime(), from:nil) maskLayer?.speed = 0.0 maskLayer?.timeOffset = gradientPauseTime! } - + /** Un-pauses a previously paused text scrolling animation. This method has no effect if the label was not previously paused using `pauseLabel`. @@ -1297,14 +1309,14 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { guard (isPaused) else { return } - + // Unpause sublabel position animations let labelPausedTime = sublabel.layer.timeOffset sublabel.layer.speed = 1.0 sublabel.layer.timeOffset = 0.0 sublabel.layer.beginTime = 0.0 sublabel.layer.beginTime = sublabel.layer.convertTime(CACurrentMediaTime(), from:nil) - labelPausedTime - + // Unpause gradient fade animation let gradientPauseTime = maskLayer?.timeOffset maskLayer?.speed = 1.0 @@ -1312,13 +1324,13 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { maskLayer?.beginTime = 0.0 maskLayer?.beginTime = maskLayer!.convertTime(CACurrentMediaTime(), from:nil) - gradientPauseTime! } - + @objc public func labelWasTapped(_ recognizer: UIGestureRecognizer) { if labelShouldScroll() && !awayFromHome { updateAndScroll() } } - + /** Called when the label animation is about to begin. @@ -1329,7 +1341,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { // Default implementation does nothing - override to customize return } - + /** Called when the label animation has finished, and the label is at the home position. @@ -1345,17 +1357,17 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { // Default implementation does nothing - override to customize return } - + // // MARK: - Modified UILabel Functions/Getters/Setters // - + #if os(iOS) override open func forBaselineLayout() -> UIView { // Use subLabel view for handling baseline layouts return sublabel } - + override open var forLastBaselineLayout: UIView { // Use subLabel view for handling baseline layouts return sublabel @@ -1366,7 +1378,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { get { return sublabel.text } - + set { if sublabel.text == newValue { return @@ -1376,12 +1388,12 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { super.text = text } } - + override open var attributedText: NSAttributedString? { get { return sublabel.attributedText } - + set { if sublabel.attributedText == newValue { return @@ -1391,194 +1403,196 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate { super.attributedText = attributedText } } - + override open var font: UIFont! { get { return sublabel.font } - + set { if sublabel.font == newValue { return } sublabel.font = newValue super.font = newValue - + updateAndScroll() } } - + override open var textColor: UIColor! { get { return sublabel.textColor } - + set { sublabel.textColor = newValue super.textColor = newValue } } - + override open var backgroundColor: UIColor? { get { return sublabel.backgroundColor } - + set { sublabel.backgroundColor = newValue super.backgroundColor = newValue } } - + override open var shadowColor: UIColor? { get { return sublabel.shadowColor } - + set { sublabel.shadowColor = newValue super.shadowColor = newValue } } - + override open var shadowOffset: CGSize { get { return sublabel.shadowOffset } - + set { sublabel.shadowOffset = newValue super.shadowOffset = newValue } } - + override open var highlightedTextColor: UIColor? { get { return sublabel.highlightedTextColor } - + set { sublabel.highlightedTextColor = newValue super.highlightedTextColor = newValue } } - + override open var isHighlighted: Bool { get { return sublabel.isHighlighted } - + set { sublabel.isHighlighted = newValue super.isHighlighted = newValue } } - + override open var isEnabled: Bool { get { return sublabel.isEnabled } - + set { sublabel.isEnabled = newValue super.isEnabled = newValue } } - + override open var numberOfLines: Int { get { return super.numberOfLines } - + set { // By the nature of MarqueeLabel, this is 1 super.numberOfLines = 1 } } - + override open var adjustsFontSizeToFitWidth: Bool { get { return super.adjustsFontSizeToFitWidth } - + set { // By the nature of MarqueeLabel, this is false super.adjustsFontSizeToFitWidth = false } } - + override open var minimumScaleFactor: CGFloat { get { return super.minimumScaleFactor } - + set { super.minimumScaleFactor = 0.0 } } - + override open var baselineAdjustment: UIBaselineAdjustment { get { return sublabel.baselineAdjustment } - + set { sublabel.baselineAdjustment = newValue super.baselineAdjustment = newValue } } - + override open var intrinsicContentSize: CGSize { var content = sublabel.intrinsicContentSize content.width += leadingBuffer return content } - + override open var tintColor: UIColor! { get { return sublabel.tintColor } - + set { sublabel.tintColor = newValue super.tintColor = newValue } } - + override open func tintColorDidChange() { super.tintColorDidChange() sublabel.tintColorDidChange() } - + override open var contentMode: UIViewContentMode { get { return sublabel.contentMode } - + set { super.contentMode = contentMode sublabel.contentMode = newValue } } + // // MARK: - Support // - + fileprivate func offsetCGPoint(_ point: CGPoint, offset: CGFloat) -> CGPoint { return CGPoint(x: point.x + offset, y: point.y) } - + // // MARK: - Deinit // - + deinit { NotificationCenter.default.removeObserver(self) } - + } + // // MARK: - Support // @@ -1588,6 +1602,7 @@ public protocol MarqueeStep { var edgeFades: EdgeFade { get } } + /** `ScrollStep` types define the label position at a specified time delta since the last `ScrollStep` step, as well as the animation curve to that position and edge fade state at the position @@ -1611,25 +1626,25 @@ public struct ScrollStep: MarqueeStep { case away case partial(CGFloat) } - + /** The desired time between this step and the previous `ScrollStep` in a sequence. */ public let timeStep: CGFloat - + /** The animation curve to utilize between the previous `ScrollStep` in a sequence and this step. - Note: The animation curve value for the first `ScrollStep` in a sequence has no effect. */ public let timingFunction: UIViewAnimationCurve - + /** The position of the label for this scroll step. - SeeAlso: Position */ public let position: Position - + /** The option set defining the edge fade state for this scroll step. @@ -1637,7 +1652,7 @@ public struct ScrollStep: MarqueeStep { the direction of scroll) and trailing edge of the label. */ public let edgeFades: EdgeFade - + public init(timeStep: CGFloat, timingFunction: UIViewAnimationCurve = .linear, position: Position, edgeFades: EdgeFade) { self.timeStep = timeStep self.position = position @@ -1646,6 +1661,7 @@ public struct ScrollStep: MarqueeStep { } } + /** `FadeStep` types allow additional edge fade state definitions, around the states defined by the `ScrollStep` steps of a sequence. `FadeStep` steps are defined by the time delta to the preceding or subsequent `ScrollStep` step and the timing @@ -1667,12 +1683,12 @@ public struct FadeStep: MarqueeStep { a `ScrollStep`. */ public let timeStep: CGFloat - + /** The animation curve to utilize between the previous fade state in a sequence and this step. */ public let timingFunction: UIViewAnimationCurve - + /** The option set defining the edge fade state for this fade step. @@ -1682,7 +1698,7 @@ public struct FadeStep: MarqueeStep { As an Option Set type, both edge fade states may be defined using an array literal: `[.leading, .trailing]`. */ public let edgeFades: EdgeFade - + public init(timeStep: CGFloat, timingFunction: UIViewAnimationCurve = .linear, edgeFades: EdgeFade) { self.timeStep = timeStep self.timingFunction = timingFunction @@ -1690,32 +1706,32 @@ public struct FadeStep: MarqueeStep { } } -public struct EdgeFade: OptionSet { +public struct EdgeFade : OptionSet { public let rawValue: Int public static let leading = EdgeFade(rawValue: 1 << 0) public static let trailing = EdgeFade(rawValue: 1 << 1) - + public init(rawValue: Int) { - self.rawValue = rawValue + self.rawValue = rawValue; } } // Define helpful typealiases -fileprivate typealias MLAnimationCompletionBlock = (_ finished: Bool) -> Void +fileprivate typealias MLAnimationCompletionBlock = (_ finished: Bool) -> () fileprivate typealias MLAnimation = (anim: CAKeyframeAnimation, duration: CGFloat) -private class GradientSetupAnimation: CABasicAnimation { +fileprivate class GradientSetupAnimation: CABasicAnimation { } fileprivate extension UIResponder { // Thanks to Phil M // http://stackoverflow.com/questions/1340434/get-to-uiviewcontroller-from-uiview-on-iphone - + func firstAvailableViewController() -> UIViewController? { // convenience function for casting and to "mask" the recursive function return self.traverseResponderChainForFirstViewController() } - + func traverseResponderChainForFirstViewController() -> UIViewController? { if let nextResponder = self.next { if nextResponder is UIViewController { @@ -1731,30 +1747,30 @@ fileprivate extension UIResponder { } fileprivate extension CAMediaTimingFunction { - + func durationPercentageForPositionPercentage(_ positionPercentage: CGFloat, duration: CGFloat) -> CGFloat { // Finds the animation duration percentage that corresponds with the given animation "position" percentage. // Utilizes Newton's Method to solve for the parametric Bezier curve that is used by CAMediaAnimation. - + let controlPoints = self.controlPoints() let epsilon: CGFloat = 1.0 / (100.0 * CGFloat(duration)) - + // Find the t value that gives the position percentage we want let t_found = solveTforY(positionPercentage, epsilon: epsilon, controlPoints: controlPoints) - + // With that t, find the corresponding animation percentage let durationPercentage = XforCurveAt(t_found, controlPoints: controlPoints) - + return durationPercentage } - + func solveTforY(_ y_0: CGFloat, epsilon: CGFloat, controlPoints: [CGPoint]) -> CGFloat { // Use Newton's Method: http://en.wikipedia.org/wiki/Newton's_method // For first guess, use t = y (i.e. if curve were linear) var t0 = y_0 var t1 = y_0 var f0, df0: CGFloat - + for _ in 0..<15 { // Base this iteration of t1 calculated from last iteration t0 = t1 @@ -1774,56 +1790,56 @@ fileprivate extension CAMediaTimingFunction { // Else recalculate t1 t1 = t0 - f0/df0 } - + // Give up - shouldn't ever get here...I hope print("MarqueeLabel: Failed to find t for Y input!") return t0 } - - func YforCurveAt(_ t: CGFloat, controlPoints: [CGPoint]) -> CGFloat { + + func YforCurveAt(_ t: CGFloat, controlPoints:[CGPoint]) -> CGFloat { let P0 = controlPoints[0] let P1 = controlPoints[1] let P2 = controlPoints[2] let P3 = controlPoints[3] - + // Per http://en.wikipedia.org/wiki/Bezier_curve#Cubic_B.C3.A9zier_curves - let y0 = (pow((1.0 - t), 3.0) * P0.y) + let y0 = (pow((1.0 - t),3.0) * P0.y) let y1 = (3.0 * pow(1.0 - t, 2.0) * t * P1.y) let y2 = (3.0 * (1.0 - t) * pow(t, 2.0) * P2.y) let y3 = (pow(t, 3.0) * P3.y) - + return y0 + y1 + y2 + y3 } - + func XforCurveAt(_ t: CGFloat, controlPoints: [CGPoint]) -> CGFloat { let P0 = controlPoints[0] let P1 = controlPoints[1] let P2 = controlPoints[2] let P3 = controlPoints[3] - + // Per http://en.wikipedia.org/wiki/Bezier_curve#Cubic_B.C3.A9zier_curves - - let x0 = (pow((1.0 - t), 3.0) * P0.x) + + let x0 = (pow((1.0 - t),3.0) * P0.x) let x1 = (3.0 * pow(1.0 - t, 2.0) * t * P1.x) let x2 = (3.0 * (1.0 - t) * pow(t, 2.0) * P2.x) let x3 = (pow(t, 3.0) * P3.x) - + return x0 + x1 + x2 + x3 } - + func derivativeCurveYValueAt(_ t: CGFloat, controlPoints: [CGPoint]) -> CGFloat { let P0 = controlPoints[0] let P1 = controlPoints[1] let P2 = controlPoints[2] let P3 = controlPoints[3] - + let dy0 = (P0.y + 3.0 * P1.y + 3.0 * P2.y - P3.y) * -3.0 let dy1 = t * (6.0 * P0.y + 6.0 * P2.y) let dy2 = (-3.0 * P0.y + 3.0 * P1.y) return dy0 * pow(t, 2.0) + dy1 + dy2 } - + func controlPoints() -> [CGPoint] { // Create point array to point to var point: [Float] = [0.0, 0.0] @@ -1832,7 +1848,8 @@ fileprivate extension CAMediaTimingFunction { self.getControlPoint(at: i, values: &point) pointArray.append(CGPoint(x: CGFloat(point[0]), y: CGFloat(point[1]))) } - + return pointArray } } +