10.2 自定義緩沖函數(shù)

2018-02-24 15:07 更新

自定義緩沖函數(shù)

在第八章中,我們給時(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),然后用UIBezierPathCAShapeLayer來把它畫出來。

曲線的起始和終點(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;
    }
}

更加復(fù)雜的動(dòng)畫曲線

考慮一個(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];
}
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)