In a recent custom PageControl — KYAnimatedPageControl, I implemented CALayer’s deformable animation and CALayer’s elastic animation.
Review images
Make an outline:
-
The first topic I shared was “How to transform CALayer”. This technique was covered in my previous project ———— KYCuteView, and I also wrote a short blog about how it works. Here’s another example.
-
I’ve done elastic animations that look like jelly before, like this project — KYGooeyMenu. The core technology used is CAKeyframeAnimation, and then set several key frames in different states to achieve the initial elastic effect. However, after all, there are only a few keyframes, and they need to be calculated manually, which is not precise, and the animation is not delicate enough, after all, you can’t manually create 60 keyframes. So, the second topic today — “How to create 60 keyframes with damped vibration functions”, To implement a CALayer that produces something like UIView AnimateWithDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion] the elasticity of the animation.
The text.
How do YOU deform CALayer?
The key technique is simple: you need to “spell” the Layer with multiple Bezier curves. The reason for doing this is obvious, because it allows us to deform.
For example, this little ball in KYAnimatedPageControl, it’s actually drawn like this:
Review images
The ball is composed of arc AB, arc BC, arc CD and arc DA. Each arc is bound to two control points: arc AB is bound to C1 and C2. Arc BC is bound to C3 and C4…..
* How to express each point?
First, A, B, C, and D are the four moving points controlled by the variable contentoffset.x of ScrollView. We can get this variable in real time in -(void)scrollViewDidScroll:(UIScrollView *)scrollView and convert it to a factor between 0 and 1.
_factor = MIN(1, MAX(0, (ABS(scrollView.contentOffset.x - self.lastContentOffset) / scrollView.frame.size.width)));
Copy the code
Assume that the maximum variation distance of A, B, C and D is 2/5 of the diameter of the ball. (self.width * 2/5) * factor) (self.width * 2/5) * factor) (self.width * 2/5) * factor) (self.width * 2/5) * factor) (self.width * 2/5) When factor == 1, the maximum deformation state is reached, and the change distance of the four points is (self.width * 2/5).
Note: according to the sliding direction, we also depend on whether point B or D is moving.
CGPoint pointA = CGPointMake(rectCenter.x ,self.currentRect.origin.y + extra);
CGPoint pointB = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? rectCenter.x + self.currentRect.size.width/2 : rectCenter.x + self.currentRect.size.width/2 + extra*2 ,rectCenter.y);
CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y + self.currentRect.size.height/2 - extra);
CGPoint pointD = CGPointMake(self.scrollDirection == ScrollDirectionLeft ? self.currentRect.origin.x - extra*2 : self.currentRect.origin.x, rectCenter.y);
Copy the code
Then there are control points:
The key is to know a-C1, B-C2, B-C3, c-C4…. The lengths of these horizontal and vertical dashed lines are named offSet. After many attempts, I came to this conclusion:
When offSet is set to diameter divided by 3.6, arcs fit perfectly into arcs. I have a vague feeling that 3.6 is inevitable, which seems to have something to do with 360 degrees. Maybe 3.6 is inevitable through calculation, but I didn’t try.
Therefore, the coordinates of each control point are:
CGPoint c1 = CGPointMake(pointA.x + offset, pointA.y);
CGPoint c2 = CGPointMake(pointB.x, pointB.y - offset);
CGPoint c3 = CGPointMake(pointB.x, pointB.y + offset);
CGPoint c4 = CGPointMake(pointC.x + offset, pointC.y);
CGPoint c5 = CGPointMake(pointC.x - offset, pointC.y);
CGPoint c6 = CGPointMake(pointD.x, pointD.y + offset);
CGPoint c7 = CGPointMake(pointD.x, pointD.y - offset);
CGPoint c8 = CGPointMake(pointA.x - offset, pointA.y);
Copy the code
With an end point and a control point, You can use the method provided in UIBezierPath – (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2; I’ve drawn a line segment.
Reload CALayer’s – (void)drawInContext:(CGContextRef) CTX; Method, draw a pattern inside:
- (void)drawInContext:(CGContextRef)ctx{ .... UIBezierPath* ovalPath = [UIBezierPath bezierPath]; [ovalPath moveToPoint: pointA]; [ovalPath addCurveToPoint:pointB controlPoint1:c1 controlPoint2:c2]; [ovalPath addCurveToPoint:pointC controlPoint1:c3 controlPoint2:c4]; [ovalPath addCurveToPoint:pointD controlPoint1:c5 controlPoint2:c6]; [ovalPath addCurveToPoint:pointA controlPoint1:c7 controlPoint2:c8]; [ovalPath closePath]; CGContextAddPath(ctx, ovalPath.CGPath); CGContextSetFillColorWithColor(ctx, self.indicatorColor.CGColor); CGContextFillPath(ctx); }Copy the code
Now, when you slide the ScrollView, the ball will deform.
How to create 60 keyframes with damped vibration function?
One of the most important factors in the above example is the contentoffset. x variable in ScrollView, without which nothing happens next. However, to obtain this variable, users need to touch and slide to interact. In a certain animation, the user does not have direct interactive input. For example, when the finger leaves, the ball needs to bounce back to its initial state with jelly effect. In this process, the finger has left the screen and there is no input.
As we know, iOS7 apple in UIView (UIViewAnimationWithBlocks) added a new animation elastic factory method:
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
Copy the code
But there is no direct elastic CAAnimation subclass, like CABasicAnimation or CAKeyframeAnimation, to animate CALayer directly. The good news is that iOS9 has added a public CASpringAnimation. But for the sake of compatibility with lower versions and knowledge seeking, let’s see how to manually create an elastic animation for CALayer.
A review of high school physics is required before we begin ———— Damped vibrations, which you can do a little bit of by clicking on the highlighted link.
View pictures View pictures
According to Wikipedia, we can get the general formula of vibration function as follows:
Review images
And of course, this is just a general formula, so we want the graph to go through (0,0) and decay to 1. We can flip the original image 180 degrees around the X-axis, so we can add a minus sign. And then we’re going to shift it up by 1. Therefore, the following function can be obtained with slight deformation:
Review images
Want to see the graph of the function? Desmos is recommended to view the function image online. Copy and paste the formula 1-\left(e^{-5x}\cdot \cos (30x)\right) to see the image.
The improved function graph looks like this:
Review images
It perfectly meets the requirement that our graph goes over (0,0) and the oscillation decays to 1. 5 in the formula is equivalent to the damping coefficient, and the smaller the value, the greater the amplitude; 30 in this formula corresponds to the oscillation frequency, and the higher the number, the more oscillations.
Next you need to convert it to code.
The general idea is to create 60 keyframes (because the maximum screen refresh rate is 60FPS) and assign those 60 frames to the VALUES property of CAKeyframeAnimation.
Generate 60 frames, save it to an array and return it, where //1 creates 60 values using the formula above:
+(NSMutableArray *) animationValues:(id)fromValue toValue:(id)toValue usingSpringWithDamping:(CGFloat)damping InitialSpringVelocity :(CGFloat)velocity duration:(CGFloat)duration{//60 keyframes NSInteger numOfPoints = duration * 60; NSMutableArray *values = [NSMutableArray arrayWithCapacity:numOfPoints]; for (NSInteger i = 0; i < numOfPoints; I++) {[values addObject: @ (0.0)]. } // CGFloat d_value = [toValue floatValue] - [fromValue floatValue]; for (NSInteger point = 0; pointCopy the code
Next create an external class method and return a CAKeyframeAnimation:
+(CAKeyframeAnimation *)createSpring:(NSString *)keypath duration:(CFTimeInterval)duration usingSpringWithDamping:(CGFloat)damping initialSpringVelocity:(CGFloat)velocity fromValue:(id)fromValue toValue:(id)toValue{
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:keypath];
NSMutableArray *values = [KYSpringLayerAnimation animationValues:fromValue toValue:toValue usingSpringWithDamping:damping * dampingFactor initialSpringVelocity:velocity * velocityFactor duration:duration];
anim.values = values;
anim.duration = duration;
return anim;
}
Copy the code
The other key
Above, we created the CAKeyframeAnimation. But who are the values really working for? If you’re familiar with CoreAnimation, yes, it works on the keypath passed in. And those keypath are actually the @property in CALayer. For example, the reason CAKeyframeAnimation rotates the layer when passed keypath to transform.rotation. X is because CAKeyframeAnimation sees that the CALayer has a property called transform. So the animation happens. Now what we need to change is the factor variable in theme 1, so it’s natural to think that we can add a property called Factor to CALayer so that when we add CAKeyframeAnimation to the layer we see that the layer has this factor property, I’m going to copy 60 different frames of values to Factor. FromValue and toValue should be set between 0 and 1:
CAKeyframeAnimation * anim = [KYSpringLayerAnimation createSpring: @ "factor" duration: usingSpringWithDamping 0.8:0.5 InitialSpringVelocity :3 fromValue:@(0.5+[HowManyDistance floatValue]* 1.5) toValue:@(0)]; self.factor = 0; [self addAnimation:anim forKey:@"restoreAnimation"];Copy the code
As a final step, CAKeyframeAnimation changed the desired factor in real time, but we had to notify the screen to refresh so we could see the animation.
+(BOOL)needsDisplayForKey:(NSString *)key{
if ([key isEqual:@"factor"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
Copy the code
The code above tells the screen to refresh in real time when factor changes.
Finally, you need to override the -(id)initWithLayer:(GooeyCircle *)layer method in CALayer. To make sure the animation is coherent, you need to copy the layer of the previous state and all its properties.
-(id)initWithLayer:(GooeyCircle *)layer{
self = [super initWithLayer:layer];
if (self) {
self.indicatorSize = layer.indicatorSize;
self.indicatorColor = layer.indicatorColor;
self.currentRect = layer.currentRect;
self.lastContentOffset = layer.lastContentOffset;
self.scrollDirection = layer.scrollDirection;
self.factor = layer.factor;
}
return self;
}
Copy the code
Conclusion:
Do custom animation to have variables, to have input. For example, when sliding a ScrollView, the sliding distance is the input of the animation, which can be used as a variable of the animation. CAAnimation can be used when there is no interaction. In fact, there is a timer at the bottom of CAAnimation, and the function of the timer is to generate variables, time is the variable, you can generate the changing input, you can see the changing state, connected to the animation.