3

我拼命想把 asmallLabel变成 a bigLabel。通过morphing,我的意思是从一个标签转换以下属性以匹配另一个标签的相应属性,并具有平滑的动画:

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

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

iOS 大标题动画

现在我知道去年的 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")
}

这是我得到的:

动画

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

  1. 字体粗细不会动画,但会在动画过程中突然切换。

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

我的问题是:

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

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

4

1 回答 1

2

可能比你想象的更简单。只需对图层或视图进行快照。苹果过渡的视频中的红色文本渗出,因此它们都只是通过快照或只是变换混合在一起。我倾向于对视图进行快照,以免影响下面的真实视图。这是一个 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
    }
}

结果: 更新的GIF

于 2018-06-16T16:10:45.900 回答