Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animation doesn't work, if text doesn't changed #90

Open
LexDeBash opened this issue Aug 23, 2017 · 2 comments
Open

Animation doesn't work, if text doesn't changed #90

LexDeBash opened this issue Aug 23, 2017 · 2 comments

Comments

@LexDeBash
Copy link

Hello. Thank you very much for your wonderful framework. I use it in my application to select a random result from the list. If a repeated value occurs, the animation does not work and there is a feeling that the tap on the screen did not work.

@LqYe
Copy link

LqYe commented Oct 17, 2017

same issue

@Charchoghlyan
Copy link

Hi
Change this LTMorphingLabel.swift code
import Foundation
import UIKit
import QuartzCore

private func < (lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l < r
case (nil, _?):
return true
default:
return false
}
}

private func >= (lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l >= r
default:
return !(lhs < rhs)
}
}

enum LTMorphingPhases: Int {
case start, appear, disappear, draw, progress, skipFrames
}

typealias LTMorphingStartClosure =
() -> Void

typealias LTMorphingEffectClosure =
(Character, _ index: Int, _ progress: Float) -> LTCharacterLimbo

typealias LTMorphingDrawingClosure =
(LTCharacterLimbo) -> Bool

typealias LTMorphingManipulateProgressClosure =
(_ index: Int, _ progress: Float, _ isNewChar: Bool) -> Float

typealias LTMorphingSkipFramesClosure =
() -> Int

@objc public protocol LTMorphingLabelDelegate {
@objc optional func morphingDidStart(_ label: LTMorphingLabel)
@objc optional func morphingDidComplete(_ label: LTMorphingLabel)
@objc optional func morphingOnProgress(_ label: LTMorphingLabel, progress: Float)
}

// MARK: - LTMorphingLabel
@IBDesignable open class LTMorphingLabel: UILabel {

@IBInspectable open var morphingProgress: Float = 0.0
@IBInspectable open var morphingDuration: Float = 0.6
@IBInspectable open var morphingCharacterDelay: Float = 0.026
@IBInspectable open var morphingEnabled: Bool = true

@IBOutlet open weak var delegate: LTMorphingLabelDelegate?
open var morphingEffect: LTMorphingEffect = .scale

var startClosures = [String: LTMorphingStartClosure]()
var effectClosures = [String: LTMorphingEffectClosure]()
var drawingClosures = [String: LTMorphingDrawingClosure]()
var progressClosures = [String: LTMorphingManipulateProgressClosure]()
var skipFramesClosures = [String: LTMorphingSkipFramesClosure]()
var diffResults: LTStringDiffResult?
var previousText = ""

var currentFrame: Float = 0.0
var totalFrames = 0
var totalDelayFrames = 0

var totalWidth: Float = 0.0
var previousRects = [CGRect]()
var newRects = [CGRect]()
var charHeight: CGFloat = 0.0
var skipFramesCount: Int = 0

fileprivate var displayLink: CADisplayLink?

private var tempRenderMorphingEnabled = true

#if TARGET_INTERFACE_BUILDER
let presentingInIB = true
#else
let presentingInIB = false
#endif

override open var font: UIFont! {
    get {
        return super.font ?? UIFont.systemFont(ofSize: 15)
    }
    set {
        super.font = newValue
        setNeedsLayout()
    }
}

override open var text: String? {
    get {
        return super.text ?? ""
    }
    set {
        guard text != newValue else { return }

        previousText = text ?? ""
        diffResults = previousText.diffWith(newValue)
        super.text = newValue ?? ""
        
        morphingProgress = 0.0
        currentFrame = 0.0
        totalFrames = 0
        
        tempRenderMorphingEnabled = morphingEnabled
        setNeedsLayout()
        
        if !morphingEnabled {
            return
        }
        
        if presentingInIB {
            morphingDuration = 0.01
            morphingProgress = 0.5
        } else if previousText != text {
            start()
            let closureKey = "\(morphingEffect.description)\(LTMorphingPhases.start)"
            if let closure = startClosures[closureKey] {
                return closure()
            }
            
            delegate?.morphingDidStart?(self)
        }
    }
}

open func start() {
    guard displayLink == nil else { return }
    displayLink = CADisplayLink(target: self, selector: #selector(displayFrameTick))
    displayLink?.add(to: .current, forMode: RunLoop.Mode.common)
}

open func pause() {
    displayLink?.isPaused = true
}

open func unpause() {
    displayLink?.isPaused = false
}

open func finish() {
    displayLink?.isPaused = false
}

open func stop() {
    displayLink?.remove(from: .current, forMode: RunLoop.Mode.common)
    displayLink?.invalidate()
    displayLink = nil
}

open var textAttributes: [NSAttributedString.Key: Any]? {
    didSet {
        setNeedsLayout()
    }
}

open override func setNeedsLayout() {
    super.setNeedsLayout()
    previousRects = rectsOfEachCharacter(previousText, withFont: font)
    newRects = rectsOfEachCharacter(text ?? "", withFont: font)
}

override open var bounds: CGRect {
    get {
        return super.bounds
    }
    set {
        super.bounds = newValue
        setNeedsLayout()
    }
}

override open var frame: CGRect {
    get {
        return super.frame
    }
    set {
        super.frame = newValue
        setNeedsLayout()
    }
}

deinit {
    stop()
}

lazy var emitterView: LTEmitterView = {
    let emitterView = LTEmitterView(frame: self.bounds)
    self.addSubview(emitterView)
    return emitterView
    }()

}

// MARK: - Animation extension
extension LTMorphingLabel {

public func updateProgress(progress: Float) {
    guard let displayLink = displayLink else { return }
    if displayLink.duration > 0.0 && totalFrames == 0 {
        var frameRate = Float(0)
        if #available(iOS 10.0, tvOS 10.0, *) {
            var frameInterval = 1
            if displayLink.preferredFramesPerSecond == 60 {
                frameInterval = 1
            } else if displayLink.preferredFramesPerSecond == 30 {
                frameInterval = 2
            } else {
                frameInterval = 1
            }
            frameRate = Float(displayLink.duration) / Float(frameInterval)
        } else {
            frameRate = Float(displayLink.duration) / Float(displayLink.frameInterval)
        }
        totalFrames = Int(ceil(morphingDuration / frameRate))
        
        let totalDelay = Float((text!).count) * morphingCharacterDelay
        totalDelayFrames = Int(ceil(totalDelay / frameRate))
    }
    
    guard !(progress.isNaN || progress.isInfinite) else {
        return
    }
    
    currentFrame = progress * Float(totalFrames)
    
    if previousText != text && currentFrame < Float(totalFrames + totalDelayFrames + 5) {
        morphingProgress = progress
        
        let closureKey = "\(morphingEffect.description)\(LTMorphingPhases.skipFrames)"
        if let closure = skipFramesClosures[closureKey] {
            skipFramesCount += 1
            if skipFramesCount > closure() {
                skipFramesCount = 0
                setNeedsDisplay()
            }
        } else {
            setNeedsDisplay()
        }
        
        if let onProgress = delegate?.morphingOnProgress {
            onProgress(self, morphingProgress)
        }
    } else {
        stop()
        
        delegate?.morphingDidComplete?(self)
    }
}

@objc func displayFrameTick() {
    updateProgress(progress: Float(currentFrame + 1) / Float(totalFrames))
}

// Could be enhanced by kerning text:
// http://stackoverflow.com/questions/21443625/core-text-calculate-letter-frame-in-ios
func rectsOfEachCharacter(_ textToDraw: String, withFont font: UIFont) -> [CGRect] {
    var charRects = [CGRect]()
    var leftOffset: CGFloat = 0.0
    
    charHeight = "Leg".size(withAttributes: [.font: font]).height
    
    let topOffset = (bounds.size.height - charHeight) / 2.0

    for char in textToDraw {
        let charSize = String(char).size(withAttributes: [.font: font])
        charRects.append(
            CGRect(
                origin: CGPoint(
                    x: leftOffset,
                    y: topOffset
                ),
                size: charSize
            )
        )
        leftOffset += charSize.width
    }
    
    totalWidth = Float(leftOffset)
    
    var stringLeftOffSet: CGFloat = 0.0
    
    switch textAlignment {
    case .center:
        stringLeftOffSet = CGFloat((Float(bounds.size.width) - totalWidth) / 2.0)
    case .right:
        stringLeftOffSet = CGFloat(Float(bounds.size.width) - totalWidth)
    default:
        ()
    }
    
    var offsetedCharRects = [CGRect]()
    
    for r in charRects {
        offsetedCharRects.append(r.offsetBy(dx: stringLeftOffSet, dy: 0.0))
    }
    
    return offsetedCharRects
}

func limboOfOriginalCharacter(
    _ char: Character,
    index: Int,
    progress: Float) -> LTCharacterLimbo {
        
        var currentRect = previousRects[index]
        let oriX = Float(currentRect.origin.x)
        var newX = Float(currentRect.origin.x)
        let diffResult = diffResults!.0[index]
        var currentFontSize: CGFloat = font.pointSize
        var currentAlpha: CGFloat = 1.0
        
        switch diffResult {
            // Move the character that exists in the new text to current position
        case .same:
            newX = Float(newRects[index].origin.x)
            currentRect.origin.x = CGFloat(
                LTEasing.easeOutQuint(progress, oriX, newX - oriX)
            )
        case .move(let offset):
            newX = Float(newRects[index + offset].origin.x)
            currentRect.origin.x = CGFloat(
                LTEasing.easeOutQuint(progress, oriX, newX - oriX)
            )
        case .moveAndAdd(let offset):
            newX = Float(newRects[index + offset].origin.x)
            currentRect.origin.x = CGFloat(
                LTEasing.easeOutQuint(progress, oriX, newX - oriX)
            )
        default:
            // Otherwise, remove it
            
            // Override morphing effect with closure in extenstions
            if let closure = effectClosures[
                "\(morphingEffect.description)\(LTMorphingPhases.disappear)"
                ] {
                    return closure(char, index, progress)
            } else {
                // And scale it by default
                let fontEase = CGFloat(
                    LTEasing.easeOutQuint(
                        progress, 0, Float(font.pointSize)
                    )
                )
                // For emojis
                currentFontSize = max(0.0001, font.pointSize - fontEase)
                currentAlpha = CGFloat(1.0 - progress)
                currentRect = previousRects[index].offsetBy(
                    dx: 0,
                    dy: CGFloat(font.pointSize - currentFontSize)
                )
            }
        }
        
        return LTCharacterLimbo(
            char: char,
            rect: currentRect,
            alpha: currentAlpha,
            size: currentFontSize,
            drawingProgress: 0.0
        )
}

func limboOfNewCharacter(
    _ char: Character,
    index: Int,
    progress: Float) -> LTCharacterLimbo {
        
        let currentRect = newRects[index]
        var currentFontSize = CGFloat(
            LTEasing.easeOutQuint(progress, 0, Float(font.pointSize))
        )
        
        if let closure = effectClosures[
            "\(morphingEffect.description)\(LTMorphingPhases.appear)"
            ] {
                return closure(char, index, progress)
        } else {
            currentFontSize = CGFloat(
                LTEasing.easeOutQuint(progress, 0.0, Float(font.pointSize))
            )
            // For emojis
            currentFontSize = max(0.0001, currentFontSize)
            
            let yOffset = CGFloat(font.pointSize - currentFontSize)
            
            return LTCharacterLimbo(
                char: char,
                rect: currentRect.offsetBy(dx: 0, dy: yOffset),
                alpha: CGFloat(morphingProgress),
                size: currentFontSize,
                drawingProgress: 0.0
            )
        }
}

func limboOfCharacters() -> [LTCharacterLimbo] {
    var limbo = [LTCharacterLimbo]()
    
    // Iterate original characters
    for (i, character) in previousText.enumerated() {
        var progress: Float = 0.0
        
        if let closure = progressClosures[
            "\(morphingEffect.description)\(LTMorphingPhases.progress)"
            ] {
                progress = closure(i, morphingProgress, false)
        } else {
            progress = min(1.0, max(0.0, morphingProgress + morphingCharacterDelay * Float(i)))
        }
        
        let limboOfCharacter = limboOfOriginalCharacter(character, index: i, progress: progress)
        limbo.append(limboOfCharacter)
    }
    
    // Add new characters
    for (i, character) in (text!).enumerated() {
        if i >= diffResults?.0.count {
            break
        }
        
        var progress: Float = 0.0
        
        if let closure = progressClosures[
            "\(morphingEffect.description)\(LTMorphingPhases.progress)"
            ] {
                progress = closure(i, morphingProgress, true)
        } else {
            progress = min(1.0, max(0.0, morphingProgress - morphingCharacterDelay * Float(i)))
        }
        
        // Don't draw character that already exists
        if diffResults?.skipDrawingResults[i] == true {
            continue
        }
        
        if let diffResult = diffResults?.0[i] {
            switch diffResult {
            case .moveAndAdd, .replace, .add, .delete:
                let limboOfCharacter = limboOfNewCharacter(
                    character,
                    index: i,
                    progress: progress
                )
                limbo.append(limboOfCharacter)
            default:
                ()
            }
        }
    }
    
    return limbo
}

}

// MARK: - Drawing extension
extension LTMorphingLabel {

override open func didMoveToSuperview() {
    guard nil != superview else {
        stop()
        return
    }

    if let s = text {
        text = s
    }
    
    // Load all morphing effects
    for effectName: String in LTMorphingEffect.allValues {
        let effectFunc = Selector("\(effectName)Load")
        if responds(to: effectFunc) {
            perform(effectFunc)
        }
    }
}

override open func drawText(in rect: CGRect) {
    if !tempRenderMorphingEnabled || limboOfCharacters().count == 0 {
        super.drawText(in: rect)
        return
    }
    
    for charLimbo in limboOfCharacters() {
        let charRect = charLimbo.rect
        
        let willAvoidDefaultDrawing: Bool = {
            if let closure = drawingClosures[
                "\(morphingEffect.description)\(LTMorphingPhases.draw)"
                ] {
                    return closure($0)
            }
            return false
            }(charLimbo)

        if !willAvoidDefaultDrawing {
            var attrs: [NSAttributedString.Key: Any] = [
                .foregroundColor: textColor.withAlphaComponent(charLimbo.alpha)
            ]

            if let font = UIFont(name: font.fontName, size: charLimbo.size) {
                attrs[.font] = font
            }
            
            for (key, value) in textAttributes ?? [:] {
                attrs[key] = value
            }
            
            let s = String(charLimbo.char)
            s.draw(in: charRect, withAttributes: attrs)
        }
    }
}

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants