在第八章中,我們給時(shí)鐘項(xiàng)目添加了動(dòng)畫。看起來很贊,但是如果有合適的緩沖函數(shù)就更好了。在顯示世界中,鐘表指針轉(zhuǎn)動(dòng)的時(shí)候,通常起步很慢,然后迅速啪地一聲,最后緩沖到終點(diǎn)。但是標(biāo)準(zhǔn)的緩沖函數(shù)在這里每一個(gè)適合它,那該如何創(chuàng)建一個(gè)新的呢?
除了+functionWithName:
之外,CAMediaTimingFunction
同樣有另一個(gè)構(gòu)造函數(shù),一個(gè)有四個(gè)浮點(diǎn)參數(shù)的+functionWithControlPoints::::
(注意這里奇怪的語法,并沒有包含具體每個(gè)參數(shù)的名稱,這在objective-C中是合法的,但是卻違反了蘋果對(duì)方法命名的指導(dǎo)方針,而且看起來是一個(gè)奇怪的設(shè)計(jì))。
使用這個(gè)方法,我們可以創(chuàng)建一個(gè)自定義的緩沖函數(shù),來匹配我們的時(shí)鐘動(dòng)畫,為了理解如何使用這個(gè)方法,我們要了解一些CAMediaTimingFunction
是如何工作的。
CAMediaTimingFunction
函數(shù)的主要原則在于它把輸入的時(shí)間轉(zhuǎn)換成起點(diǎn)和終點(diǎn)之間成比例的改變。我們可以用一個(gè)簡(jiǎn)單的圖標(biāo)來解釋,橫軸代表時(shí)間,縱軸代表改變的量,于是線性的緩沖就是一條從起點(diǎn)開始的簡(jiǎn)單的斜線(圖10.1)。
圖10.2 三次貝塞爾緩沖函數(shù)
實(shí)際上它是一個(gè)很奇怪的函數(shù),先加速,然后減速,最后快到達(dá)終點(diǎn)的時(shí)候又加速,那么標(biāo)準(zhǔn)的緩沖函數(shù)又該如何用圖像來表示呢?
CAMediaTimingFunction
有一個(gè)叫做-getControlPointAtIndex:values:
的方法,可以用來檢索曲線的點(diǎn),這個(gè)方法的設(shè)計(jì)的確有點(diǎn)奇怪(或許也就只有蘋果能回答為什么不簡(jiǎn)單返回一個(gè)CGPoint
),但是使用它我們可以找到標(biāo)準(zhǔn)緩沖函數(shù)的點(diǎn),然后用UIBezierPath
和CAShapeLayer
來把它畫出來。
曲線的起始和終點(diǎn)始終是{0, 0}和{1, 1},于是我們只需要檢索曲線的第二個(gè)和第三個(gè)點(diǎn)(控制點(diǎn))。具體代碼見清單10.4。所有的標(biāo)準(zhǔn)緩沖函數(shù)的圖像見圖10.3。
清單10.4 使用UIBezierPath
繪制CAMediaTimingFunction
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create timing function
CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
//get control points
CGPoint controlPoint1, controlPoint2;
[function getControlPointAtIndex:1 values:(float *)&controlPoint1];
[function getControlPointAtIndex:2 values:(float *)&controlPoint2];
//create curve
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointZero];
[path addCurveToPoint:CGPointMake(1, 1)
controlPoint1:controlPoint1 controlPoint2:controlPoint2];
//scale the path up to a reasonable size for display
[path applyTransform:CGAffineTransformMakeScale(200, 200)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 4.0f;
shapeLayer.path = path.CGPath;
[self.layerView.layer addSublayer:shapeLayer];
//flip geometry so that 0,0 is in the bottom-left
self.layerView.layer.geometryFlipped = YES;
}
@end
圖10.4 自定義適合時(shí)鐘的緩沖函數(shù)
清單10.5 添加了自定義緩沖函數(shù)的時(shí)鐘程序
- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView ?animated:(BOOL)animated
{
//generate transform
CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
if (animated) {
//create transform animation
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 = self;
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
//apply animation
handView.layer.transform = transform;
[handView.layer addAnimation:animation forKey:nil];
} else {
//set transform directly
handView.layer.transform = transform;
}
}
考慮一個(gè)橡膠球掉落到堅(jiān)硬的地面的場(chǎng)景,當(dāng)開始下落的時(shí)候,它會(huì)持續(xù)加速知道落到地面,然后經(jīng)過幾次反彈,最后停下來。如果用一張圖來說明,它會(huì)如圖10.5所示。
這可以起到作用,但效果并不是很好,到目前為止我們所完成的只是一個(gè)非常復(fù)雜的方式來使用線性緩沖復(fù)制CABasicAnimation
的行為。這種方式的好處在于我們可以更加精確地控制緩沖,這也意味著我們可以應(yīng)用一個(gè)完全定制的緩沖函數(shù)。那么該如何做呢?
緩沖背后的數(shù)學(xué)并不很簡(jiǎn)單,但是幸運(yùn)的是我們不需要一一實(shí)現(xiàn)它。羅伯特·彭納有一個(gè)網(wǎng)頁關(guān)于緩沖函數(shù)(http://www.robertpenner.com/easing),包含了大多數(shù)普遍的緩沖函數(shù)的多種編程語言的實(shí)現(xiàn)的鏈接,包括C。這里是一個(gè)緩沖進(jìn)入緩沖退出函數(shù)的示例(實(shí)際上有很多不同的方式去實(shí)現(xiàn)它)。
float quadraticEaseInOut(float t)
{
return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1;
}
對(duì)我們的彈性球來說,我們可以使用bounceEaseOut
函數(shù):
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 (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
}
return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}
如果修改清單10.7的代碼來引入bounceEaseOut
方法,我們的任務(wù)就是僅僅交換緩沖函數(shù),現(xiàn)在就可以選擇任意的緩沖類型創(chuàng)建動(dòng)畫了(見清單10.8)。
清單10.8 用關(guān)鍵幀實(shí)現(xiàn)自定義的緩沖函數(shù)
- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);
//set up animation parameters
NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
CFTimeInterval duration = 1.0;
//generate keyframes
NSInteger numFrames = duration * 60;
NSMutableArray *frames = [NSMutableArray array];
for (int i = 0; i < numFrames; i++) {
float time = 1/(float)numFrames * i;
//apply easing
time = bounceEaseOut(time);
//add keyframe
[frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
}
//create keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 1.0;
animation.delegate = self;
animation.values = frames;
//apply animation
[self.ballView.layer addAnimation:animation forKey:nil];
}
更多建議: