如何使用ios uibezierpath动画实现一个弹性视图动画

动画绘制线条颜色渐变的折线图_安科开发_安科网
动画绘制线条颜色渐变的折线图
效果图 ....................
现状 折线图的应用比较广泛,为了增强用户体验,很多应用中都嵌入了折线图。折线图可以更加直观的表示数据的变化。网络上有很多绘制折线图的demo,有的也使用了动画,但是线条颜色渐变的折线图的demo少之又少,甚至可以说没有。该Blog阐述了动画绘制线条颜色渐变的折线图的实现方案,以及折线图线条颜色渐变的实现原理,并附以完整的示例。 成果
本人已将折线图封装到了一个UIView子类中,并提供了相应的接口。该自定义折线图视图,基本上可以适用于大部分需要集成折线图的项目。若你遇到相应的需求可以直接将文件拖到项目中,调用相应的接口即可 项目文件中包含了大量的注释代码,若你的需求与折线图的实现效果有差别,那么你可以对项目文件的进行修改,也可以依照思路定义自己的折线图视图
Blog中涉及到的知识点
图层,可以简单的看做一个不接受用户交互的UIView 每个图层都具有一个CALayer类型mask属性,作用与蒙版相似 Blog中主要用到的CALayer子类有
CAGradientLayer,绘制颜色渐变的背景图层 CAShapeLayer,绘制折线图
CAAnimation
核心动画的基类(不可实例化对象),实现动画操作
一个二维的绘图引擎,用来绘制折线(Path)和坐标轴信息(Text)
折线图视图
整个折线图将会被自定义到一个UIView子类中
坐标轴绘制
坐标轴直接绘制到折线图视图上,在自定义折线图视图的 drawRect 方法中绘制坐标轴相关信息(线条和文字) 注意坐标系的转换
线条颜色渐变
失败的方案
开始的时候,为了实现线条颜色渐变,我的思考方向是,如何改变路径(UIBezierPath)的渲染颜色(strokeColor)。但是strokeColor只可以设置一种,所以最终无法实现线条颜色的渐变。
成功的方案
在探索过程中找到了CALayer的CALayer类型的mask()属性,最终找到了解决方案,即:使用UIView对象封装渐变背景视图(frame为折线图视图的减去坐标轴后的frame),创建一个CAGradientLayer渐变图层添加到背景视图上。 创建一个CAShapeLayer对象,用于绘制线条,线条的渲染颜色(strokeColor)为whiteColor,填充颜色(fillColor)为clearColor,从而显示出渐变图层的颜色。将CAShapeLayer对象设置为背景视图的mask属性,即背景视图的蒙版。
使用 UIBezierPath 类来绘制折线 折线转折处尖角的处理,使用 kCALineCapRound 与 kCALineJoinRound 设置折线转折处为圆角 折线起点与终点的圆点的处理,可以直接在 UIBezierPath 对象上添加一个圆,设置远的半径为路径宽度的一半,从而保证是一个实心的圆而不是一个圆环
折线转折处的点
折线转折处点使用一个类来描述(不使用CGPoint的原因是:折线转折处的点需要放到一个数组中)
坐标轴信息
X轴、Y轴的信息分别放到一个数组中 X轴显示的是最近七天的日期,Y轴显示的是最近七天数据变化的幅度
使用CABasicAnimation类来完成绘制折线图时的动画 需要注意的是,折线路径在一开始时需要社会线宽为0,开始绘制时才设置为适当的线宽,保证一开折线路径是隐藏的
在动画结束时,向折线图视图上添加一个标签(UIButton对象),显示折线终点的信息 标签的位置,需要根据折线终点的位置计算
折线转折处的点
使用一个类来描述折线转折处的点,代码如下: // 接口
/** 折线图上的点 */
@interface IDLineChartPoint : NSObject
/** x轴偏移量 */
@property (nonatomic, assign)
/** y轴偏移量 */
@property (nonatomic, assign)
/** 工厂方法 */
+ (instancetype)pointWithX:(float)x andY:(float)y;
@implementation IDLineChartPoint
+ (instancetype)pointWithX:(float)x andY:(float)y {
IDLineChartPoint *point = [[self alloc] init];
自定义折线图视图
折线图视图是一个自定义的UIView子类,代码如下: // 接口
/** 折线图视图 */
@interface IDLineChartView : UIView
/** 折线转折点数组 */
@property (nonatomic, strong) NSMutableArray&IDLineChartPoint *& *pointA
/** 开始绘制折线图 */
- (void)startDrawlineC
@interface IDLineChartView ()
@implementation IDLineChartView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 设置折线图的背景色
self.backgroundColor = [UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1.0];
@end 效果如图
绘制坐标轴信息
与坐标轴绘制相关的常量 /** 坐标轴信息区域宽度 */
static const CGFloat kPadding = 25.0;
/** 坐标系中横线的宽度 */
static const CGFloat kCoordinateLineWith = 1.0; 在分类中添加与坐标轴绘制相关的成员变量 /** X轴的单位长度 */
@property (nonatomic, assign) CGFloat xAxisS
/** Y轴的单位长度 */
@property (nonatomic, assign) CGFloat yAxisS
/** X轴的信息 */
@property (nonatomic, strong) NSMutableArray&NSString *& *xAxisInformationA
/** Y轴的信息 */
@property (nonatomic, strong) NSMutableArray&NSString *& *yAxisInformationA 与坐标轴绘制相关的成员变量的get方法 - (CGFloat)xAxisSpacing {
if (_xAxisSpacing == 0) {
_xAxisSpacing = (self.bounds.size.width - kPadding) / (float)self.xAxisInformationArray.
return _xAxisS
- (CGFloat)yAxisSpacing {
if (_yAxisSpacing == 0) {
_yAxisSpacing = (self.bounds.size.height - kPadding) / (float)self.yAxisInformationArray.
return _yAxisS
- (NSMutableArray&NSString *& *)xAxisInformationArray {
if (_xAxisInformationArray == nil) {
// 创建可变数组
_xAxisInformationArray = [[NSMutableArray alloc] init];
// 当前日期和日历
NSDate *today = [NSDate date];
NSCalendar *currentCalendar = [NSCalendar currentCalendar];
// 设置日期格式
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @&MM-dd&;
// 获取最近一周的日期
NSDateComponents *components = [[NSDateComponents alloc] init];
for (int i = -7; i&0; i++) {
components.day =
NSDate *dayOfLatestWeek = [currentCalendar dateByAddingComponents:components toDate:today options:0];
NSString *dateString = [dateFormatter stringFromDate:dayOfLatestWeek];
[_xAxisInformationArray addObject:dateString];
return _xAxisInformationA
- (NSMutableArray&NSString *& *)yAxisInformationArray {
if (_yAxisInformationArray == nil) {
_yAxisInformationArray = [NSMutableArray arrayWithObjects:@&0&, @&10&, @&20&, @&30&, @&40&, @&50&, nil];
return _yAxisInformationA
} 绘制坐标轴的相关信息 - (void)drawRect:(CGRect)rect {
// 获取上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// x轴信息
[self.xAxisInformationArray enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 计算文字尺寸
UIFont *informationFont = [UIFont systemFontOfSize:10];
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
attributes[NSForegroundColorAttributeName] = [UIColor colorWithRed:158/255.0 green:158/255.0 blue:158/255.0 alpha:1.0];
attributes[NSFontAttributeName] = informationF
CGSize informationSize = [obj sizeWithAttributes:attributes];
// 计算绘制起点
float drawStartPointX = kPadding + idx * self.xAxisSpacing + (self.xAxisSpacing - informationSize.width) * 0.5;
float drawStartPointY = self.bounds.size.height - kPadding + (kPadding - informationSize.height) / 2.0;
CGPoint drawStartPoint = CGPointMake(drawStartPointX, drawStartPointY);
// 绘制文字信息
[obj drawAtPoint:drawStartPoint withAttributes:attributes];
[self.yAxisInformationArray enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 计算文字尺寸
UIFont *informationFont = [UIFont systemFontOfSize:10];
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
attributes[NSForegroundColorAttributeName] = [UIColor colorWithRed:158/255.0 green:158/255.0 blue:158/255.0 alpha:1.0];
attributes[NSFontAttributeName] = informationF
CGSize informationSize = [obj sizeWithAttributes:attributes];
// 计算绘制起点
float drawStartPointX = (kPadding - informationSize.width) / 2.0;
float drawStartPointY = self.bounds.size.height - kPadding - idx * self.yAxisSpacing - informationSize.height * 0.5;
CGPoint drawStartPoint = CGPointMake(drawStartPointX, drawStartPointY);
// 绘制文字信息
[obj drawAtPoint:drawStartPoint withAttributes:attributes];
// 横向标线
CGContextSetRGBStrokeColor(context, 231 / 255.0, 231 / 255.0, 231 / 255.0, 1.0);
CGContextSetLineWidth(context, kCoordinateLineWith);
CGContextMoveToPoint(context, kPadding, self.bounds.size.height - kPadding - idx * self.yAxisSpacing);
CGContextAddLineToPoint(context, self.bounds.size.width, self.bounds.size.height - kPadding - idx * self.yAxisSpacing);
CGContextStrokePath(context);
} 效果如图
渐变背景视图
在分类中添加与背景视图相关的常量 /** 渐变背景视图 */
@property (nonatomic, strong) UIView *gradientBackgroundV
/** 渐变图层 */
@property (nonatomic, strong) CAGradientLayer *gradientL
/** 颜色数组 */
@property (nonatomic, strong) NSMutableArray *gradientLayerC 在初始化方法中添加调用设置背景视图方法的代码 [self drawGradientBackgroundView]; 设置渐变视图方法的具体实现 - (void)drawGradientBackgroundView {
// 渐变背景视图(不包含坐标轴)
self.gradientBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(kPadding, 0, self.bounds.size.width - kPadding, self.bounds.size.height - kPadding)];
[self addSubview:self.gradientBackgroundView];
/** 创建并设置渐变背景图层 */
//初始化CAGradientlayer对象,使它的大小为渐变背景视图的大小
self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.frame = self.gradientBackgroundView.
//设置渐变区域的起始和终止位置(范围为0-1),即渐变路径
self.gradientLayer.startPoint = CGPointMake(0, 0.0);
self.gradientLayer.endPoint = CGPointMake(1.0, 0.0);
//设置颜色的渐变过程
self.gradientLayerColors = [NSMutableArray arrayWithArray:@[(__bridge id)[UIColor colorWithRed:253 / 255.0 green:164 / 255.0 blue:8 / 255.0 alpha:1.0].CGColor, (__bridge id)[UIColor colorWithRed:251 / 255.0 green:37 / 255.0 blue:45 / 255.0 alpha:1.0].CGColor]];
self.gradientLayer.colors = self.gradientLayerC
//将CAGradientlayer对象添加在我们要设置背景色的视图的layer层
[self.gradientBackgroundView.layer addSublayer:self.gradientLayer];
} 效果如图
在分类中添加与折线绘制相关的成员变量 /** 折线图层 */
@property (nonatomic, strong) CAShapeLayer *lineChartL
/** 折线图终点处的标签 */
@property (nonatomic, strong) UIButton *tapB 在初始化方法中添加调用设置折线图层方法的代码 [self setupLineChartLayerAppearance]; 设置折线图层方法的具体实现 - (void)setupLineChartLayerAppearance {
/** 折线路径 */
UIBezierPath *path = [UIBezierPath bezierPath];
[self.pointArray enumerateObjectsUsingBlock:^(IDLineChartPoint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == 0) {
[path moveToPoint:CGPointMake(self.xAxisSpacing * 0.5 + (obj.x - 1) * self.xAxisSpacing, self.bounds.size.height - kPadding - obj.y * self.yAxisSpacing)];
[path addLineToPoint:CGPointMake(self.xAxisSpacing * 0.5 + (obj.x - 1) * self.xAxisSpacing, self.bounds.size.height - kPadding - obj.y * self.yAxisSpacing)];
// 折线起点和终点位置的圆点
if (idx == 0 || idx == self.pointArray.count - 1) {
[path addArcWithCenter:CGPointMake(self.xAxisSpacing * 0.5 + (obj.x - 1) * self.xAxisSpacing, self.bounds.size.height - kPadding - obj.y * self.yAxisSpacing) radius:2.0 startAngle:0 endAngle:2 * M_PI clockwise:YES];
/** 将折线添加到折线图层上,并设置相关的属性 */
self.lineChartLayer = [CAShapeLayer layer];
self.lineChartLayer.path = path.CGP
self.lineChartLayer.strokeColor = [UIColor whiteColor].CGC
self.lineChartLayer.fillColor = [[UIColor clearColor] CGColor];
// 默认设置路径宽度为0,使其在起始状态下不显示
self.lineChartLayer.lineWidth = 0;
self.lineChartLayer.lineCap = kCALineCapR
self.lineChartLayer.lineJoin = kCALineJoinR
// 设置折线图层为渐变图层的mask
self.gradientBackgroundView.layer.mask = self.lineChartL
} 效果如图(初始状态不显示折线)
动画的开始与结束
动画开始 /** 动画开始,绘制折线图 */
- (void)startDrawlineChart {
// 设置路径宽度为4,使其能够显示出来
self.lineChartLayer.lineWidth = 4;
// 移除标签,
if ([self.subviews containsObject:self.tapButton]) {
[self.tapButton removeFromSuperview];
// 设置动画的相关属性
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@&strokeEnd&];
pathAnimation.duration = 2.5;
pathAnimation.repeatCount = 1;
pathAnimation.removedOnCompletion = NO;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
// 设置动画代理,动画结束时添加一个标签,显示折线终点的信息
pathAnimation.delegate =
[self.lineChartLayer addAnimation:pathAnimation forKey:@&strokeEnd&];
} 动画结束,添加标签 /** 动画结束时,添加一个标签 */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if (self.tapButton == nil) { // 首次添加标签(避免多次创建和计算)
CGRect tapButtonFrame = CGRectMake(self.xAxisSpacing * 0.5 + ([self.pointArray[self.pointArray.count - 1] x] - 1) * self.xAxisSpacing + 8, self.bounds.size.height - kPadding - [self.pointArray[self.pointArray.count - 1] y] * self.yAxisSpacing - 34, 30, 30);
self.tapButton = [[UIButton alloc] initWithFrame:tapButtonFrame];
self.tapButton.enabled = NO;
[self.tapButton setBackgroundImage:[UIImage imageNamed:@&bubble&] forState:UIControlStateDisabled];
[self.tapButton.titleLabel setFont:[UIFont systemFontOfSize:10]];
[self.tapButton setTitle:@&20& forState:UIControlStateDisabled];
[self addSubview:self.tapButton];
集成折线图视图
创建折线图视图
添加成员变量 /** 折线图 */
@property (nonatomic, strong) IDLineChartView *lineCharV 在viewDidLoad方法中创建折线图并添加到控制器的view上 self.lineCharView = [[IDLineChartView alloc] initWithFrame:CGRectMake(35, 164, 340, 170)];
[self.view addSubview:self.lineCharView];
添加开始绘制折线图视图的按钮
添加成员变量 /** 开始绘制折线图按钮 */
@property (nonatomic, strong) UIButton *drawLineChartB 在viewDidLoad方法中创建开始按钮并添加到控制器的view上 self.drawLineChartButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.drawLineChartButton.frame = CGRectMake(180, 375, 50, 44);
[self.drawLineChartButton setTitle:@&开始& forState:UIControlStateNormal];
[self.drawLineChartButton addTarget:self action:@selector(drawLineChart) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.drawLineChartButton]; 开始按钮的点击事件 // 开始绘制折线图
- (void)drawLineChart {
[self.lineCharView startDrawlineChart];
声明:若需要工程文件,请在评论中联系我,非常愿意与广大技术爱好者沟通交流。下一篇博客将会介绍如何使用UICollectionView实现具有签到功能的日历1 视图的概念:视图的用处。因为在中国有联通和电信两个网络运营商,举个例子。为什么
在前面的文章中我们讲述了Android动画之视图动画学习了怎么对一个view实现动画,可以实现
Android动画之Drawable Animation
上一篇我们讲解了视图动画View Animation,也就是TweenAnimation。
三层架构:表现层:采用MVC模式:M 模型 :实体类,作为数据封装和数据传输的介质,V 视图
surfaceView是视图(View)的继承类,这个视图里内嵌了一个专门用于绘制的surface。我们可以
在Word2007中提供了5种视图供用户选择,这5种视图包括页面视图、阅读版式视图、Web版式视
利用大纲视图快速将Word中的文字转换为PPT内容。小伙伴们,大家好!!我是你们的好朋友
PPT创建或自定义的幻灯片母版可以达到随心所欲。1 打开一个空演示文稿,然后在“视图”
1,RestoreViewPhase(重载视图阶段):如名所述,这一阶段重载画面的视图结构。首先试图从s
Oracle谓词推入:所谓推入,是把库表中判断的条件推入进视图中进行判断。后续不再对库表
为啥要给input增加视图呢?不增加的话可能会报错、程序无法继续运行。加上的话就算图片&
观察者模式是对象的行为模式,又叫发布-订阅(Publish Subscribe)模式、模型-视图(Model View)模式
概念:VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的
由于手机屏幕的高度有限,当普通布局放不下现实和的内容时,ScrollView视图(滚动视图)
连载|iOS交互设计基础之内容视图(三)。每个活动表示一个系统提供的或自定义的服务—
1 标签视图TagView直接继承TextView,这样有几个好处:不用去重写onMeasure()接口, 不用自己绘
UIBezierPath用于定义一个由直线 曲线组合而成的路径, 并且可以在自定义视图中渲染该路径
补间动画没有真正改变View的属性,仅仅改变了视图的外观。比如先给一个View设置点击事件
Spring mvc框架相信很多人都很熟悉了,关于这方面的资料也是一搜一大把。但是感觉讲的都
SQL数据定义语言主要用来定义逻辑结构,包括定义基表,视图和索引。定义表:Create table ([
热门文章热门标签
10月26日 |
10月26日 |
10月26日 |
10月26日 |
10月26日 |
10月26日 |
10月26日 |
10月26日 |iOS Core Animation Advanced Techniques(五):图层时间和缓冲
时间: 18:56:17
&&&& 阅读:271
&&&& 评论:
&&&& 收藏:0
标签:图层时间
时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克
在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。
CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。
持续和重复
我们在第八章&显式动画&中简单提到过duration(CAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。
这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做repeatCount,代表动画重复的迭代次数。如果duration是2,repeatCount设为3.5(三个半迭代),那么完整的动画时长将是7秒。
duration和repeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了&默认&,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。
清单9.1 测试duration和repeatCount
@interface&ViewController&()
@property&(nonatomic,&weak)&IBOutlet&UIView&*containerV
@property&(nonatomic,&weak)&IBOutlet&UITextField&*durationF
@property&(nonatomic,&weak)&IBOutlet&UITextField&*repeatF
@property&(nonatomic,&weak)&IBOutlet&UIButton&*startB
@property&(nonatomic,&strong)&CALayer&*shipL
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&self.shipLayer&=&[CALayer&layer];
&&&&self.shipLayer.frame&=&CGRectMake(0,&0,&128,&128);
&&&&self.shipLayer.position&=&CGPointMake(150,&150);
&&&&self.shipLayer.contents&=&(__bridge&id)[UIImage&imageNamed:&@"Ship.png"].CGI
&&&&[self.containerView.layer&addSublayer:self.shipLayer];
-&(void)setControlsEnabled:(BOOL)enabled
&&&&for&(UIControl&*control&in&@[self.durationField,&self.repeatField,&self.startButton])&{
&&&&&&&&control.enabled&=&
&&&&&&&&control.alpha&=&enabled?&1.0f:&0.25f;
-&(IBAction)hideKeyboard
&&&&?[self.durationField&resignFirstResponder];
&&&&[self.repeatField&resignFirstResponder];
-&(IBAction)start
&&&&CFTimeInterval&duration&=&[self.durationField.text&doubleValue];
&&&&float&repeatCount&=&[self.repeatField.text&floatValue];
&&&&CABasicAnimation&*animation&=&[CABasicAnimation&animation];
&&&&animation.keyPath&=&@"transform.rotation";
&&&&animation.duration&=&
&&&&animation.repeatCount&=&repeatC
&&&&animation.byValue&=&@(M_PI&*&2);
&&&&animation.delegate&=&
&&&&[self.shipLayer&addAnimation:animation&forKey:@"rotateAnimation"];
&&&&[self&setControlsEnabled:NO];
-&(void)animationDidStop:(CAAnimation&*)anim&finished:(BOOL)flag
&&&&[self&setControlsEnabled:YES];
图9.1 演示duration和repeatCount的测试程序
创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做autoreverses的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它(图9.2)。
图9.2 摆动门的动画
对门进行摆动的代码见清单9.2。我们用了autoreverses来使门在打开后自动关闭,在这里我们把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCount为INFINITY也有同样的效果。注意repeatCount和repeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。
清单9.2 使用autoreverses属性实现门的摇摆
@interface&ViewController&()
@property&(nonatomic,&weak)&UIView&*containerV
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&CALayer&*doorLayer&=&[CALayer&layer];
&&&&doorLayer.frame&=&CGRectMake(0,&0,&128,&256);
&&&&doorLayer.position&=&CGPointMake(150&-&64,&150);
&&&&doorLayer.anchorPoint&=&CGPointMake(0,&0.5);
&&&&doorLayer.contents&=&(__bridge&id)[UIImage&imageNamed:&@"Door.png"].CGI
&&&&[self.containerView.layer&addSublayer:doorLayer];
&&&&CATransform3D&perspective&=&CATransform3DI
&&&&perspective.m34&=&-1.0&/&500.0;
&&&&self.containerView.layer.sublayerTransform&=&
&&&&CABasicAnimation&*animation&=&[CABasicAnimation&animation];
&&&&animation.keyPath&=&@"transform.rotation.y";
&&&&animation.toValue&=&@(-M_PI_2);
&&&&animation.duration&=&2.0;
&&&&animation.repeatDuration&=&INFINITY;
&&&&animation.autoreverses&=&YES;
&&&&[doorLayer&addAnimation:animation&forKey:nil];
每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。
beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。
speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。
timeOffset和beginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。
和beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。
可以用清单9.3的测试程序验证一下,设置speed和timeOffset滑块到随意的值,然后点击播放来观察效果(见图9.3)
清单9.3 测试timeOffset和speed属性
@interface&ViewController&()
@property&(nonatomic,&weak)&IBOutlet&UIView&*containerV
@property&(nonatomic,&weak)&IBOutlet&UILabel&*speedL
@property&(nonatomic,&weak)&IBOutlet&UILabel&*timeOffsetL
@property&(nonatomic,&weak)&IBOutlet&UISlider&*speedS
@property&(nonatomic,&weak)&IBOutlet&UISlider&*timeOffsetS
@property&(nonatomic,&strong)&UIBezierPath&*bezierP
@property&(nonatomic,&strong)&CALayer&*shipL
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&self.bezierPath&=&[[UIBezierPath&alloc]&init];
&&&&[self.bezierPath&moveToPoint:CGPointMake(0,&150)];
&&&&[self.bezierPath&addCurveToPoint:CGPointMake(300,&150)&controlPoint1:CGPointMake(75,&0)&controlPoint2:CGPointMake(225,&300)];
&&&&CAShapeLayer&*pathLayer&=&[CAShapeLayer&layer];
&&&&pathLayer.path&=&self.bezierPath.CGP
&&&&pathLayer.fillColor&=&[UIColor&clearColor].CGC
&&&&pathLayer.strokeColor&=&[UIColor&redColor].CGC
&&&&pathLayer.lineWidth&=&3.0f;
&&&&[self.containerView.layer&addSublayer:pathLayer];
&&&&self.shipLayer&=&[CALayer&layer];
&&&&self.shipLayer.frame&=&CGRectMake(0,&0,&64,&64);
&&&&self.shipLayer.position&=&CGPointMake(0,&150);
&&&&self.shipLayer.contents&=&(__bridge&id)[UIImage&imageNamed:&@"Ship.png"].CGI
&&&&[self.containerView.layer&addSublayer:self.shipLayer];
&&&&[self&updateSliders];
-&(IBAction)updateSliders
&&&&CFTimeInterval&timeOffset&=&self.timeOffsetSlider.
&&&&self.timeOffsetLabel.text&=&[NSString&stringWithFormat:@"%0.2f",&imeOffset];
&&&&float&speed&=&self.speedSlider.
&&&&self.speedLabel.text&=&[NSString&stringWithFormat:@"%0.2f",&speed];
-&(IBAction)play
&&&&CAKeyframeAnimation&*animation&=&[CAKeyframeAnimation&animation];
&&&&animation.keyPath&=&@"position";
&&&&animation.timeOffset&=&self.timeOffsetSlider.
&&&&animation.speed&=&self.speedSlider.
&&&&animation.duration&=&1.0;
&&&&animation.path&=&self.bezierPath.CGP
&&&&animation.rotationMode&=&kCAAnimationRotateA
&&&&animation.removedOnCompletion&=&NO;
&&&&[self.shipLayer&addAnimation:animation&forKey:@"slide"];
图9.3 测试时间偏移和速度的简单的应用程序
对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?
一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章&隐式动画&,模型图层和呈现图层的解释)。
另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。
这种行为就交给开发者了,它可以被CAMediaTiming的fillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:
kCAFillModeForwards&
kCAFillModeBackwards&
kCAFillModeBoth&
kCAFillModeRemoved
默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。
这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。
层级关系时间
在第三章&图层几何学&中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用CAAnimationGroup实例)。
对CALayer或者CAGroupAnimation调整duration和repeatCount/repeatDuration属性并不会影响到子动画。但是beginTime,timeOffset和speed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayer和CAGroupAnimation的speed属性将会对动画以及子动画速度应用一个缩放的因子。
全局时间和本地时间
CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间(&马赫&实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用CACurrentMediaTime函数来访问马赫时间:
CFTimeInterval&time&=&CACurrentMediaTime();
这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。
因此马赫时间对长时间测量并不有用。比如用CACurrentMediaTime去更新一个实时闹钟并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。
每个CALayer和CAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTime,timeOffset和speed属性计算。就和转换不同图层之间坐标关系一样,CALayer同样也提供了方法来转换不同图层之间的本地时间。如下:
-&(CFTimeInterval)convertTime:(CFTimeInterval)t&fromLayer:(CALayer&*)l;&
-&(CFTimeInterval)convertTime:(CFTimeInterval)t&toLayer:(CALayer&*)l;
当用来同步不同图层之间有不同的speed,timeOffset和beginTime的动画,这些方法会很有用。
暂停,倒回和快进
设置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用-animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。
如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。
一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。可以在app delegate设置如下进行验证:
self.window.layer.speed&=&100;
你也可以通过这种方式来减速,但其实也可以在模拟器通过切换慢速动画来实现。
timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。
举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。
因为在动画添加到图层之后不能再做修改了,我们来通过调整layer的timeOffset达到同样的效果(清单9.4)。
清单9.4 通过触摸手势手动控制动画
@interface&ViewController&()
@property&(nonatomic,&weak)&UIView&*containerV
@property&(nonatomic,&strong)&CALayer&*doorL
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&self.doorLayer&=&[CALayer&layer];
&&&&self.doorLayer.frame&=&CGRectMake(0,&0,&128,&256);
&&&&self.doorLayer.position&=&CGPointMake(150&-&64,&150);
&&&&self.doorLayer.anchorPoint&=&CGPointMake(0,&0.5);
&&&&self.doorLayer.contents&=&(__bridge&id)[UIImage&imageNamed:@"Door.png"].CGI
&&&&[self.containerView.layer&addSublayer:self.doorLayer];
&&&&CATransform3D&perspective&=&CATransform3DI
&&&&perspective.m34&=&-1.0&/&500.0;
&&&&self.containerView.layer.sublayerTransform&=&
&&&&UIPanGestureRecognizer&*pan&=&[[UIPanGestureRecognizer&alloc]&init];
&&&&[pan&addTarget:self&action:@selector(pan:)];
&&&&[self.view&addGestureRecognizer:pan];
&&&&self.doorLayer.speed&=&0.0;
&&&&CABasicAnimation&*animation&=&[CABasicAnimation&animation];
&&&&animation.keyPath&=&@"transform.rotation.y";
&&&&animation.toValue&=&@(-M_PI_2);
&&&&animation.duration&=&1.0;
&&&&[self.doorLayer&addAnimation:animation&forKey:nil];
-&(void)pan:(UIPanGestureRecognizer&*)pan
&&&&CGFloat&x&=&[pan&translationInView:self.view].x;
&&&&x&/=&200.0f;
&&&&CFTimeInterval&timeOffset&=&self.doorLayer.timeO
&&&&timeOffset&=&MIN(0.999,&MAX(0.0,&timeOffset&-&x));
&&&&self.doorLayer.timeOffset&=&timeO
&&&&[pan&setTranslation:CGPointZero&inView:self.view];
这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来直接设置门的transform会更简单。
在这个例子中的确是这样,但是对于比如说关键这这样更加复杂的情况,或者有多个图层的动画组,相对于实时计算每个图层的属性而言,这就显得方便的多了。
在这一章,我们了解了CAMediaTiming协议,以及Core Animation用来操作时间控制动画的机制。在下一章,我们将要接触缓冲,另一个用来使动画更加真实的操作时间的技术。
--------------------------------------------------------------------------------------------------------------------------------------------------------缓冲
生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿
在第九章&图层时间&中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如何对你的动画控制和自定义缓冲曲线。
动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来:
velocity = change / time
这里的变化可以指的是一个物体移动的距离,时间指动画持续的时长,用这样的一个移动可以更加形象的描述(比如position和bounds属性的动画),但实际上它应用于任意可以做动画的属性(比如color和opacity)。
上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章&显式动画&的情况),对于这种恒定速度的动画我们称之为&线性步调&,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。
考虑一个场景,一辆车行驶在一定距离内,它并不会一开始就以60mph的速度行驶,然后到达终点后突然变成0mph。一是因为需要无限大的加速度(即使是最好的车也不会在0秒内从0跑到60),另外不然的话会干死所有乘客。在现实中,它会慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下来。
那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。
现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。
CAMediaTimingFunction
那么该如何使用缓冲方程式呢?首先需要设置CAAnimation的timingFunction属性,是CAMediaTimingFunction类的一个对象。如果想改变隐式动画的计时函数,同样也可以使用CATransaction的+setAnimationTimingFunction:方法。
这里有一些方式来创建CAMediaTimingFunction,最简单的方式是调用+timingFunctionWithName:的构造方法。这里传入如下几个常量之一:
kCAMediaTimingFunctionLinear&
kCAMediaTimingFunctionEaseIn&
kCAMediaTimingFunctionEaseOut&
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimation的timingFunction属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。
kCAMediaTimingFunctionEaseIn常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。
kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。
kCAMediaTimingFunctionEaseInEaseOut创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。
最后还有一个kCAMediaTimingFunctionDefault,它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式的CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。
你可以使用一个简单的测试工程来实验一下(清单10.1),在运行之前改变缓冲函数的代码,然后点击任何地方来观察图层是如何通过指定的缓冲移动的。
清单10.1 缓冲函数的简单测试
@interface&ViewController&()
@property&(nonatomic,&strong)&CALayer&*colorL
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&self.colorLayer&=&[CALayer&layer];
&&&&self.colorLayer.frame&=&CGRectMake(0,&0,&100,&100);
&&&&self.colorLayer.position&=&CGPointMake(self.view.bounds.size.width/2.0,&self.view.bounds.size.height/2.0);
&&&&self.colorLayer.backgroundColor&=&[UIColor&redColor].CGC
&&&&[self.view.layer&addSublayer:self.colorLayer];
-&(void)touchesBegan:(NSSet&*)touches&withEvent:(UIEvent&*)event
&&&&[CATransaction&begin];
&&&&[CATransaction&setAnimationDuration:1.0];
&&&&[CATransaction&setAnimationTimingFunction:[CAMediaTimingFunction&functionWithName:kCAMediaTimingFunctionEaseOut]];
&&&&self.colorLayer.position&=&[[touches&anyObject]&locationInView:self.view];
&&&&[CATransaction&commit];
UIView的动画缓冲
UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:
UIViewAnimationOptionCurveEaseInOut&
UIViewAnimationOptionCurveEaseIn&
UIViewAnimationOptionCurveEaseOut&
UIViewAnimationOptionCurveLinear
它们和CAMediaTimingFunction紧密关联,UIViewAnimationOptionCurveEaseInOut是默认值(这里没有kCAMediaTimingFunctionDefault相对应的值了)。
具体使用方法见清单10.2(注意到这里不再使用UIView额外添加的图层,因为UIKit的动画并不支持这类图层)。
清单10.2 使用UIKit动画的缓冲测试工程
@interface&ViewController&()
@property&(nonatomic,&strong)&UIView&*colorV
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&self.colorView&=&[[UIView&alloc]&init];
&&&&self.colorView.bounds&=&CGRectMake(0,&0,&100,&100);
&&&&self.colorView.center&=&CGPointMake(self.view.bounds.size.width&/&2,&self.view.bounds.size.height&/&2);
&&&&self.colorView.backgroundColor&=&[UIColor&redColor];
&&&&[self.view&addSubview:self.colorView];
-&(void)touchesBegan:(NSSet&*)touches&withEvent:(UIEvent&*)event
&&&&[UIView&animateWithDuration:1.0&delay:0.0
&&&&&&&&&&&&&&&&&&&&&&&&options:UIViewAnimationOptionCurveEaseOut
&&&&&&&&&&&&&&&&&&&&&animations:^{
&&&&&&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&self.colorView.center&=&[[touches&anyObject]&locationInView:self.view];
&&&&&&&&&&&&&&&&&&&&&&&&}
&&&&&&&&&&&&&&&&&&&&&completion:NULL];
缓冲和关键帧动画
或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因(见清单8.5)看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合适的缓冲方法,例如kCAMediaTimingFunctionEaseIn,给图层的颜色变化添加一点脉冲效果,让它更像现实中的一个彩色灯泡。
我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓冲,于是每次颜色的变换都会有脉冲效果。
CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。
在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了(见清单10.3)。
运行更新后的代码,你会发现动画看起来更加自然了。
清单10.3 对CAKeyframeAnimation使用CAMediaTimingFunction
@interface&ViewController&()
@property&(nonatomic,&weak)&IBOutlet&UIView&*layerV
@property&(nonatomic,&weak)&IBOutlet&CALayer&*colorL
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&self.colorLayer&=&[CALayer&layer];
&&&&self.colorLayer.frame&=&CGRectMake(50.0f,&50.0f,&100.0f,&100.0f);
&&&&self.colorLayer.backgroundColor&=&[UIColor&blueColor].CGC
&&&&[self.layerView.layer&addSublayer:self.colorLayer];
-&(IBAction)changeColor
&&&&CAKeyframeAnimation&*animation&=&[CAKeyframeAnimation&animation];
&&&&animation.keyPath&=&@"backgroundColor";
&&&&animation.duration&=&2.0;
&&&&animation.values&=&@[
&&&&&&&&&&&&&&&&&&&&&&&&&(__bridge&id)[UIColor&blueColor].CGColor,
&&&&&&&&&&&&&&&&&&&&&&&&&(__bridge&id)[UIColor&redColor].CGColor,
&&&&&&&&&&&&&&&&&&&&&&&&&(__bridge&id)[UIColor&greenColor].CGColor,
&&&&&&&&&&&&&&&&&&&&&&&&&(__bridge&id)[UIColor&blueColor].CGColor&];
&&&&CAMediaTimingFunction&*fn&=&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseIn];
&&&&animation.timingFunctions&=&@[fn,&fn,&fn];
&&&&[self.colorLayer&addAnimation:animation&forKey:nil];
自定义缓冲函数
在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?
除了+functionWithName:之外,CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。
使用这个方法,我们可以创建一个自定义的缓冲函数,来匹配我们的时钟动画,为了理解如何使用这个方法,我们要了解一些CAMediaTimingFunction是如何工作的。
三次贝塞尔曲线
CAMediaTimingFunction函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。
图10.1 线性缓冲函数的图像
这条曲线的斜率代表了速度,斜率的改变代表了加速度,原则上来说,任何加速的曲线都可以用这种图像来表示,但是CAMediaTimingFunction使用了一个叫做三次贝塞尔曲线的函数,它只可以产出指定缓冲函数的子集(我们之前在第八章中创建CAKeyframeAnimation路径的时候提到过三次贝塞尔曲线)。
你或许会回想起,一个三次贝塞尔曲线通过四个点来定义,第一个和最后一个点代表了曲线的起点和终点,剩下中间两个点叫做控制点,因为它们控制了曲线的形状,贝塞尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过它们。你可以把它们想象成吸引经过它们曲线的磁铁。
图10.2展示了一个三次贝塞尔缓冲函数的例子
图10.2 三次贝塞尔缓冲函数
实际上它是一个很奇怪的函数,先加速,然后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢?
CAMediaTimingFunction有一个叫做-getControlPointAtIndex:values:的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为什么不简单返回一个CGPoint),但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPath和CAShapeLayer来把它画出来。
曲线的起始和终点始终是{0, 0}和{1, 1},于是我们只需要检索曲线的第二个和第三个点(控制点)。具体代码见清单10.4。所有的标准缓冲函数的图像见图10.3。
清单10.4 使用UIBezierPath绘制CAMediaTimingFunction
@interface&ViewController&()
@property&(nonatomic,&weak)&IBOutlet&UIView&*layerV
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&CAMediaTimingFunction&*function&=&CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseOut];
&&&&CGPoint&controlPoint1,&controlPoint2;
&&&&[function&getControlPointAtIndex:1&values:(float&*)&controlPoint1];
&&&&[function&getControlPointAtIndex:2&values:(float&*)&controlPoint2];
&&&&UIBezierPath&*path&=&[[UIBezierPath&alloc]&init];
&&&&[path&moveToPoint:CGPointZero];
&&&&[path&addCurveToPoint:CGPointMake(1,&1)
&&&&&&&&&&&&controlPoint1:controlPoint1&controlPoint2:controlPoint2];
&&&&[path&applyTransform:CGAffineTransformMakeScale(200,&200)];
&&&&CAShapeLayer&*shapeLayer&=&[CAShapeLayer&layer];
&&&&shapeLayer.strokeColor&=&[UIColor&redColor].CGC
&&&&shapeLayer.fillColor&=&[UIColor&clearColor].CGC
&&&&shapeLayer.lineWidth&=&4.0f;
&&&&shapeLayer.path&=&path.CGP
&&&&[self.layerView.layer&addSublayer:shapeLayer];
&&&&self.layerView.layer.geometryFlipped&=&YES;
图10.3 标准CAMediaTimingFunction缓冲曲线
那么对于我们自定义时钟指针的缓冲函数来说,我们需要初始微弱,然后迅速上升,最后缓冲到终点的曲线,通过一些实验之后,最终结果如下:
[CAMediaTimingFunction&functionWithControlPoints:1&:0&:0.75&:1];
如果把它转换成缓冲函数的图像,最后如图10.4所示,如果把它添加到时钟的程序,就形成了之前一直期待的非常赞的效果(见代清单10.5)。
图10.4 自定义适合时钟的缓冲函数
清单10.5 添加了自定义缓冲函数的时钟程序
-&(void)setAngle:(CGFloat)angle&forHand:(UIView&*)handView&?animated:(BOOL)animated
&&&&CATransform3D&transform&=&CATransform3DMakeRotation(angle,&0,&0,&1);
&&&&if&(animated)&{
&&&&&&&&CABasicAnimation&*animation&=&[CABasicAnimation&animation];
&&&&&&&&animation.keyPath&=&@"transform";
&&&&&&&&animation.fromValue&=&[handView.layer.presentationLayer&valueForKey:@"transform"];
&&&&&&&&animation.toValue&=&[NSValue&valueWithCATransform3D:transform];
&&&&&&&&animation.duration&=&0.5;
&&&&&&&&animation.delegate&=&
&&&&&&&&animation.timingFunction&=&[CAMediaTimingFunction&functionWithControlPoints:1&:0&:0.75&:1];
&&&&&&&&handView.layer.transform&=&
&&&&&&&&[handView.layer&addAnimation:animation&forKey:nil];
&&&&}&else&{
&&&&&&&&handView.layer.transform&=&
更加复杂的动画曲线
考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,然后经过几次反弹,最后停下来。如果用一张图来说明,它会如图10.5所示。
图10.5 一个没法用三次贝塞尔曲线描述的反弹的动画
这种效果没法用一个简单的三次贝塞尔曲线表示,于是不能用CAMediaTimingFunction来完成。但如果想要实现这样的效果,可以用如下几种方法:
用CAKeyframeAnimation创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数(具体下节介绍)。
使用定时器逐帧更新实现动画(见第11章,&基于定时器的动画&)。
基于关键帧的缓冲
为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。
清单10.6展示了实现反弹球动画的代码(见图10.6)
清单10.6 使用关键帧实现反弹球的动画
@interface&ViewController&()
@property&(nonatomic,&weak)&IBOutlet&UIView&*containerV
@property&(nonatomic,&strong)&UIImageView&*ballV
@implementation&ViewController
-&(void)viewDidLoad
&&&&[super&viewDidLoad];
&&&&UIImage&*ballImage&=&[UIImage&imageNamed:@"Ball.png"];
&&&&self.ballView&=&[[UIImageView&alloc]&initWithImage:ballImage];
&&&&[self.containerView&addSubview:self.ballView];
&&&&[self&animate];
-&(void)touchesBegan:(NSSet&*)touches&withEvent:(UIEvent&*)event
&&&&[self&animate];
-&(void)animate
&&&&self.ballView.center&=&CGPointMake(150,&32);
&&&&CAKeyframeAnimation&*animation&=&[CAKeyframeAnimation&animation];
&&&&animation.keyPath&=&@"position";
&&&&animation.duration&=&1.0;
&&&&animation.delegate&=&
&&&&animation.values&=&@[
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&32)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&268)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&140)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&268)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&220)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&268)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&250)],
&&&&&&&&&&&&&&&&&&&&&&&&&[NSValue&valueWithCGPoint:CGPointMake(150,&268)]
&&&&&&&&&&&&&&&&&&&&&&&&&];
&&&&animation.timingFunctions&=&@[
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseIn],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseOut],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseIn],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseOut],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseIn],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseOut],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&[CAMediaTimingFunction&functionWithName:&kCAMediaTimingFunctionEaseIn]
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&];
&&&&animation.keyTimes&=&@[@0.0,&@0.3,&@0.5,&@0.7,&@0.8,&@0.9,&@0.95,&@1.0];
&&&&self.ballView.layer.position&=&CGPointMake(150,&268);
&&&&[self.ballView.layer&addAnimation:animation&forKey:nil];
图10.6 使用关键帧实现的反弹球动画
这种方式还算不错,但是实现起来略显笨重(因为要不停地尝试计算各种关键帧和时间偏移)并且和动画强绑定了(因为如果要改变动画的一个属性,那就意味着要重新计算所有的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面我们来实现它。
流程自动化
在清单10.6中,我们把动画分割成相当大的几块,然后用Core Animation的缓冲进入和缓冲退出函数来大约形成我们想要的曲线。但如果我们把动画分割成更小的几部分,那么我们就可以用直线来拼接这些曲线(也就是线性缓冲)。为了实现自动化,我们需要知道如何做如下两件事情:
自动把任意属性动画分割成多个关键帧
用一个数学函数表示弹性动画,使得可以对帧做便宜
为了解决第一个问题,我们需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1):
value = (endValue & startValue) & time + startV
那么如果要插入一个类似于CGPoint,CGColorRef或者CATransform3D这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就CGPoint中的x和y值,CGColorRef中的红,蓝,绿,透明值,或者是CATransform3D中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。
一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。清单10.7展示了相关代码。
注意到我们用了60 x 动画时间(秒做单位)作为关键帧的个数,这时因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果我们每秒生成60个关键帧,就可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效果)。
我们在示例中仅仅引入了对CGPoint类型的插值代码。但是,从代码中很清楚能看出如何扩展成支持别的类型。作为不能识别类型的备选方案,我们仅仅在前一半返回了fromValue,在后一半返回了toValue。
清单10.7 使用插入的值创建一个关键帧动画
float&interpolate(float&from,&float&to,&float&time)
&&&&return&(to&-&from)&*&time&+&
-&(id)interpolateFromValue:(id)fromValue&toValue:(id)toValue&time:(float)time
&&&&if&([fromValue&isKindOfClass:[NSValue&class]])&{
&&&&&&&&const&char&*type&=&[fromValue&objCType];
&&&&&&&&if&(strcmp(type,&@encode(CGPoint))&==&0)&{
&&&&&&&&&&&&CGPoint&from&=&[fromValue&CGPointValue];
&&&&&&&&&&&&CGPoint&to&=&[toValue&CGPointValue];
&&&&&&&&&&&&CGPoint&result&=&CGPointMake(interpolate(from.x,&to.x,&time),&interpolate(from.y,&to.y,&time));
&&&&&&&&&&&&return&[NSValue&valueWithCGPoint:result];
&&&&return&(time&&&0.5)?&fromValue:&toV
-&(void)animate
&&&&self.ballView.center&=&CGPointMake(150,&32);
&&&&NSValue&*fromValue&=&[NSValue&valueWithCGPoint:CGPointMake(150,&32)];
&&&&NSValue&*toValue&=&[NSValue&valueWithCGPoint:CGPointMake(150,&268)];
&&&&CFTimeInterval&duration&=&1.0;
&&&&NSInteger&numFrames&=&duration&*&60;
&&&&NSMutableArray&*frames&=&[NSMutableArray&array];
&&&&for&(int&i&=&0;&i&&&numF&i++)&{
&&&&&&&&float&time&=&1&/&(float)numFrames&*&i;
&&&&&&&&[frames&addObject:[self&interpolateFromValue:fromValue&toValue:toValue&time:time]];
&&&&CAKeyframeAnimation&*animation&=&[CAKeyframeAnimation&animation];
&&&&animation.keyPath&=&@"position";
&&&&animation.duration&=&1.0;
&&&&animation.delegate&=&
&&&&animation.values&=&
&&&&[self.ballView.layer&addAnimation:animation&forKey:nil];
这可以起到作用,但效果并不是很好,到目前为止我们所完成的只是一个非常复杂的方式来使用线性缓冲复制CABasicAnimation的行为。这种方式的好处在于我们可以更加精确地控制缓冲,这也意味着我们可以应用一个完全定制的缓冲函数。那么该如何做呢?
缓冲背后的数学并不很简单,但是幸运的是我们不需要一一实现它。罗伯特&彭纳有一个网页关于缓冲函数(),包含了大多数普遍的缓冲函数的多种编程语言的实现的链接,包括C。这里是一个缓冲进入缓冲退出函数的示例(实际上有很多不同的方式去实现它)。
float&quadraticEaseInOut(float&t)&
&&&&return&(t&&&0.5)?&(2&*&t&*&t):&(-2&*&t&*&t)&+&(4&*&t)&-&1;&
对我们的弹性球来说,我们可以使用bounceEaseOut函数:
float&bounceEaseOut(float&t)
&&&&if&(t&&&4/11.0)&{
&&&&&&&&return&(121&*&t&*&t)/16.0;
&&&&}&else&if&(t&&&8/11.0)&{
&&&&&&&&return&(363/40.0&*&t&*&t)&-&(99/10.0&*&t)&+&17/5.0;
&&&&}&else&if&(t&&&9/10.0)&{
&&&&&&&&return&(&*&t&*&t)&-&(.0&*&t)&+&.0;
&&&&return&(54/5.0&*&t&*&t)&-&(513/25.0&*&t)&+&268/25.0;
如果修改清单10.7的代码来引入bounceEaseOut方法,我们的任务就是仅仅交换缓冲函数,现在就可以选择任意的缓冲类型创建动画了(见清单10.8)。
清单10.8 用关键帧实现自定义的缓冲函数
-&(void)animate
&&&&self.ballView.center&=&CGPointMake(150,&32);
&&&&NSValue&*fromValue&=&[NSValue&valueWithCGPoint:CGPointMake(150,&32)];
&&&&NSValue&*toValue&=&[NSValue&valueWithCGPoint:CGPointMake(150,&268)];
&&&&CFTimeInterval&duration&=&1.0;
&&&&NSInteger&numFrames&=&duration&*&60;
&&&&NSMutableArray&*frames&=&[NSMutableArray&array];
&&&&for&(int&i&=&0;&i&&&numF&i++)&{
&&&&&&&&float&time&=&1/(float)numFrames&*&i;
&&&&&&&&time&=&bounceEaseOut(time);
&&&&&&&&[frames&addObject:[self&interpolateFromValue:fromValue&toValue:toValue&time:time]];
&&&&CAKeyframeAnimation&*animation&=&[CAKeyframeAnimation&animation];
&&&&animation.keyPath&=&@"position";
&&&&animation.duration&=&1.0;
&&&&animation.delegate&=&
&&&&animation.values&=&
&&&&[self.ballView.layer&addAnimation:animation&forKey:nil];
在这一章中,我们了解了有关缓冲和CAMediaTimingFunction类,它可以允许我们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用CAKeyframeAnimation来避开CAMediaTimingFunction的限制,创建完全自定义的缓冲函数。
在下一章中,我们将要研究基于定时器的动画--另一个给我们对动画更多控制的选择,并且实现对动画的实时操纵标签:
&&国之画&&&& &&
版权所有 京ICP备号-2
迷上了代码!

我要回帖

更多关于 ios 重力弹性动画 的文章

 

随机推荐