How to properly morph text in iOS?

核能气质少年 提交于 2021-02-07 22:23:29

问题


I'm desperately trying to morph a smallLabel into a bigLabel. By morphing, I mean transforming the following properties from one label to match the respective properties of the other label, with a smooth animation:

  • font size
  • font weight
  • frame (i.e. bounds and position)

The desired effect should look similar to the animation that is applied to the navigation controller's title label when using large titles:

Now I'm aware of last year's WWDC session Advanced Animations with UIKit where they show how to do just that. However, this technique is pretty limited as it's basically just applying a transform to the label's frame and thus it only works if all properties other than the font size are identical.

The technique fails already when one label has a regular font weight and the other a bold weight – those properties do not change when applying a transform. Thus, I decided to dig a little deeper and use Core Animation for morphing.

First, I create a new text layer which I set up to be visually identical with the 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
}

Then, I create the necessary animations and add them to this layer:

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")
}

Here is what I get:

As you can see, it doesn't quite work as intended. There are two issues with this animation:

  1. The font weight doesn't animate but switches abruptly in the middle of the animation process.

  2. While the frame of the (cyan colored) text layer moves and increases in size as expected, the text itself somehow moves towards the lower-left corner of the layer and is cut off from the right side.

My questions are:

1️⃣Why does this happen (especially 2.)?

and

2️⃣How can I achieve the large title morphing behavior – including a font-weight animation – as shown above?


回答1:


Probably something more simple than you think. Just snapshot the layers or views. The red text bleeds in the video of the apple transition so they are both just being blended together either with a snapshot or just a transform. I tend to snapshot views so as to not effect the real view underneath. Here is a UIView animation although the same thing could be done with 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
    }
}

RESULT:



来源:https://stackoverflow.com/questions/50876789/how-to-properly-morph-text-in-ios

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!