问题
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:
The font weight doesn't animate but switches abruptly in the middle of the animation process.
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