UIView动画

金钊
2023-12-01

UIView记录

1、UIView提供属性变化和视图转换动画,对于UIView没有提供的动画,就用Core Animation实现;对于没有对应view的layer只能使用Core Animation加动画。
2、当视图内容发生变化,需要调用setNeedsDisplay或setNeedsDisplayInRect:告知系统需要重绘(调用drawRect:),系统会在下一次run loop进行重绘,否则系统一般都不会再调用绘制代码而是直接用上一次绘制结果的缓存。
3、通过在drawRect:中修改transform来进行修改位置,比起重新在新位置创建一个对象要更快且花销更少。
4、使用Core Animation对UIView的layer的修改,会影响UIView;对UIView的修改实际也是对layer的修改。
5、如果一开始给UIView设置了frame,希望保持center不变只修改大小所以给设置bounds添加动画,会发现动画效果是先立刻出现在偏下方,然后渐渐移动到原来的中心点位置,如果修改的是frame而不是bounds就不会有这个奇怪的效果而是会保持中心不变大小渐渐变大或变小。

对属性修改加动画:frame、bound、center、transform、alpha、backgroundColor

[UIView animateWithDuration:1 delay:1 options:0 animations:^{
        /*
         变形:缩放、旋转、平移
         x' = a*x + c*y + tx
         y' = b*x + d*y + ty
         CGAffineTransform = [a, b, c, d, tx, ty]
         x和y是相对中心点的坐标,xy轴正向和屏幕坐标系正向一致(也就是,x轴正向往右,y轴正向往下),比如frame=(50, 50, 200, 100)的左上角的(x,y)=(-100,-50),旋转90度之后变成(50,-100),所以求出(a=0,b=1,c=-1,d=0,tx=0,ty=0)。
        */
        CGAffineTransform transform;
        transform = CGAffineTransformMake(1, 0, -1, 1, 0, 0); // 保持y坐标不变,x轴以上的点往右移,x轴以下的点往左移,移动增量是原y坐标值的绝对值
//        transform = CGAffineTransformMakeRotation(M_PI_2); // [0, 1, -1, 0, 0, 0]
        self.testView.transform = transform;
        } completion:nil];

(由于transform不等于CGAffineTransformIdentity时,frame是无效的,所以如果想恢复变形之后的视图为原来的样子,必须先将transform修改成identity才能修改frame。如果先修改frame再修改为identity则无法恢复原来的样子。)

UIViewAnimationOptions一些选项的使用示例

UIViewAnimationOptionLayoutSubviews

使子视图的大小位置在父视图大小位置变化时,一直保持着相对关系。(使用起来有一些限制,具体看代码示例注释。)

/*--------- 客户端文件中的动画代码 ---------*/
	UIViewAnimationOptions option = UIViewAnimationOptionLayoutSubviews; // 为了测试这个选项,①testOptionView的子视图的frame需要与父视图即testOptionView的大小有关,②在自定制的RCCustomView(即testOptionView类型)中重写-layoutSubviews:并在这个方法中设置子类的frame,①②设置之后才会使这个选项生效,也就是父视图的大小位置动态渐渐改变时,子视图的frame一直保持着和父视图的相对关系比如一直是父视图大小的二分之一减7.5。并且,子视图需要是UIView不能是UILabel,UILabel的frame无法在父视图大小位置变化时保持和父视图的相对关系即高度应该是父视图减10。(即使使用约束NSLayoutConstraint设置子视图和父视图的相对关系,还是无法使UILabel在父视图位置大小变化时保持约束。)
    [UIView animateWithDuration:1 delay:0 options:option animations:^{
        self.testOptionView.frame = CGRectMake(200, 70, 100, 120);
    } completion:nil];


/*--------- RCCustomView.m文件中代码 ---------*/
@implementation RCCustomView
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor darkGrayColor];
        
        _leftView = [[UIView alloc] initWithFrame:CGRectZero];
        self.leftView.backgroundColor = [UIColor greenColor];
        [self addSubview:self.leftView];
        
        _rightView = [[UILabel alloc] initWithFrame:CGRectZero]; // UIViewAnimationOptionLayoutSubviews无法使UILabel在父视图的frame变化时和父视图保持着约束。
        self.rightView.text = @"custom";
        self.rightView.backgroundColor = [UIColor systemPinkColor];
        [self addSubview:self.rightView];
    }
    return self;
}
/** 首次显示本视图,或者本视图的大小改变时都会调用(本视图只有位置改变时不会调用) */
- (void)layoutSubviews {
    // 子视图的frame与父视图的大小有关,才能用来测试UIViewAnimationOptionLayoutSubviews
    self.leftView.frame = CGRectMake(5, 5, CGRectGetWidth(self.bounds)*0.5-7.5, CGRectGetHeight(self.bounds)-10);
    self.rightView.frame = CGRectMake(CGRectGetMidX(self.bounds)+2.5, 5, CGRectGetWidth(self.bounds)*0.5-7.5, CGRectGetHeight(self.bounds)-10);
}
@end

UIViewAnimationOptionBeginFromCurrentState

1、官方文档的说明是“Start the animation from the current setting associated with an already in-flight animation.If this key is not present, all in-flight animations are allowed to finish before the new animation is started. If another animation is not in flight, this key has no effect.”,实际测试结果是,对于alpha动画,option中设置了UIViewAnimationOptionBeginFromCurrentState和不设置的动画效果有区别,而对于frame没有区别。对于alpha动画(animation2),设置了xxCurrentState和不设置都会使一个进行中的alpha动画(animation1)立即结束,而且,如果animation2的动画代码前面修改了alpha值,则设置了xxCurrentState时不会从该alpha值开始变化而没有设置才会。具体看下面代码注释。

/** 用于测试UIViewAnimationOptionBeginFromCurrentState的进行中动画 */
- (void)executeAnimationForUIViewAnimationOptionBeginFromCurrentState {
    UIViewAnimationOptions option;
//    option = UIViewAnimationOptionBeginFromCurrentState;
    option = 0;
    [UIView animateWithDuration:4 delay:0 options:option animations:^{
        self.testView.alpha = 0.5;
        NSLog(@"animation1");
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d (animation1)", NSStringFromSelector(_cmd), finished);
    }];
}
/** 测试UIViewAnimationOptionBeginFromCurrentState */
- (void)executeAnimationWithUIViewAnimationOptionBeginFromCurrentState {
    UIViewAnimationOptions option;
    option = UIViewAnimationOptionBeginFromCurrentState; // 这个选项对frame所加的动画无效,对alpha有效:①不管option是UIViewAnimationOptionBeginFromCurrentState还是0,本动画animation2开始时会使得已经运行中的动画animation1立即结束(前提是animation1是修改alpha而不是frame,如果是frame则不会结束),调用completion且finished=NO,所以xxCurrentState并不影响一个已经运行中的动画是否该立即结束,而这个运行中的动画是否立即结束取决于动画修改的是哪个属性(但是还没有验证过alpha和frame以外的属性)。②如果没有进行中的alpha动画,则option=xxCurrentState使得动画不会从<animateWithDurationxx代码前面设置alpha值>即0.1开始变化,而是从当前屏幕显示的alpha开始变化;option=0使得动画是从0.1开始变化。②如果有进行中的alpha动画,option=0使得动画是从0.1开始变化,而xxCurrentState看起来立即停止了animation1的动画,但是animation2的起始alpha不是animation1结束时的alpha,而是比结束时更接近0.5(animation1的目标值)的alpha值。
//    option = 0;
    self.testView.alpha = 0.1; // 如果将这句代码改成“[UIView animateWithDuration:0 animations:^{self.testView.alpha = 0.1;} completion:^(BOOL finished){[UIView animateWithDuration:xx”给0.1加一个duration=0的动画并在completion中写上animation2的整个代码并且animation2使用xxCurrentState,结果是每次animation2都是从0.1开始,但其实animation2的option不要设置为xxCurrentState就可以达到每次从0.1开始的效果。
    [UIView animateWithDuration:5 delay:0 options:option animations:^{
        self.testView.alpha = 1;
        NSLog(@"animation2");
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d (animation2)", NSStringFromSelector(_cmd), finished);
    }];
}

2、补充:当执行到animateWithDurationxx这个方法时,会立即调用animations这个block里面的代码,但是不一定启动动画比如当设置了延迟1.5时;当并列的第一个动画animation1的block中修改的属性值和第二个动画animation2的block中属性值一样时,第二个动画没有运行,直接调用completion。

UIViewAnimationOptionOverrideInheritedDuration & 嵌套动画

1、嵌套的动画默认会和外层动画的时长一样,除非设置option=xxOverrideInheritedDuration,这样嵌套动画设置的时长才会生效。

	UIViewAnimationOptions option = UIViewAnimationOptionOverrideInheritedDuration;
    [UIView animateWithDuration:6 delay:0 options:0 animations:^{
        self.testView.frame = CGRectMake(150, 50, 100, 120);
        NSLog(@"animation1");
        [UIView animateWithDuration:2 delay:1 options:option animations:^{ // ①嵌套动画,如果delay是0则和外层动画同时开始,如果非0则比外层延迟delay秒;②如果option是UIViewAnimationOptionOverrideInheritedDuration,则嵌套动画运行时长就是设置的2s,如果option=0,则嵌套动画运行时长和外层一样,是6s而不是2s(而且因为启动比外层延迟1s所以结束也会比外层慢1s)。
            self.testView.alpha = 0.1;
            NSLog(@"animation2");
        } completion:^(BOOL finished) {
            NSLog(@"%@: finished=%d (animation2)", NSStringFromSelector(_cmd), finished);
        }];
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d (animation1)", NSStringFromSelector(_cmd), finished);
    }];

2、animations这个block中再创建一个动画,这个嵌套动画会和外层动画同时开始,如果需要设置嵌套动画的curve,需要包括xxOverrideInheritedCurve之后再设置所需curve比如“UIViewAnimationOptionOverrideInheritedCurve|UIViewAnimationOptionCurveLinear”。
3、如果先后创建动画(不在animations也不在completion而是先后两个“[UIView animateWithxx]; [UIView animateWithxx];”),则两个同时启动(除非设置了delay),和嵌套的区别是嵌套动画会继承外层动画的duration和curve。

UIViewAnimationOptionAllowAnimatedContent & transitionWithView动画

1、无法确定UIViewAnimationOptionAllowAnimatedContent代表什么样的动画效果,官方文档说明“Animate the views by changing the property values dynamically and redrawing the view.If this key is not present, the views are animated using a snapshot image.”,但测试过程中option不管设置还是不设置这个选项,transitionWithViewxx这个方法的animations修改view的属性如frame都会有动画效果,而且都不会调用drawRect。

UIViewAnimationOptions option;
    option = UIViewAnimationOptionTransitionCurlUp;
//    option = UIViewAnimationOptionTransitionCurlUp|UIViewAnimationOptionAllowAnimatedContent;
    // transition动画的第一个参数view是容器视图,改变这个容器视图的内容(比如添加一个子视图),然后加上过渡选项比如UIViewAnimationOptionTransitionFlipFromLeft,就会翻转后显示这个容器视图的新样式新内容,如果没有设置过渡选项则容器视图内容改变没有动画而是立即生效;如果在animations中什么都不做不改变容器视图的内容而只添加xxTransitionFlipFromLeft选项,这样容器视图还是有翻转的效果,但是翻转之后内容没有变。
    [UIView transitionWithView:self.testOptionView duration:6 options:option animations:^{
        self.testOptionView.rightView.frame = CGRectMake(30, 15, 67.5, 40); // 不管有没有设置xxAllowAnimatedContent,这个属性变化都有动画效果(rightView原frame是(77.5, 5, 67.5, 40))
        self.testOptionView.leftView.hidden = YES;
        self.testOptionView.centerView.hidden = NO;
        
        self.testOptionView.center = CGPointMake(80, 100); //(testOptionView原frame是(200, 50, 150, 50))①xxCurlUp:如果没有这句代码,则xxCurlUp翻页效果在原视图位置上有投影,并且卷起来的视图vj位置会超出原视图上边界,并且逐渐变透明消失;如果有这句代码,则卷起来的视图vj上部分不显示,而且vj的位置和没有这句代码时的位置不一样,偏上。②xxCurlUp|xxAllowAnimatedContent:如果没有这句代码,效果和①一样;如果有这句代码,vj上部分不显示,而且超出testOptionView新位置的部分不显示。(xxFlipFromLeft没有这个效果) —— 这个测试还是无法确定UIViewAnimationOptionAllowAnimatedContent代表什么效果
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d", NSStringFromSelector(_cmd), finished);
    }];

2、+transitionWithView:duration:options:animations:completion::给第一个参数的view添加过渡动画,比如option设置为UIViewAnimationOptionTransitionFlipFromLeft时,这个view有翻转效果,animations中可以什么都不做,也可以修改view的内容比如添加或者移除子视图或者修改子视图样式,这样翻转之后会显示view新的内容。

UIViewAnimationOptionShowHideTransitionViews & transitionFromView动画

默认情况下-transitionFromView:toView:duration:options:completion:是将fromView从它的父视图中移除,将toView添加到fromView的父视图,如果设置了UIViewAnimationOptionShowHideTransitionViews,则不会从父视图中移除、添加,而是只设置hidden为YES、NO。

UIViewAnimationOptions option;
    option = UIViewAnimationOptionShowHideTransitionViews;
//    option = 0;
//    option = UIViewAnimationOptionShowHideTransitionViews|UIViewAnimationOptionTransitionFlipFromLeft;
    // ①这个动画方法默认会将fromView从它的父视图中移除(调用RCCustomView的willRemoveSubview),将toView添加到fromView的父视图(调用RCCustomView的didAddSubview),添加位置取决于toView的frame。②如果options设置了UIViewAnimationOptionShowHideTransitionViews,则不会将fromView从它的父视图中移除,而是设置这个视图的hidden为YES,而且会将toView的hidden设置为NO,但是不会修改toView的父视图,所以如果toView不在fromView的父视图中则执行完这个动画方法之后还是不在。③可以使用UIViewAnimationOptionTransitionFlipFromLeft这些transition选项,比如xxTransitionFlipFromLeft会给fromView的父视图添加翻转效果。
    [UIView transitionFromView:self.testOptionView.leftView toView:self.testOptionView.centerView duration:5 options:option completion:^(BOOL finished) {
            NSLog(@"%@: finished=%d, leftView.superview=%@, leftView.hidden=%d", NSStringFromSelector(_cmd), finished, self.testOptionView.leftView.superview, self.testOptionView.leftView.hidden);
    }];

UIViewAnimationOptionOverrideInheritedOptions & 弹簧动画

即使不设置UIViewAnimationOptionOverrideInheritedOptions,也不会继承外层动画的options如UIViewAnimationOptionAutoreverse、UIViewAnimationOptionOverrideInheritedDuration等;如果不设置UIViewAnimationOptionOverrideInheritedOptions,且外层动画是弹簧动画+animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:,则嵌套动画继承弹簧效果,如果设置了则不会继承弹簧效果。

UIViewAnimationOptions option;
//    option = UIViewAnimationOptionOverrideInheritedOptions; // 嵌套动画的option设置了xxOverrideInheritedOptions,就不会继承外层的弹簧时间曲线,默认是Linear时间曲线
//    option = 0;
//    option = UIViewAnimationOptionOverrideInheritedCurve|UIViewAnimationOptionCurveEaseInOut; // 设置不生效,还是继承了外层的弹簧效果
    option = UIViewAnimationOptionOverrideInheritedCurve|UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionOverrideInheritedOptions; // 设置生效,嵌套动画时间曲线不是弹簧曲线而是EaseInOut
    [UIView animateWithDuration:5 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:0.5 options:0 animations:^{
        self.testView.frame = CGRectMake(150, 50, 100, 120);
        NSLog(@"animation1");
        [UIView animateWithDuration:1 delay:0 options:option animations:^{ // ①即使不设置UIViewAnimationOptionOverrideInheritedOptions,也不会继承外层的options如UIViewAnimationOptionAutoreverse、UIViewAnimationOptionOverrideInheritedDuration等(不管外层动画是弹簧动画还是transition动画还是普通动画)。②不设置xxOverrideInheritedOptions,会继承外层的弹簧效果。
            self.testOptionView.frame = CGRectMake(150, 50, 100, 120);
            NSLog(@"animation2");
        } completion:^(BOOL finished) {
            NSLog(@"%@: finished=%d (animation2)", NSStringFromSelector(_cmd), finished);
        }];
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d (animation1)", NSStringFromSelector(_cmd), finished);
    }];

UIViewAnimationOptionPreferredFramesPerSecond30

一般不需要设置这个选项,除非有明确的帧率需求。

	UIViewAnimationOptions option;
    option = UIViewAnimationOptionPreferredFramesPerSecond30; // 这个动画效果看起来有点卡顿,而xx60和Default相对平滑点,官方文档说明“It's recommended that you use the default value unless you have identified a specific need for an explicit rate.”,除非需要明确的帧率,否则不需要设置这个选项。
    [UIView animateWithDuration:0.3 delay:0 options:option animations:^{
        self.testView.frame = CGRectMake(250, 550, 200, 100); // 原位置(100, 50, 200, 100)
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d", NSStringFromSelector(_cmd), finished);
    }];

关键帧动画

[UIView animateKeyframesWithDuration:5 delay:0 options:0 animations:^{
        self.testView.alpha = 0.1; // 这个属性变化持续时长5s
        [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.5 animations:^{
            self.testView.frame = CGRectMake(10, 5, 200, 100); // 这个属性变化持续时长5*0.5=2.5s
        }];
        [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.2 animations:^{
            self.testView.bounds = CGRectMake(0, 0, 50, 50);
        }];
        [UIView addKeyframeWithRelativeStartTime:0.7 relativeDuration:0.3 animations:^{
            self.testView.frame = CGRectMake(250, 100, 200, 100);
        }];
    } completion:^(BOOL finished) {
        NSLog(@"%@: finished=%d", NSStringFromSelector(_cmd), finished);
    }];

+performSystemAnimation:onViews:options:animations:completion:

这个动画会修改onViews中的视图的transform为[0.25, 0, 0, 0.25, 0, 0],修改alpha为0,并且从父视图中移除,总体效果是立即放大后缩小且变透明。

[UIView performSystemAnimation:UISystemAnimationDelete onViews:@[self.testOptionView.leftView, self.testOptionView.rightView] options:0 animations:^{
        // 在这个block中,不能修改onViews中的视图属性,可以修改其他视图属性或者置空
//        self.testView.transform = CGAffineTransformMake(0.25, 0, 0, 0.25, 0, 0);
    } completion:nil];

+performWithoutAnimation:

传入animation的block中属性动画会被去掉,属性修改立即生效;对于transition动画,无法去掉CurlUp卷页效果。

	[UIView performWithoutAnimation:^{
        [UIView animateWithDuration:1 delay:0 options:0 animations:^{
        	self.testView.transform = CGAffineTransformMake(1, 0, -1, 1, 0, 0);
    	} completion:nil]; // 外部包含performWithoutAnimation时,属性变化没有动画而是立即生效
        // 看网上的资料,performWithoutAnimation一般用于UITableView的reloadSections:withRowAnimation:。
    }];

+setAnimationsEnabled:

只对调用这句代码之后提交的动画有效。

[UIView animateWithDuration:1 delay:2 options:0 animations:^{
        self.testOptionView.alpha = 0.2;
        NSLog(@"(animation1) areAnimationsEnabled=%d", [UIView areAnimationsEnabled]);
    } completion:^(BOOL finished) {
        NSLog(@"%@: (animation1) finished=%d, areAnimationsEnabled=%d", NSStringFromSelector(_cmd), finished, [UIView areAnimationsEnabled]);
    }];
    [UIView setAnimationsEnabled:NO]; // 设置之后的animation属性修改立即生效没有动画,只对这句代码之后创建的animation有效,而这句之前创建的动画即使延迟还没有启动,在启动时还是有动画效果的
    [UIView animateWithDuration:1 delay:0 options:0 animations:^{
        self.testView.frame = CGRectMake(200, 70, 100, 120);
        NSLog(@"(animation2) areAnimationsEnabled=%d", [UIView areAnimationsEnabled]);
    } completion:^(BOOL finished) {
        NSLog(@"%@: (animation2) finished=%d, areAnimationsEnabled=%d", NSStringFromSelector(_cmd), finished, [UIView areAnimationsEnabled]);
    }];

GitHub代码:RCAnimation

参考文档:
1、 Core Animation Cookbook(OS X)
2、Animation Overview (OS X)
3、View Programming Guide for iOS
4、UIViewCGAffineTransform
5、Animation Programming Guide for Cocoa (OS X)

 类似资料: