如何在iOS中正确变换文字?

时间:2018-06-15 13:39:15

标签: ios animation uilabel core-animation morphing

我拼命试图将smallLabel变身为bigLabel。通过变形,我的意思是从一个标签转换以下属性以匹配另一个标签的相应属性,并使用平滑动画:

  • 字体大小
  • 字体重量
  • 框架(即边界和位置)

在使用大型标题时,所需效果应类似于应用于导航控制器标题标签的动画:

iOS Large Title Animation

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

当一个标签的字体权重为regular而另一个标签的权重为bold时,该技术已失败 - 这些属性在应用转换时不会发生变化。因此,我决定深入挖掘并使用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. 当(青色)文本图层的框架按预期移动并增大尺寸时,文本本身会以某种方式向图层的左下角移动,并从右侧切除。

  3. 我的问题是:

    1️⃣为什么会发生这种情况(尤其是2)?

    2️⃣如何实现大标题变形行为 - 包括字体权重动画 - 如上所示?

1 个答案:

答案 0 :(得分:1)

可能比你想象的更简单。只需对图层或视图进行快照。红色文本在Apple转换的视频中流血,因此它们只是通过快照或仅仅转换混合在一起。我倾向于快照视图,以便不影响下面的真实视图。这是一个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