- preface
- choose
- create
- start
-
- 1. Task decomposition
- 2. Start with the text layout
- 3. Animation to achieve
-
- Call it a day
- The last
Hello everyone, welcome to sin pit, I am not a well-known programmer, I want to tell you a traditional cross talk show, sorry, said wrong. I want to share a little bit about text and animation. This is not a pure-blood, high-precision, pointy technical article, but hopefully some people will like it as much as they like a biracial beauty.
preface
I remember seeing a great text effect animation, something like this:
I was amazed at that time, and I saw it again by chance recently. I still like it so much. Suppose we now need to implement such a requirement. We usually go to Github to find the wheels (github is basically only you can’t think of, there is no it does not have). Most of the time, no problem. Or a slight problem: Too many wheels to talk about.
choose
Choosing a wheel is like choosing a girl, you don’t know what’s waiting for you —– sin pit xiao Cheng said
Programmers river’s lake, the martial arts of each warrior and routine are not the same, Shaolin, Wudang, Kunlun each party, each flower. The idea of wheels is also different. Some wheels are deep and obscure and powerful, while others are clear and simple. But there is a point is the same, the wrong choice will be pit, but pit pit small problems. Choosing wheels naturally requires great care, both to match the requirements and to be able to fill the holes when they fall (crap, there are bugs, you don’t fill them, who fill them), and to be able to hold them. However, filling pits can not be so simple, first of all, the wheel implementation ideas, code structure, running sequence you have to make clear, what functions are provided, what functions are not provided, you have to understand it. Basically, a complicated wheel will take a long time to study. And that doesn’t include the time you spend in meetings, communicating, bug solving, drinking tea, pouring water, going to the bathroom, smoking, swearing, or teasing product requirements. So, girls, don’t ask if we’re working overtime today, ask if you can leave before you go to bed today. The project is going to be online soon. If Cheng doesn’t work overtime, who will? Uh huh.)
create
If code can play god, the old master’s mass production is not a problem, depending on whether the product manager’s requirements are simulated or flat. —- Sin Pit xiao Cheng said
Since the time cost of choosing a wheel is not cheap, sometimes we can build our own wheel. In fact, the benefits of writing a lot, there is a sense of accomplishment, write good can brag force, write bad pit filling speed. But the question is, what if I haven’t written it before? Not sure what to do? For example, we now need to achieve the above text effect, but do not know how to write, what to do?
Never mind, God created the world also divided into seven steps to go, follow the lead behind the eldest brother to learn, always can’t wrong.
start
Rome is not built in a day, the film is not a one-time shot of —- sin pit xiao Cheng said
Gee, that’s a little too much. Sorry, now we start shooting (Cang Lao prepare, xiao Cheng also prepare, Action) :
1. Task decomposition
From a brief observation we can see immediately that the above animation effect is achieved by animating each character. In iOS, the most common and commonly used text display control is UILabel. There are two kinds of Explicit Animation in iOS: Properties Animation and KeyFrame Animation.
But the UILabel control does not provide control over every character in its Text, so we need to change that. To animate each character, you need properties like frame, bounds, Position, and Transform.
So we need two weapons: a framework for typography, which, needless to say, is TextKit. The other is a class that shows that a single character also has properties like frame, bounds, position, transform, etc. Naturally, we think of CATextLayer.
2. Start with the text layout
TextKit has three classes NSTextStorage, NSLayoutManager, and NSContainer. Together they help us solve the text layout, typesetting work.
-
NSTextStorage: NSMutableAttributedString subclasses, hold text content, when character change, notify NSLayoutManager object
-
NSLayoutManager: our hero, after obtaining text content from NSTextStorage, converts it into the corresponding Glyph, and displays glyph according to the visible Region of NSTextContainer.
-
NSContainer: Identifies a region to lay out the text. This region is used by NSLayoutManager to decide where to break lines
Unfortunately, UILabel does not have these three classes as its own property objects, so we need to solve the problem ourselves:
class TextAnimationLabel: UILabel,NSLayoutManagerDelegate {
let textStorage:NSTextStorage = NSTextStorage(string: "")
let textLayoutManager:NSLayoutManager = NSLayoutManager()
let textContainer:NSTextContainer = NSTextContainer()
}Copy the code
In addition, we need two arrays to store the old character before the text transformation and the new character after the transformation:
var oldCharacterTextLayers:[CATextLayer] = []
var newCharacterTextLayers:[CATextLayer] = []Copy the code
Because we need to use our own textStorage object, we need to override attributes like Text and attributedText.
override var text:String! { get { return super.text } set { super.text = text let attributedText = NSMutableAttributedString(string: newValue) let textRange = NSMakeRange(0,newValue.characters.count) attributedText.setAttributes([NSForegroundColorAttributeName:self.textColor], range: textRange) attributedText.setAttributes([NSFontAttributeName:self.font], range: textRange) let paragraphyStyle = NSMutableParagraphStyle() paragraphyStyle.alignment = self.textAlignment attributedText.addAttributes([NSParagraphStyleAttributeName:paragraphyStyle], range: textRange) self.attributedText = attributedText } } override var attributedText:NSAttributedString! { get { return self.textStorage as NSAttributedString } set{ cleanOutOldCharacterTextLayers() oldCharacterTextLayers = Array(newCharacterTextLayers) textStorage.setAttributedString(newValue) self.startAnimation { () -> () in } self.endAnimation(nil) } }Copy the code
When the text content of the TextStorage changes, a notification send textLayoutManager is triggered to rearrange the layout. Obviously we can set up a CATextLayer for each character creation after the layout is complete and set up the appropriate frame to display the content correctly. We could have a function to do that. And called when Layout Finish is complete.
//Mark:NSLayoutMangerDelegate func layoutManager(layoutManager: NSLayoutManager, didCompleteLayoutForTextContainer textContainer: NSTextContainer? , atEnd layoutFinishedFlag: Bool) { calculateTextLayers() print("\(textStorage.string)") } //MARK:CalculateTextLayer func calculateTextLayers() { }Copy the code
The next main idea is to find each character in the text and the corresponding Glyph rect. Then create the CATextLayer with character and Glyph rect
First we need to have an empty array to hold the new CATextLayer. And get the attributedText of textStorage.
func calculateTextLayers()
{
newCharacterTextLayers.removeAll(keepCapacity:false)
let attributedText = textStorage.string
}Copy the code
Next we need to find the Used Rect of the TextContainer by LayoutManger so that we can center the text vertically, just like a normal Label.
func calculateTextLayers() { newCharacterTextLayers.removeAll(keepCapacity:false) let attributedText = textStorage.string let wordRange = NSMakeRange(0, attributedText.characters.count) let attributedString = self.internalAttributedText(); let layoutRect = textLayoutManager.usedRectForTextContainer(textContainer) var index = wordRange.location let totalLength = NSMaxRange(wordRange) while index < totalLength { ... }}Copy the code
Now we start iterating through each character in the text, creating a glyphRange and using that glyphRange to find the corresponding character, and then we throw the Glyph index to LayoutManager to get the textContainer, Then use container and glyphRange to get glyphRect(note Kerning’s problem here).
let glyphRange = NSMakeRange(index, 1)
let characterRange = textLayoutManager.characterRangeForGlyphRange(glyphRange, actualGlyphRange: nil)
let textContainer = textLayoutManager.textContainerForGlyphAtIndex(index, effectiveRange: nil)
var glyphRect = textLayoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer!)Copy the code
If kerningRange. Location == index, we need to take out the previous textLayer and adjust its Rect width to the far right of the new glyphRect. Make sure glyph is not cropped (compare the two images below)
let kerningRange = textLayoutManager.rangeOfNominallySpacedGlyphsContainingIndex(index) if kerningRange.location == Index && kerningRange. Length > 1 {if newCharacterTextLayers. Count > 0 {/ / if the previous textlayer frame. The size. The width the same large, The current textLayer will block part of the font, Such as "Yes" Y the upper right corner will be cut off part of the let previousLayer = newCharacterTextLayers var frame = [newCharacterTextLayers. EndIndex - 1] previousLayer.frame frame.size.width += CGRectGetMaxX(glyphRect) - CGRectGetMaxX(frame) previousLayer.frame = frame } }Copy the code
Here’s a bit more about Kerning and Glyph. Let’s start with glyph. To put it simply, glyph refers to A specific style of character, but they are not one-to-one correspondence. For example, A letter “A” can be written in different ways.
The “ff” character above is two characters, but glyph is one.
But don’t worry, the powerful LayoutManager provides two methods to help us find the corresponding one from the other.
func characterIndexForGlyphAtIndex(_ glyphIndex: Int) -> Int
func glyphIndexForCharacterAtIndex(_ charIndex: Int) -> IntCopy the code
Now let’s talk about Kerning. Generally, glyph are placed next to glyph in horizontal text, but there may be a slight mismatch between one glyph and another in order to make the text more readable and elegant, as in the following case:
This is why the “Y” above appears incomplete.
Create Textlayer, set vertical center, add to array, index+= CharacterRange.Length, and start the next loop
glyphRect.origin.y += (self.bounds.size.height/2)-(layoutRect.size.height/2)
let textLayer = CATextLayer(frame: glyphRect, string: attributedString.attributedSubstringFromRange(characterRange));
layer.addSublayer(textLayer);
newCharacterTextLayers.append(textLayer);
index += characterRange.lengthCopy the code
3. Animation to achieve
Animating opacity and transform is the main function of animating properties. Opacity and transform allow each font to gradually display and disappear, while transform performs two deformations. One is to move down, the other is to rotate. CABasicAnimation can handle a single property animation, while CAAnimationGroup can handle multiple animation compounding effects.
func groupAnimationWithLayerChanges(old olderLayer:CALayer, new newLayer:CALayer) -> CAAnimationGroup? { var animationGroup:CAAnimationGroup? var animations:[CABasicAnimation] = [CABasicAnimation]() if ! CATransform3DEqualToTransform(olderLayer.transform, newLayer.transform) { let basicAnimation = CABasicAnimation(keyPath: "transform") basicAnimation.fromValue = NSValue(CATransform3D: olderLayer.transform) basicAnimation.toValue = NSValue(CATransform3D: newLayer.transform) animations.append(basicAnimation) } if olderLayer.opacity ! = newLayer.opacity { let basicAnimation = CABasicAnimation(keyPath: "opacity") basicAnimation.fromValue = olderLayer.opacity basicAnimation.toValue = newLayer.opacity animations.append(basicAnimation) } if animations.count > 0 { animationGroup = CAAnimationGroup() animationGroup! .animations = animations } }Copy the code
One thing to be aware of here is implicit Animation. Core Animation is based on the assumption that anything on the screen can (or can) be animated. When we write code, we should have the impression that you just set the layer to a value and don’t add animations. But you’ll see a smooth transition rather than a very abrupt change. This is implicit animation. When we change a property, Core Animation creates an Animation for us. The Animation time depends on the current NSTransaction setting, and the Animation type depends on the layer behavior.
One interesting thing here, to talk about it a little bit more, is when we animate the layer associated with UIView as opposed to a single layer, for example
func changeColor() { CATransaction.begin(); CATransaction. SetAnimationDuration (1.0) CGFloat red = CGFloat (arc4random ()/(CGFloat) INT_MAX); CGFloat green = CGFloat(arc4random() / (CGFloat)INT_MAX); CGFloat blue = CGFloat(arc4random() / (CGFloat)INT_MAX); Self. LayerView. Layer. The backgroundColor = UIColor. (colorWithRed: red, green, green, blue, blue, alpha: 1.0). The CGColor; CATransaction.commit(); }Copy the code
Instead of a smooth transition, the layer color instantly switches to the new value, and implicit animation seems to be turned off.
We know that the most important relationship between UIView and CALayer is that UIView is the delegate of CALayer,
When we change a CALayer property, it calls func actionForKey(_ Event: String) -> CAAction? This method, what happens next is written in the official documentation, is actually the following steps:
-
If the layer has a delegate that implements the actionForLayer:forKey: method, the layer calls that method. The delegate must do one of the following:
1. Return the action object for the given key. 2. Return the NSNull object if it does not handle the action.Copy the code
-
The layer looks in the layer’s actions dictionary for a matching key/action pair.
-
The layer looks in the style dictionary for an actions dictionary for a matching key/action pair.
-
The layer calls the defaultActionForKey: class method to look for any class-defined actions.
UIView acts as a Delegate for its associated layer, implementing actionForLayer(_ Layer: CALayer, forKey Event: String) -> CAAction? UIView returns nil when not in an animation block, and returns a non-null value when in an animation block
print("OutSide:\(self.view.actionForLayer(self.view.layer, forKey: "backgroundColor"))")
UIView.beginAnimations(nil, context: nil)
print("InSide:\(self.view.actionForLayer(self.view.layer,
forKey: "backgroundColor"))")
UIView.commitAnimations()Copy the code
OutSide:Optional()
InSide:Optional()Copy the code
Of course, returning nil is not the only way to disable implicit animation, but this will do
CATransaction.setDisableActions(true)Copy the code
So why say this question? Since implicit animation needs to be turned off before each character is animated, otherwise it will be animated twice, as shown below:
So, we create an oldLayer and then change the corresponding properties to produce a new newLayer. Then create the appropriate animation group and add the explicit animation.
let olderLayer = animationObjc.animatableLayerCopy(layer)
CATransaction.begin()
CATransaction.setDisableActions(true)
newLayer = effectAnimationClosure(layer: layer)
CATransaction.commit()
var animationGroup:CAAnimationGroup?
animationGroup = groupAnimationWithLayerChanges(old: olderLayer, new: newLayer!)
layer.addAnimation(textAniamtionGroup, forKey: textAnimationGroupKey)Copy the code
Call it a day
Well, when all of the above work is done, which is what we saw in the beginning, the code has been uploaded to Github and you can download it here. In fact, the label implemented in this demo has a lot of room for optimization. Such as support for multiple types of animation effects, animation effects can be configured and so on. That’s what I’m going to do next.
I am uneducated, mistakes and omissions are unavoidable, we welcome criticism. If you find a bug, you can make a pull request. If you have a better idea, please tell me, let me progress, I will buy you coffee :).
This is my wechat account (not finished, please read down) :
The last
Unconsciously working for many years, these years the girl became a girl, the girl became the child’s mother. Everyone from QQ space to kill circle of friends. From my girlfriend to my wedding photos to my baby. In recent years, Naruto has lived up to the expectations of the public as a member of the fire shadow (ya also do not invite dinner). People are changing.
Learning to start writing and sharing might be a change I want.
As an unknown programmer, I usually play on Github by myself, thinking that I have no ability to boost morale and no unique skills to share. Until I got a letter from the organization.
Recently, I joined a guild, which gathered all kinds of masters, light skill, internal force, secret weapon, everyone has their own strengths, from time to time to share their unique skills. Still the old saying, don’t look don’t know, a look startled. The world is still big outside, the girl is still beautiful in the city. In the group, everyone is very active, the learning atmosphere is unusually good. In fact, since the start of programmers, so many years, although natural disposition lazy, but self-study did not dare to put down, after all, against the current, not to retreat. There is no denying that studying alone is very boring, but it is also very gratifying to be accompanied by a group of people.
Finally, I would like to quote from that email to express my views on the guild:
Proves the significance of the community, in the Internet age, there are so many people are willing to pay for the accumulation of knowledge and personal growth, knowledge itself to the value of cognition, people connect in a decentralized way, creation and sharing of P2P, may make the accumulation of knowledge and skills, spread to a new height, depth and breadth
I think the reason why I suddenly want to write such a blog is probably because of this. After all, the beneficiaries of knowledge are always those who actively participate in learning and keep thinking.
Well, the last thing I want to say is, yes, you read the quote correctly, this is a paid group, a paid group in the free era, it’s called the Siege Lion’s Path.
Knowledge is expensive, I always think so. Besides, it’s a hard story.