带有淡入淡出文本动画的TextView

问题描述 投票:0回答:1

我正在尝试在 Swift 中实现 Textview,例如:https://github.com/chtgupta/FadeInTextView-Android

我尝试为文本更改时的不透明度动画设置 CALayer, 但动画和新文字设置好后,新文字会闪烁一次。

更新

class AnimatedTextView: UITextView {

private var animatingTextLayer: CATextLayer?
private var aniStatus: Bool = false

override func layoutSublayers(of layer: CALayer) {
    super.layoutSublayers(of: layer)
    if !aniStatus {
        animatingTextLayer?.removeFromSuperlayer()
    }
}

func appendTextAndAnimate(with newText: String) {
    guard let font = self.font else { return }
    
    animatingTextLayer = CATextLayer()
    guard let animatingTextLayer = animatingTextLayer else { return }
    
    animatingTextLayer.string = newText
    animatingTextLayer.foregroundColor = UIColor.black.cgColor
    animatingTextLayer.alignmentMode = .left
    animatingTextLayer.frame = calculateTextLayerFrame(for: newText, in: self)
    animatingTextLayer.opacity = 0
    self.layer.addSublayer(animatingTextLayer)

    let fadeAnimation = CABasicAnimation(keyPath: "opacity")
    fadeAnimation.fromValue = 0
    fadeAnimation.toValue = 1
    fadeAnimation.duration = 0.5
    
    CATransaction.begin()
    CATransaction.setCompletionBlock {
        animatingTextLayer.opacity = 1
        self.text.append(newText)
        self.aniStatus = false
    }
    animatingTextLayer.add(fadeAnimation, forKey: "fadeIn")
    aniStatus = true
    CATransaction.commit()
}

func calculateTextLayerFrame(for newCharacter: String, in textView: UITextView) -> CGRect {
    guard let font = textView.font else { return .zero }
    
    let textLength = textView.text.count
    
    let layoutManager = textView.layoutManager
    let textContainer = textView.textContainer
    let glyphRange = layoutManager.glyphRange(forCharacterRange: NSRange(location: textLength, length: 0), actualCharacterRange: nil)
    let glyphRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    let xOffset = glyphRect.origin.x + textView.textContainerInset.left
    let yOffset = glyphRect.origin.y + textView.textContainerInset.top

    let newTextSize = (newCharacter as NSString).size(withAttributes: [.font: font])
    
    return CGRect(x: xOffset, y: yOffset, width: newTextSize.width, height: newTextSize.height)
}
}

为了使用这个视图,我实现了一个 UIViewRepresentable:

struct TextViewRepresentable: UIViewRepresentable {
var content: String

func updateUIView(_ uiView: AnimatedTextView, context: Context) {
    let currentText = uiView.text ?? ""
    let newText = String(content.dropFirst(currentText.count))
    
    if !newText.isEmpty {
        uiView.appendTextAndAnimate(with: newText)
    }
}
}

图层动画效果很好,但是动画结束后,似乎Textview.text = newText导致刷新布局,并且textview闪烁一次。

我正在寻找一种正确的方法来删除动画层和文本更新时间。

ios swift animation textview calayer
1个回答
0
投票

一种方法是使用 2 个文本视图,并在使用交叉溶解过渡时交替向每个文本视图添加 1 个字符。

enter image description here

示例代码...


UIView子类:

class TextAnimView: UIView {

    public var theText: String = "Test"

    public var font: UIFont = .systemFont(ofSize: 16.0) {
        didSet {
            textViews[0].font = font
            textViews[1].font = font
        }
    }
    public var textColor: UIColor = .black {
        didSet {
            textViews[0].textColor = textColor
            textViews[1].textColor = textColor
        }
    }
    override var backgroundColor: UIColor? {
        didSet {
            super.backgroundColor = backgroundColor
            textViews[0].backgroundColor = backgroundColor
            textViews[1].backgroundColor = backgroundColor
        }
    }
    
    // two text views
    private let textViews: [UITextView] = [
        UITextView(), UITextView(),
    ]
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        for v in textViews {
            v.translatesAutoresizingMaskIntoConstraints = false
            addSubview(v)
            NSLayoutConstraint.activate([
                v.topAnchor.constraint(equalTo: topAnchor),
                v.leadingAnchor.constraint(equalTo: leadingAnchor),
                v.trailingAnchor.constraint(equalTo: trailingAnchor),
                v.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
            v.isScrollEnabled = false
            v.isUserInteractionEnabled = false
            v.backgroundColor = .white
        }
    }
    
    private var cCounter: Int = 0
    
    public func doAnim() -> Void {
        cCounter = 1
        textViews[0].text = ""
        textViews[1].text = ""
        textViews[0].isHidden = false
        textViews[1].isHidden = true
        nextChar()
    }
    private func nextChar() -> Void {
        // fromView is the one that is NOT hidden
        let fromView: UITextView = textViews[0].isHidden ? textViews[1] : textViews[0]
        
        // toView is the one that IS hidden
        let toView: UITextView = textViews[0].isHidden ? textViews[0] : textViews[1]
        
        // set the text of the "view to show" to one character longer
        toView.text = String(theText.prefix(cCounter))
        
        UIView.transition(from: fromView,
                          to: toView,
                          duration: 0.15,
                          options: [.transitionCrossDissolve, .showHideTransitionViews],
                          completion: { b in
            self.cCounter += 1
            if self.cCounter <= self.theText.count {
                self.nextChar()
            } else {
                // if we want to do something when the text has been fully shown
            }
        })
    }
    
}

示例视图控制器类: - 点击任意位置即可循环到下一个示例字符串...

class TextAnimVC: UIViewController {
    
    let testView = TextAnimView()
    
    var sampleStrings: [String] = [
        "Sup!\nI'm a Fade-In TextView.",
        "This is another\nSample String!",
        "When using text wrapping, though, this animation may not be suitable.",
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        testView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(testView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            testView.heightAnchor.constraint(equalToConstant: 200.0),
        ])
        
        testView.font = .systemFont(ofSize: 32.0, weight: .regular)
        testView.textColor = .white
        testView.backgroundColor = .blue
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let s = sampleStrings.removeFirst()
        sampleStrings.append(s)
        testView.theText = s
        testView.doAnim()
    }

}
© www.soinside.com 2019 - 2024. All rights reserved.