iOS学习笔记-动画篇1

自学iOS以来一直没有做笔记的习惯,但是看了很多大牛关于技术总结、探索以及学习的技术博客,感觉还是很有必要的保证学习笔记的积累和更新的。

最近买了一本关于iOS动画的书,书名《A Guide To iOS Animation》,作者是KittenYang,很年轻的一位大神,动画功底非常深。通过对他的书的练习以及自己代码的实践,感觉自己的动画认知和制作水平有了比较大的提高。

说到动画,就离不开贝塞尔曲线。iOS中的基本上只要是复杂动画就一定会用到贝塞尔曲线。

比如一个圆形视图,可以通过给UIView的layer设置conerRadius然后mask或clip来呈现出圆形。也可以直接在layer上调用drawInContext方法通过

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);

CGContextDrawPath(ctx, kCGPathFillStroke);
复制代码

来画圆,其中ABCD是圆的4个端点,两个端点之间通过两个中间点来用贝塞尔曲线连接起来。中间点的位置选择取决于矩形的边长。这样子组成的圆形可扩展性更高,并能实现各种偏移动画。比如利用手指拖拽圆的偏移量使圆压扁,变长,以及阻尼效果。

iOS动画中还有一个重要的对象CADisplayLink,设置好CADisplayLink的target和selector后将其放入runloop,就可以配合贝塞尔曲线精准的显示动画了。由于CADisplayLink绑定的方法每次屏幕刷新都会被调用,而iOS默认屏幕刷新次数是1s/60次(可以设置CADisplayLink的frameInterval属性来控制刷新次数),所以非常适合UI重绘。

_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
 
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];```


就我目前比较浅薄的水平而已,我感觉CoreAnimation(CAPropertyAnimation、CABasicAnimation、CAKeyframeAnimation)可以满足日常工作中的90%的动画要求。

而我看到的那些很牛逼很复杂的动画,只不过是一个个简单的动画组合而成,只不过由于动画时间比较短,所以看着很复杂。通过各种组合动画(duration,delay,timeFunction,damping,velocity...)以及它的基本单位(transition、rotation、scale...)以及合理的参数调节,就能做出优秀的动画了。

俗话说:好动画,是慢慢调出来的~

一个优秀动画的特点除了在短时间内输出足够多的信息量;另一个特点,就是细节决定质量,用户通过自己的观察从而发现这种细节会是一件让用户很有成就感的事情。

就我用动画而言,大多数都要用到这个函数

复制代码

[UIView animateWithDuration: delay: usingSpringWithDamping:initialSpringVelocity: options: animations: completion:];

通过设置damping和初速度来设置弹簧效果,通过options来设置动画显示方式,其他参数就不用一一赘述了。

我做了一个简单的动画,没有用到任何复杂的数学知识,就是单纯的用UIKIT自带的函数实现简单的动画。

首先创建一个bottomView,为了保障程序的健壮性,最好remove掉所有的子视图。

复制代码

bottomView = [[UIView alloc] initWithFrame:CGRectMake(0, SCREENHEIGHT, SCREENWIDTH, HEIGHT)];

bottomView.backgroundColor = [UIColor blueColor];

for (id subview in bottomView.subviews) {

[subview removeFromSuperview]; }

[self.view addSubview:bottomView];

然后给bottomView添加几个按钮,个数随意

复制代码

for (int i = 0; i < BUTTONCOUNT; i++) {

UIButton *button = [[UIButton alloc] init];

CGFloat buttonWidth = SCREENWIDTH / 4;

CGFloat buttonHeight = HEIGHT / 2;

int row = i / 4;

int line = i % 4;

button.frame = CGRectMake(line * buttonWidth, row * buttonHeight, buttonWidth, buttonHeight);

[button setTitle:[NSString stringWithFormat:@"第%d个",i] forState:UIControlStateNormal];

button.backgroundColor = [UIColor redColor];

[bottomView addSubview:button];

}

然后添加一个成员变量triggered,来判断bottomView是否弹出。

复制代码

@implementation ViewController{

BOOL triggered;

UIView *bottomView;

}``` 然后给视图控制器的view(self.view)添加一个triggerButton,通过点击来调用弹出退下的动画。

这里也可以通过UIPanGestureRecognizer给self.view添加手势操作,target设置为self,action设置为@selector:(pan:)然后override掉pan:,通过pan:传入的gestureRecognizer参数可以得到手指滑动的偏移量和滑动状态。然后根据滑动状态觉得bottomView的偏移量代码如下:


-(void)pan:(UIPanGestureRecognizer *)gestureRecognizer{

CGPoint translationPoint = [gestureRecognizer translationInView:self.view];

CGFloat translationY = translationPoint.y;
//手指在屏幕上移动
if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
if (translationY < -20 &&translationY > - 220) {
CGFloat realOffsetY = translationY + 20;
NSLog(@" 111---  %f ---111",realOffsetY );

CGFloat frameY = SCREENHEIGHT + realOffsetY;
[UIView animateWithDuration:1/ 60 animations:^{
bottomView.frame = CGRectMake(0, frameY, SCREENWIDTH, HEIGHT);
}];//中间空40px用来做防止误触的处理
}else if (translationY > 20){
if (triggered) {
CGFloat realOffsetY = translationY - 20;

CGFloat frameY = SCREENHEIGHT-HEIGHT + realOffsetY;
[UIView animateWithDuration:1/ 60 animations:^{
bottomView.frame = CGRectMake(0, frameY, SCREENWIDTH, HEIGHT);
}];
}
}
}//手指离开屏幕
else if (gestureRecognizer.state == UIGestureRecognizerStateCancelled || gestureRecognizer.state == UIGestureRecognizerStateEnded){


if (triggered) {
if (translationY > 50){
[UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:1 options:UIViewAnimationOptionCurveEaseOut animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT, SCREENWIDTH, HEIGHT);
triggered = NO;
}completion:^(BOOL finished) {
}];
}else{
[UIView animateWithDuration:0.3 animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT - HEIGHT, SCREENWIDTH, HEIGHT);
}];
triggered = YES;
}
}else{
if (translationY < - 50) {
[UIView animateWithDuration:0.3 animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT-HEIGHT, SCREENWIDTH, HEIGHT);
triggered = YES;
}];
}else{
[UIView animateWithDuration:0.3 animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT, SCREENWIDTH, HEIGHT);
triggered = NO;
}];
}
}
}
}```
上面的例子最好的做法是把bottomView抽出来,然后预留一个superView的接口,这样子动画的实现全部在bottomView这个类中实现,减轻Controller的负担并降低耦合性。~(>_<)~ 所以将当做一个失败的MVC案例来看吧,毕竟目前讨论的不是设计模式。


接下来是关于CoreAnimatio和转场动画的简单例子。模仿iOS上的GameCenter,首先在xib上创建4个button:
` NSArray *btnAry = [NSArray arrayWithObjects:self.blueBtn,self.orrangeBtn,self.redButton,self.yellowBtn,nil];
`
然后在`viewWillAppear`里设置
复制代码

self.redButton.transform = CGAffineTransformMakeTranslation(-29, 700); self.blueBtn.transform = CGAffineTransformMakeTranslation(20, 700); self.orrangeBtn.transform = CGAffineTransformMakeTranslation(40, 700); self.yellowBtn.transform = CGAffineTransformMakeTranslation(60, 700);

这样子就会有一个从底部升起的动画。
为了保证动画的协调性:
复制代码

for (int i = 0; i < btnAry.count; i ++) { [UIView animateWithDuration:1 delay:i * 0.15 usingSpringWithDamping:1 initialSpringVelocity:0.8 options:UIViewAnimationOptionTransitionCurlDown animations:^{ UIButton *btn = btnAry[i]; btn.transform = CGAffineTransformIdentity; }

然后遍历这4个button,分别设置CAKeyframeAnimation 的keyPath为position(4个button的小范围飘动),scale(4个button的小范围呼吸效应)。(正好验证一句话简单动画叠加可以得到复杂动画)
这里移动的路径用到`                    CGPathAddEllipseInRect(ellipsePath, nil, circleContainer);
`作用是在circleContainer里截取一个内接椭圆的路径。
部分代码如下:

复制代码

if ( i == btnAry.count - 1) {

for (UIButton *btn in btnAry) {

CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; // pathAnimation.calculationMode = kCAAnimationPaced; pathAnimation.fillMode = kCAFillModeForwards; pathAnimation.removedOnCompletion = false; pathAnimation.repeatCount = MAXFLOAT; pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; if (btn == self.yellowBtn) { pathAnimation.duration = 5.0; }else if (btn == self.orrangeBtn){ pathAnimation.duration = 6.0; }else if (btn == self.redButton){ pathAnimation.duration = 7.0; }else if (btn == self.blueBtn){ pathAnimation.duration = 8.0; } //设置button移动路径 CGMutablePathRef ellipsePath = CGPathCreateMutable();

CGRect circleContainer = CGRectInset(btn.frame, btn.frame.size.width/2 - 3 , btn.frame.size.width / 2 - 3);

CGPathAddEllipseInRect(ellipsePath, nil, circleContainer); pathAnimation.path = ellipsePath; [btn.layer addAnimation:pathAnimation forKey:@"myCircleAnimation"];

CAKeyframeAnimation *scaleX = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.x"]; scaleX.values = @[@1.0,@1.1,@1.0]; scaleX.keyTimes = @[@0.0,@0.5,@1.0]; scaleX.repeatCount = MAXFLOAT; scaleX.autoreverses = YES; scaleX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; if (btn == self.yellowBtn) { scaleX.duration = 3; }else if (btn == self.orrangeBtn){ scaleX.duration = 4; }else if (btn == self.redButton){ scaleX.duration = 6; }else if (btn == self.blueBtn){ scaleX.duration = 5; } [btn.layer addAnimation:scaleX forKey:@"scaleXAnimation"];

CAKeyframeAnimation *scaleY = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.y"]; scaleY.values = @[@1.0,@1.1,@1.0]; scaleY.keyTimes = @[@0.0,@0.5,@1.0]; scaleY.repeatCount = MAXFLOAT; scaleY.autoreverses = YES; scaleY.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; if (btn == self.yellowBtn) { scaleY.duration = 4; }else if (btn == self.orrangeBtn){ scaleY.duration = 5; }else if (btn == self.redButton){ scaleY.duration = 2; }else if (btn == self.blueBtn){ scaleY.duration = 3; } [btn.layer addAnimation:scaleY forKey:@"scaleYAnimation"];

}

} }]; }

接下来是转场动画。iOS7开始苹果推出了自定义转场动画,只要是用到CoreAnimation的地方就可以出现在两个Controller切换之间。这里我们就要用到UINavigationControllerDelegate里一个方法进行override:
`- (nullable id <UIViewControllerAnimatedTransitioning>) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC;`

然后在`- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;`这个方法中写所有的转场动画的代码。例如:
复制代码
  • (void)animateTransition:(id )transitionContext{ //containerView是包含两个切换的控制器的视图的容器,一切动画效果都在这上面进行 UIView *containerView = [transitionContext containerView]; //radius是不能为0,否则在containerView上没有承载动画效果的视图 CGFloat radius;

radius = sqrtf((self.startPoint.x * self.startPoint.x) + (containerView.bounds.size.height-self.startPoint.y)*(containerView.bounds.size.height-self.startPoint.y));

CGSize size = CGSizeMake(radius2, radius2); self.bubble = [[UIView alloc]initWithFrame:CGRectMake(0, 0, size.width, size.height)]; self.bubble.center = self.startPoint; self.bubble.layer.cornerRadius = size.width / 2; self.bubble.backgroundColor = self.bubbleColor;

if (self.transitionMode == Present) {

UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];

CGPoint originalCenter = toView.center;

self.bubble.transform = CGAffineTransformMakeScale(0.001, 0.001); //刚开始我也比较疑惑为什么要把toView放在containerView上,后来经过测试,假如不放在上面,动画效果有,但是动画结束后会黑屏。上面注释有写containerView是包含 toView.center = self.startPoint; toView.transform = CGAffineTransformMakeScale(0.001, 0.001); toView.alpha = 0.0; _bubble.alpha = 1.0;

[containerView addSubview:_bubble]; [containerView addSubview:toView];

[UIView animateWithDuration:self.duration animations:^{ _bubble.transform = CGAffineTransformIdentity; toView.transform = CGAffineTransformIdentity; toView.alpha = 1.0f; toView.center = originalCenter; } completion:^(BOOL finished) { [UIView animateWithDuration:0.3 animations:^{ _bubble.alpha = 0.0; }completion:^(BOOL finished) { [_bubble removeFromSuperview]; [transitionContext completeTransition:YES]; }]; }];

}else{ UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];

[containerView addSubview:toView]; [containerView addSubview:self.bubble];

[UIView animateWithDuration:self.duration animations:^{ self.bubble.transform = CGAffineTransformMakeScale(0.001, 0.001);

} completion:^(BOOL finished) { [self.bubble removeFromSuperview]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }];

}

}

总之记住一句话,不要害怕做复杂动画,因为复杂动画就是一个个简单动画堆积而成,仅此而已。

最后贴出demo链接https://github.com/Yang9322/AnimationDemo.git



部分代码引用来自: “A GUIDE TO IOS ANIMATION”. iBooks.
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值