iOS开发——登录页面动画、转场动画

来源:互联网 发布:焓湿图计算软件中文版 编辑:程序博客网 时间:2024/05/21 10:56

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!


需求


如下图。DEMO下载地址:https://github.com/YYProgrammer/YYLoginTranslationDemo


动画效果


需求分析


  • 分析方法



下载这个gif动图,用mac默认的打开方式打开这个gif图(双击图片即可),效果如下


01.png


鼠标选中红色箭头所指的位置,然后按住键盘方向键下键,图片会以缓慢的可控的速度播放,便于分析动画的构成。


  • 小tips:macos系统想正常浏览一个gif动图,可以鼠标单击图片后按空格,也可以选择用浏览器打开,gif图会以正常速度播放。


技术点分析


  • 如何生成一个动画让控件执行?


现流行的方式主要有三种:


1、基本动画


2、核心动画


3、三方框架——POP框架(由Facebook开发)


它们的主要差别:


1、控件的位置、大小等是不是真的发生了改变?


基本动画、pop动画,是给控件添加动画(一般也不会有用基本动画给layer添加动画的做法),所有动画完成时,控件的属性已经改变,而核心动画,是给控件的图层(view.layer)添加动画,看似发生了位置大小的变化,实际上控件本身的属性并未改变。


2、它们分别的优劣势


2.1、基本动画


优势:代码简单,代码量少


劣势:功能相对单一


2.2、核心动画的优势


优势:功能强大、流畅性好、连续几个动画之间的衔接度好。流畅主要是因为操作layer是轻量级的,不容易产生动画卡顿的感觉。


劣势:代码量大;容易写错(某些参数没有定义宏,写错了都不知道);如有需要,还要手动在动画完成时将控件的属性同步修改了。


2.3、pop动画的优势


优势:比核心动画代码要简单,最大的优势在于,容易做弹簧效果,所以很多有“Q弹”感觉的都用pop动画做


劣势:要在一个动画完成时开始另一个动画,pop动画不擅长,主要因为它的动画执行时间由"速度"和"弹性系数"两个参数控制,不好直观判断动画执行了多久,而如果在pop动画完成回调的block里提交下一个动画,会不连贯(亲测,原因不详)。


  • 转场动画怎么实现?


明明从A控制器跳往B控制器,各是各的页面,各是各的控件,怎么做到A里的控件变化形成了B的控件的效果?


的确,A和B是两个独立的页面,它们跳转过程需要动画的效果时,需要另外一个呈现于屏幕上的载体(或者称页面)来装那些做动画的控件,然后在动画完成、转场结束时,把这个载体移除掉,宣告转场结束,这个时候把真正的B的页面展示出来。


这就需要转场代理transitioningDelegate发挥作用了,具体做法和原理下文详述。


登录页分解、实现


  • 点击了GET按钮,logo图和logo文字上移


logo移动.gif


思路:


移动属于比较简单的操作,但这个移动效果具有弹簧效果,所以可以采用核心动画中的关键帧动画CAKeyframeAnimation,或者pop动画来实现,这里我用了pop,后面登录失败按钮左右摆动的动画,我用了CAKeyframeAnimation。


代码:


//图片移动动画

POPSpringAnimation *anim4 = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];//kPOPViewFrame表示改变的值是frame

//动画开始的值(.yy_x是我写的分类的语法,等同于.frame.origin.x,其它同理)

anim4.fromValue = [NSValue valueWithCGRect:CGRectMake(self.LoginImage.yy_x, self.LoginImage.yy_y, self.LoginImage.yy_width, self.LoginImage.yy_height)];

//动画结束时的值

anim4.toValue = [NSValue valueWithCGRect:CGRectMake(self.LoginImage.yy_x, self.LoginImage.yy_y-75, self.LoginImage.yy_width, self.LoginImage.yy_height)];

//开始的时间

anim4.beginTime = CACurrentMediaTime()+0.2;

//弹性系数

anim4.springBounciness = YYSpringBounciness;//YYSpringBounciness是我定义的静态变量,值是16.0

//速度

anim4.springSpeed = YYSpringSpeed;//YYSpringSpeed是我定义的静态变量,值是6.0

//加到控件上执行

[self.LoginImage pop_addAnimation:anim4 forKey:nil];

//文字移动动画

POPSpringAnimation *anim5 = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];

anim5.fromValue = [NSValue valueWithCGRect:CGRectMake(self.LoginWord.yy_x, self.LoginWord.yy_y, self.LoginWord.yy_width, self.LoginWord.yy_height)];

anim5.toValue = [NSValue valueWithCGRect:CGRectMake(self.LoginWord.yy_x, self.LoginWord.yy_y-75, self.LoginWord.yy_width, self.LoginWord.yy_height)];

anim5.beginTime = CACurrentMediaTime()+0.2;

anim5.springBounciness = YYSpringBounciness;

anim5.springSpeed = YYSpringSpeed;

[self.LoginWord pop_addAnimation:anim5 forKey:nil];


  • 点击get按钮出现输入框


get按钮形变移动.gif


1、get按钮的变化


思路:


get按钮分别进行了变宽、变宽的同时圆角变小,然后变高,然后向上移动,整个过程颜色由初始颜色变白。由于这是N个动画,有同时执行的,有接着上一步执行的,所以我选择核心动画CABasicAnimation,更容易控制每个动画的执行时间、开始时间,容易衔接得流畅。


代码:


//get背景颜色

CABasicAnimation *changeColor1 = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];

changeColor1.fromValue = (__bridge id)ButtonColor.CGColor;

changeColor1.toValue = (__bridge id)[UIColor whiteColor].CGColor;

changeColor1.duration = 0.8f;

changeColor1.beginTime = CACurrentMediaTime();

//以下两个参数,是为了动画完成后,控件的样子不回到动画前的样子

//因为上文中提到过,核心动画是给layer做动画,控件本身的属性不会变

changeColor1.fillMode = kCAFillModeForwards;

changeColor1.removedOnCompletion = false;

[animView.layer addAnimation:changeColor1 forKey:changeColor1.keyPath];

//get按钮变宽

CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"];

anim1.fromValue = @(CGRectGetWidth(animView.bounds));

anim1.toValue = @(YYScreenW*0.8);

anim1.duration = 0.1;

anim1.beginTime = CACurrentMediaTime();

anim1.fillMode = kCAFillModeForwards;

anim1.removedOnCompletion = false;

[animView.layer addAnimation:anim1 forKey:anim1.keyPath];

//get按钮变高

CABasicAnimation *anim2 = [CABasicAnimation animationWithKeyPath:@"bounds.size.height"];

anim2.fromValue = @(CGRectGetHeight(animView.bounds));

anim2.toValue = @(YYScreenH*0.3);

anim2.duration = 0.1;

anim2.beginTime = CACurrentMediaTime()+0.1;

anim2.fillMode = kCAFillModeForwards;

anim2.removedOnCompletion = false;

[animView.layer addAnimation:anim2 forKey:anim2.keyPath];

//get按钮移动动画

//这里的移动跟logo的移动是同步的,所以用pop

POPSpringAnimation *anim3 = [POPSpringAnimation animationWithPropertyNamed:kPOPViewCenter];

anim3.fromValue = [NSValue valueWithCGRect:CGRectMake(animView.yy_centerX, animView.yy_centerY, animView.yy_width, animView.yy_height)];

anim3.toValue = [NSValue valueWithCGRect:CGRectMake(animView.yy_centerX, animView.yy_centerY-75, animView.yy_width, animView.yy_height)];

anim3.beginTime = CACurrentMediaTime()+0.2;

anim3.springBounciness = YYSpringBounciness;

anim3.springSpeed = YYSpringSpeed;

[animView pop_addAnimation:anim3 forKey:nil];

2、输入框出现、LOGIN按钮出现

思路:

输入框是透明度的改变,LOGIN按钮是大小的改变。

代码:

//账号密码输入框出现

self.userTextField.alpha = 0.0;

self.passwordTextField.alpha = 0.0;

[UIView animateWithDuration:0.4 delay:0.2 options:UIViewAnimationOptionCurveEaseInOut animations:^{

    self.userTextField.alpha = 1.0;

    self.passwordTextField.alpha = 1.0;

} completion:^(BOOL finished) {

     

}];

//login按钮出现动画

self.LoginButton.yy_centerX = YYScreenW*0.5;

self.LoginButton.yy_centerY = YYScreenH*0.7+44+(YYScreenH*0.3-44)*0.5-75;

CABasicAnimation *animLoginBtn = [CABasicAnimation animationWithKeyPath:@"bounds.size"];

animLoginBtn.fromValue = [NSValue valueWithCGSize:CGSizeMake(0, 0)];

animLoginBtn.toValue = [NSValue valueWithCGSize:CGSizeMake(YYScreenW*0.5, 44)];

animLoginBtn.duration = 0.4;

animLoginBtn.beginTime = CACurrentMediaTime()+0.2;

animLoginBtn.fillMode = kCAFillModeForwards;

animLoginBtn.removedOnCompletion = false;

animLoginBtn.delegate = self;//在代理方法(动画完成回调)里,让按钮真正的宽高改变,而不仅仅是它的layer,否则看得到点不到

[self.LoginButton.layer addAnimation:animLoginBtn forKey:animLoginBtn.keyPath];

/** 动画执行结束回调 */

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag

{

    if ([((CABasicAnimation *)anim).keyPath isEqualToString:@"bounds.size"])

    {

        self.LoginButton.bounds = CGRectMake(YYScreenW*0.5, YYScreenH*0.7+44+(YYScreenH*0.3-44)*0.5-75, YYScreenW*0.5, 44);

    }

}


  • 点击LOGIN,按钮转圈


按钮转圈.gif


思路:


点击了LOGIN,按钮先从宽变圆,然后给按钮添加一条半圆的白色圆弧线,然后让这个按钮开始旋转。


代码:


//执行登录按钮转圈动画的view

//为了不影响按钮本身的效果,这里新建一个空间做转圈动画

self.LoginAnimView = [[UIView alloc] initWithFrame:self.LoginButton.frame];

self.LoginAnimView.layer.cornerRadius = 10;

self.LoginAnimView.layer.masksToBounds = YES;

self.LoginAnimView.frame = self.LoginButton.frame;

self.LoginAnimView.backgroundColor = self.LoginButton.backgroundColor;

[self.view addSubview:self.LoginAnimView];

self.LoginButton.hidden = YES;

//把view从宽的样子变圆

CGPoint centerPoint = self.LoginAnimView.center;

CGFloat radius = MIN(self.LoginButton.frame.size.width, self.LoginButton.frame.size.height);

[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{

     

    self.LoginAnimView.frame = CGRectMake(0, 0, radius, radius);

    self.LoginAnimView.center = centerPoint;

    self.LoginAnimView.layer.cornerRadius = radius/2;

    self.LoginAnimView.layer.masksToBounds = YES;

     

}completion:^(BOOL finished) {

     

    //给圆加一条不封闭的白色曲线

    UIBezierPath* path = [[UIBezierPath alloc] init];

    [path addArcWithCenter:CGPointMake(radius/2, radius/2) radius:(radius/2 - 5) startAngle:0 endAngle:M_PI_2 * 2 clockwise:YES];

    self.shapeLayer = [[CAShapeLayer alloc] init];

    self.shapeLayer.lineWidth = 1.5;

    self.shapeLayer.strokeColor = [UIColor whiteColor].CGColor;

    self.shapeLayer.fillColor = self.LoginButton.backgroundColor.CGColor;

    self.shapeLayer.frame = CGRectMake(0, 0, radius, radius);

    self.shapeLayer.path = path.CGPath;

    [self.LoginAnimView.layer addSublayer:self.shapeLayer];

     

    //让圆转圈,实现"加载中"的效果

    CABasicAnimation* baseAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];

    baseAnimation.duration = 0.4;

    baseAnimation.fromValue = @(0);

    baseAnimation.toValue = @(2 * M_PI);

    baseAnimation.repeatCount = MAXFLOAT;

    [self.LoginAnimView.layer addAnimation:baseAnimation forKey:nil];

}];


  • 登录失败按钮抖动


登录失败按钮动画.gif


思路:


这个效果跟pop动画移动后抖动的效果很类似,这里我选择用关键帧动画CAKeyframeAnimation做,它与CABasicAnimation略有不同,CABasicAnimation是从一个值到另一个值,CAKeyframeAnimation是值变化的数组。


代码:


//给按钮添加左右摆动的效果(关键帧动画)

CAKeyframeAnimation *keyFrame = [CAKeyframeAnimation animationWithKeyPath:@"position"];

CGPoint point = self.LoginAnimView.layer.position;

//这个参数就是值变化的数组

keyFrame.values = @[[NSValue valueWithCGPoint:CGPointMake(point.x, point.y)],

                     

                    [NSValue valueWithCGPoint:CGPointMake(point.x - 10, point.y)],

                     

                    [NSValue valueWithCGPoint:CGPointMake(point.x + 10, point.y)],

                     

                    [NSValue valueWithCGPoint:CGPointMake(point.x - 10, point.y)],

                     

                    [NSValue valueWithCGPoint:CGPointMake(point.x + 10, point.y)],

                     

                    [NSValue valueWithCGPoint:CGPointMake(point.x - 10, point.y)],

                     

                    [NSValue valueWithCGPoint:CGPointMake(point.x + 10, point.y)],

                     

                    [NSValue valueWithCGPoint:point]];

//timingFunction意思是动画执行的效果(这个属性玩HTML+CSS的童鞋应该很熟悉吧)

//kCAMediaTimingFunctionEaseInEaseOut表示淡入淡出

keyFrame.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

keyFrame.duration = 0.5f;

[self.LoginButton.layer addAnimation:keyFrame forKey:keyFrame.keyPath];


转场动画的原理和实现方法


上文说到,从A跳向B,需要一个中间载体来做动画,那么怎么得到这个载体呢?


需要用到转场代理transitioningDelegate。


  • 具体做法、步骤:


1、从A控制器跳到B控制器,写跳转的代码时候,赋值代理


YYFirstViewController *vc = [[YYFirstViewController alloc] init];

    vc.transitioningDelegate = self;//也就是这里

    [self presentViewController:vc animated:YES completion:nil];


2、A控制器遵守代理,实现代理方法


//遵守代理

@interface YYLoginViewController () #pragma mark UIViewControllerTransitioningDelegate(转场动画代理)

//这个是B回到A时执行的方法

- (id)animationControllerForDismissedController:(UIViewController *)dismissed

{

    //暂时别纠结YYLoginTranslation是什么,看下文

    YYLoginTranslation *loginTranslation = [[YYLoginTranslation alloc] init];

    return loginTranslation;

}

//这个是A跳到B时执行的方法

- (id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source

{

    YYLoginTranslation *loginTranslation = [[YYLoginTranslation alloc] init];

    return loginTranslation;

}


3、显而易见,上述两个方法需要返回一个遵守了这个代理的对象,所以,现在需要新建一个类遵守这个代理,实现两个代理方法


//类的.h文件

#import #import @interface YYLoginTranslation : NSObject @end

//类的.m文件

#import "YYLoginTranslation.h"

@interface YYLoginTranslation () @end

@implementation YYLoginTranslation

//代码方法-转场时长

- (NSTimeInterval)transitionDuration:(id)transitionContext

{

    return 1.0;

}

//代理方法-转场动画的代码

- (void)animateTransition:(id)transitionContext

{

    //transitionContext:转场上下文

     

    //转场过程中显示的view,所有动画控件都应该加在这上面

    //这就是那个所谓的载体

    UIView* containerView = [transitionContext containerView];

     

    //在这里把要做动画效果的控件往containerView上面加

    //开始开心的做动画

     

    //最后,在动画完成的时候,记得标识转场结束

    [transitionContext completeTransition:YES];

}


4、现在回头看第2步,那个返回的对象,就是我们第三步创建的类的对象。从A跳到B开始时,会先来到第2步中的"这个是A跳到B时执行的方法",根据你返回的对象,去对象中找代理方法,执行里面的代码,也就是第三步中的"代理方法-转场动画的代码"这个方法,这里代码执行结束后,控制器跳转也就完成了。


转场动画分解、实现



转场动图.gif


转场图.png


思路:


如上图AB控制器本来的样子是这样,转场动画需要完成一下操作:


1、LOGO图逐渐消失;


2、LOGO文字逐渐变小、上移至B中头部文字的位置;


3、A控制器的登录框消失、A控制器背景颜色变白;


4、转圈控件经过弧线运动到右下角,白色加号逐渐形成


5、B控制器背景图上移的动画。


下面分析下第4步和第2步的做法。


  • 圆形的弧线位移、加号的出现


圆形的弧线位移、加号的出现.gif


思路:


先用设定一条曲线,然后让圆沿着曲线移动,最后把加号展示出来。


代码:


//设定曲线

CGMutablePathRef path = CGPathCreateMutable();

//开始的点

CGPathMoveToPoint(path, NULL, (circularAnimView.yy_x+circularAnimView.yy_width*0.5), (circularAnimView.yy_y+circularAnimView.yy_height*0.5));

//设置结束的点和拉力点,第三个参数是拉力点

CGPathAddQuadCurveToPoint(path, NULL, YYScreenW*0.9, circularAnimView.yy_y+circularAnimView.yy_height, (originalX+circularAnimView.yy_width*0.5), (originalY+circularAnimView.yy_height*0.5));

CAKeyframeAnimation *animate = [CAKeyframeAnimation animationWithKeyPath:@"position"];

animate.delegate = self;//在动画结束的代理方法中让加号出现

animate.duration = 0.4;

animate.beginTime = CACurrentMediaTime()+0.15;

animate.fillMode = kCAFillModeForwards;

animate.repeatCount = 0;

animate.path = path;//移动路径

animate.removedOnCompletion = NO;

CGPathRelease(path);

[circularAnimView.layer addAnimation:animate forKey:@"circleMoveAnimation"];


生成曲线的原理:


设置开始的点、结束的点、拉力点,首先会从开始点指结束点形成一条直线,然后向拉力点弯曲,就好像,拉力点会“伸出一只手”,把线拉弯。


曲线原理示意.png


加号逐渐绘制的效果:


另一篇文章里说到了这种线逐渐绘制的动画,可以参考一下:http://www.jianshu.com/p/ea319c97f4c3


  • logo文字的缩小、移动



logo文字的形变和移动.gif


思路:


这是一个UILabel,它的形变就不能靠改变frame实现了,因为如果你缩小它的宽度,当宽度不够装内容时,内容会显示不全,显示不下的会用...代替。所以缩小UILabel需要靠专门的形变属性。至于移动就好说了,只需要算准位置。


代码:


CGFloat proportion = toVC.navWord.yy_width / fromVC.LoginWord.yy_width;

CABasicAnimation * LoginWordScale = [CABasicAnimation animationWithKeyPath:@"transform.scale"];

LoginWordScale.fromValue = [NSNumber numberWithFloat:1.0];

LoginWordScale.toValue = [NSNumber numberWithFloat:proportion];

LoginWordScale.duration = 0.4;

LoginWordScale.beginTime = CACurrentMediaTime()+0.15;

LoginWordScale.removedOnCompletion = NO;

LoginWordScale.fillMode = kCAFillModeForwards;

[fromVC.LoginWord.layer addAnimation:LoginWordScale forKey:LoginWordScale.keyPath];

CGPoint newPosition = [toVC.view convertPoint:toVC.navWord.center fromView:toVC.navView];

[UIView animateWithDuration:0.4 delay:0.15 options:UIViewAnimationOptionCurveEaseInOut animations:^{

    fromVC.LoginWord.yy_centerX = newPosition.x;

    fromVC.LoginWord.yy_centerY = newPosition.y;

} completion:^(BOOL finished) {

     

}];


退出登录动画


退出登录.gif


思路:


这个效果比较简单,但同时也比较实用。实现方式就是改变两个控制器view的透明度。


代码:


//transitionContext:转场上下文

//转场过程中显示的view,所有动画控件都应该加在这上面

UIView *containerView = [transitionContext containerView];

//转场的来源控制器

YYLoginViewController* toVC = (YYLoginViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

//转场去往的控制器

YYFirstViewController* fromVC = (YYFirstViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

//做一个淡入淡出的效果

toVC.view.alpha = 0;

[containerView addSubview:toVC.view];

[UIView animateWithDuration:1.0 animations:^{

    fromVC.view.alpha = 0;

} completion:^(BOOL finished) {

}];

[UIView animateWithDuration:0.6 delay:0.4 options:UIViewAnimationOptionCurveEaseInOut animations:^{

    toVC.view.alpha = 1;

} completion:^(BOOL finished) {

    [transitionContext completeTransition:YES];

}];


备注


  • POP框架的手动集成报错的问题

    pop框架推荐使用pods集成,如果要手动集成的话,比较麻烦,由于处理这个问题的时间已经有点久了,集成的麻烦点记不全了,大概就是它框架里的所有头文件的import方式要从<>改成"",还有它好像有个.cpp文件,要把后缀改成.mm,还有什么记不住了。

    如果需要手动集成pop框架,可以下这个demo,里面有手动集成的pop框架,直接把整个文件夹拖走即可。

  • Demo下载地址:https://github.com/YYProgrammer/YYLoginTranslationDemo

    如果下demo的时候能帮忙点个star就太感谢了~



  • 作者:YY程序猿

  • 链接:http://www.jianshu.com/p/a65d3463f4bc

  • iOS开发整理发布,转载请联系作者授权

【点击成为Android大神】