ios - 如何在 iOS 中正确变形文本?

标签 ios animation uilabel core-animation morphing

我拼命想把 smallLabel 变成 bigLabel。通过变形,我的意思是将一个标签的以下属性转换为与另一个标签的相应属性相匹配,并具有流畅的动画效果:

  • 字体大小
  • 字体粗细
  • 框架(即边界和位置)

当使用大标题时,所需的效果应该类似于应用于导航 Controller 标题标签的动画:

iOS Large Title Animation

现在我知道去年的 WWDC session Advanced Animations with UIKit他们在那里展示了如何做到这一点。然而,这种技术非常有限,因为它基本上只是对标签的框架应用变换,因此只有在除字体大小之外的所有属性都相同的情况下它才有效。

当一个标签具有常规字体粗细而另一个标签具有粗体粗细时,该技术已经失败——这些属性在应用时不会改变一个变换。因此,我决定更深入地挖掘并使用 Core Animation 进行变形。

首先,我创建了一个新的文本层,我将其设置为在视觉上与 smallLabel 相同:

/// Creates a text layer with its text and properties copied from the label.
func createTextLayer(from label: UILabel) -> CATextLayer {
    let textLayer = CATextLayer()
    textLayer.frame = label.frame
    textLayer.string = label.text
    textLayer.opacity = 0.3
    textLayer.fontSize = label.font.pointSize
    textLayer.foregroundColor = UIColor.red.cgColor
    textLayer.backgroundColor = UIColor.cyan.cgColor
    view.layer.addSublayer(textLayer)
    return textLayer
}

然后,我创建必要的动画并将它们添加到该层:

func animate(from smallLabel: UILabel, to bigLabel: UILabel) {

    let textLayer = createTextLayer(from: smallLabel)
    view.layer.addSublayer(textLayer)

    let group = CAAnimationGroup()
    group.duration = 4
    group.repeatCount = .infinity

    // Animate font size
    let fontSizeAnimation = CABasicAnimation(keyPath: "fontSize")
    fontSizeAnimation.toValue = bigLabel.font.pointSize

    // Animate font (weight)
    let fontAnimation = CABasicAnimation(keyPath: "font")
    fontAnimation.toValue = CGFont(bigLabel.font.fontName as CFString)

    // Animate bounds
    let boundsAnimation = CABasicAnimation(keyPath: "bounds")
    boundsAnimation.toValue = bigLabel.bounds

    // Animate position
    let positionAnimation = CABasicAnimation(keyPath: "position")
    positionAnimation.toValue = bigLabel.layer.position

    group.animations = [
        fontSizeAnimation,
        boundsAnimation,
        positionAnimation,
        fontAnimation
    ]

    textLayer.add(group, forKey: "group")
}

这是我得到的:

Animation

如您所见,它并没有完全按预期工作。这个动画有两个问题:

  1. 字体粗细不是动画而是在动画过程中突然切换。

  2. 当(青色)文本层的框架按预期移动并变大时,文本本身以某种方式向层的左下角移动并从右侧被截断。

我的问题是:

1️⃣为什么会出现这种情况(尤其是2.)?

2️⃣如何实现如上所示的大标题变形行为——包括字体粗细动画?

最佳答案

可能比您想象的更简单。只需快照层或 View 。红色文本在 apple 转换的视频中渗出,因此它们只是通过快照或转换混合在一起。我倾向于快照 View ,以免影响下面的真实 View 。这是一个 UIView 动画,尽管同样的事情可以用 CAAnimations 完成。

import UIKit

class ViewController: UIViewController {

    lazy var slider : UISlider = {
        let sld = UISlider(frame: CGRect(x: 30, y: self.view.frame.height - 60, width: self.view.frame.width - 60, height: 20))
        sld.addTarget(self, action: #selector(sliderChanged), for: .valueChanged)
        sld.value = 0
        sld.maximumValue = 1
        sld.minimumValue = 0
        sld.tintColor = UIColor.blue
        return sld
    }()

    lazy var fakeNavBar : UIView = {
        let vw = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 20), size: CGSize(width: self.view.frame.width, height: 60)))
        vw.autoresizingMask = [.flexibleWidth]
        return vw
    }()

    lazy var label1 : UILabel = {
        let lbl = UILabel(frame: CGRect(x: 10, y: 5, width: 10, height: 10))
        lbl.text = "HELLO"
        lbl.font = UIFont.systemFont(ofSize: 17, weight: .light)
        lbl.textColor = .red
        lbl.sizeToFit()
        return lbl
    }()

    lazy var label2 : UILabel = {
        let lbl = UILabel(frame: CGRect(x: 10, y: label1.frame.maxY, width: 10, height: 10))
        lbl.text = "HELLO"
        lbl.font = UIFont.systemFont(ofSize: 40, weight: .bold)
        lbl.textColor = .black
        lbl.sizeToFit()
        return lbl
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.view.addSubview(fakeNavBar)
        self.fakeNavBar.addSubview(label1)
        self.fakeNavBar.addSubview(label2)
        self.view.addSubview(slider)
        doAnimation()

    }

    func doAnimation(){

        self.fakeNavBar.layer.speed = 0
        let snap1 = label1.createImageView()
        self.fakeNavBar.addSubview(snap1)
        label1.isHidden = true

        let snap2 = label2.createImageView()
        self.fakeNavBar.addSubview(snap2)
        label2.isHidden = true

        let scaleForSnap1 = snap2.frame.height/snap1.frame.height
        let scaleForSnap2 = snap1.frame.height/snap2.frame.height

        let snap2Center = snap2.center
        let snap1Center = snap1.center
        snap2.transform = CGAffineTransform(scaleX: scaleForSnap2, y: scaleForSnap2)
        snap2.alpha = 0
        snap2.center = snap1Center

        UIView.animateKeyframes(withDuration: 1.0, delay: 0, options: .calculationModeCubic, animations: {

            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
                snap1.alpha = 0.2
            })

            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
                snap2.alpha = 0.2
            })

            UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
                snap2.alpha = 1
            })

            UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.1, animations: {
                snap1.alpha = 0
            })

            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
                snap1.center = snap2Center
                snap2.transform = .identity
                snap2.center = snap2Center
                snap1.transform = CGAffineTransform(scaleX: scaleForSnap1, y: scaleForSnap1)
            })

        }) { (finished) in
            self.label2.isHidden = false
            snap1.removeFromSuperview()
            snap2.removeFromSuperview()
        }

    }

    @objc func sliderChanged(){
        if slider.value != 1.0{
            fakeNavBar.layer.timeOffset = CFTimeInterval(slider.value)
        }
    }
}



extension UIImage {
    convenience init(view: UIView) {
        UIGraphicsBeginImageContext(view.frame.size)
        view.layer.render(in:UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        self.init(cgImage: image!.cgImage!)
    }
}

extension UIView {
    func createImageView() ->UIImageView{
        let imgView = UIImageView(frame: self.frame)
        imgView.image = UIImage(view: self)
        return imgView
    }
}

结果: updatedGif

关于ios - 如何在 iOS 中正确变形文本?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50876789/

相关文章:

ios - 从 AppDelegate 更改 ViewController

ios - 如何在 Swift 中以编程方式调整 UILabel 行间距?

ios - 使用 resizableImageWithCapInsets 将拉伸(stretch)的 UIImage 设置为 UILabel 的背景

iphone - UILabel + IRR、KRW 和 KHR 货币符号错误

ios - 如何在多个应用程序启动之间拆分音频编码?

ios - 下面的代码是否有潜在的内存泄漏?

c# - Xamarin C# 如何向 AppDelegate 添加接口(interface)或类?

html - 只触发一次动画,ani js scrollreveal

iOS:强制 tableView 单元格在点击时动画更改

android - 如何动画高度变化