玩转Facebook开源的Pop库

原文链接:“http://codeplease.io/playing-with-pop-i/”

Facebook在向外界开源一些三方库上,一直都是做的非常好的. 最近在github发布的“Pop”库在不到24小时之内就已经收到了3.5k个stars.

引用Facebook的原话:

‘pop是一个适用于iOS和OSX上的可扩展的动画引擎.除了基础的静态动画之外,还支持动态的弹性和衰减动画,可用于构建逼真的,基于物理的交互.它的API可以快速集成到现有的Objective-C代码库中,并且能够让任意对象的任何属性具有动画效果.它是一个成熟的并且经过良好测试的框架 在paper中驱动所有的动画和过渡效果.’

我用它创建一个“简单的示例”开始.我的目的是想看一下它在给用户输入框加上阴影时,能达到什么样子的效果;它做的太棒了.我也想要获取第一手的体验,用一些东西快速去构建.这个特定的案例中使用的POPSpringAnimation代码,感觉与“我做过的其他东西”很类似.

为了研究这个库,我的方法是直接进入他们的.h文件一探究竟(顺便提下,它们都是由objective-c++写的):

POPBasicAnimation
POPDecayAnimation
POPPropertyAnimation
POPSpringAnimation
POPCustomAnimation
POPAnimation
POPAnimatableProperty

关于Pop有一些非常酷的东西,当你添加一些动画的时候,他的表现层和模型层是同步的. 这个 Sam Page(@sampage)写的 “圆圈示例” 就使用了pop. 有点类似于 “这个”. 由于strokeEndstrokeStart 并不是“默认属性”的一部分,你需要创建你自己的自定义属性,比如这样:

[POPAnimatableProperty propertyWithName:@"strokeStart" initializer:^(POPMutableAnimatableProperty *prop) {
        prop.readBlock = ^(id obj, CGFloat values[]) {
            values[0] = [obj strokeStart];
        };
        prop.writeBlock = ^(id obj, const CGFloat values[]) {
            [obj setStrokeStart:values[0]];
        };
    }];

必须的说POP库是非常强大的,正如之前所说:表示层和模型层是同步的!

” Facebook’s Paper Tech Talk “里面Brian Amerige(@brianamerige)的话一直困扰在我的脑海里,特别是这一部分,他展示了如何让我们把手势融合进动画中(视频47:40部分).

所以直接从视频中看到:

基于手势的速率,可以旋转一个UIView

- (void)rotate:(UIPanGestureRecognizer*)recognizer
{
    CGPoint velocity = [recognizer velocityInView:self.view];

    POPSpringAnimation *spring = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
    spring.velocity = [NSValue valueWithCGPoint:velocity];

    [_outletView.layer pop_addAnimation:spring forKey:@"rotationAnimation"];
}

现在,当你开始接触这些坏东西的时候,这一切又开始变得有趣了.

那么,如果我们同时对positionsize 加上一些”dynamics”,会发生什么?

见代码:

- (void)rotate:(UIPanGestureRecognizer*)recognizer
{
    CGPoint velocity = [recognizer velocityInView:self.view];

POPSpringAnimation *positionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPosition];  
positionAnimation.velocity = [NSValue valueWithCGPoint:velocity];  
positionAnimation.dynamicsTension = 5;  
positionAnimation.dynamicsFriction = 5.0f;  
positionAnimation.springBounciness = 20.0f;  
[_outletView.layer pop_addAnimation:positionAnimation forKey:@"positionAnimation"];


POPSpringAnimation *sizeAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerSize];  
sizeAnimation.velocity = [NSValue valueWithCGPoint:velocity];  
sizeAnimation.springBounciness = 1.0f;  
sizeAnimation.dynamicsFriction = 1.0f;  
[_outletView.layer pop_addAnimation:sizeAnimation forKey:@"sizeAnimation"];

在删除所有与dynamics相关的代码后:

你依然可以看到一个轻微的弹力效果,但是这些是POPSpringAnimation的默认值起的效果.

至于POP,或者其他用于娱乐用途的三方库,扪心自问都涉及到一点:我现在应该使用它(这个library)来做吗?? Brian Lovin (@brian_lovin) 所写的“Design Details: Paper by Facebook(设计细节:Facebook的paper)”给了你大量的资料去思考这个问题. 下面来自于Brian Lovin的博客:

我想创建一些内容,允许我展示一个小的弹出窗口,并且有一些震动感.(好吧,是因为我喜欢这样的效果). 上图并不是我想要的效果(甚至差得很远).但是我从这里获得了灵感,所以:

是的,我知道.没有什么奇特的,但是,可以使用POP很轻松实现这个效果.同样,就动画而言,没有能比POP带来”更甜美味道”的感觉了.

- (void)hidePopup
{
    _isMenuOpen = NO;
    POPBasicAnimation *opacityAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerOpacity];
    opacityAnimation.fromValue = @(1);
    opacityAnimation.toValue = @(0);
    [_popUp.layer pop_addAnimation:opacityAnimation forKey:@"opacityAnimation"];

    POPBasicAnimation *positionAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerPosition];
    positionAnimation.fromValue = [NSValue valueWithCGPoint:VisiblePosition];
    positionAnimation.toValue = [NSValue valueWithCGPoint:HiddenPosition];
    [_popUp.layer pop_addAnimation:positionAnimation forKey:@"positionAnimation"];

    POPSpringAnimation *scaleAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerScaleXY];

    scaleAnimation.fromValue  = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)];
    scaleAnimation.toValue  = [NSValue valueWithCGSize:CGSizeMake(0.5f, 0.5f)];
    [_popUp.layer pop_addAnimation:scaleAnimation forKey:@"scaleAnimation"];
}

还有显示出效果的代码:

- (void)showPopup
{
    _isMenuOpen = YES;

    POPBasicAnimation *opacityAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerOpacity];
    opacityAnimation.fromValue = @(0);
    opacityAnimation.toValue = @(1);
    opacityAnimation.beginTime = CACurrentMediaTime() + 0.1;
    [_popUp.layer pop_addAnimation:opacityAnimation forKey:@"opacityAnimation"];

    POPBasicAnimation *positionAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerPosition];
    positionAnimation.fromValue = [NSValue valueWithCGPoint:VisibleReadyPosition];
    positionAnimation.toValue = [NSValue valueWithCGPoint:VisiblePosition];
    [_popUp.layer pop_addAnimation:positionAnimation forKey:@"positionAnimation"];


    POPSpringAnimation *scaleAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerScaleXY];
    scaleAnimation.fromValue  = [NSValue valueWithCGSize:CGSizeMake(0.5, 0.5f)];
    scaleAnimation.toValue  = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)];//@(0.0f);
    scaleAnimation.springBounciness = 20.0f;
    scaleAnimation.springSpeed = 20.0f;
    [_popUp.layer pop_addAnimation:scaleAnimation forKey:@"scaleAnimation"];
}

这里补充三个注意事项: 1.当你添加一个动画类似[myView pop_addAnimation:animation forKey:@"animationKey"]; 要记住,假如你添加其他的动画使用了相同的key,那么将会替换掉之前的那一个.

2.当你想一个动画用CACurrentMediaTime()并使用beginTime来指定. 因此,看起来就像这样:animation.beginTimee = CACurrentMediaTime() + delayInSeconds;. 我添加了一个简单的延迟,当然,他不会工作.感谢Kimon(@kimon)所提出的“警告”;

3.当你看到一个属性类似于kPOPLayerScaleXY,它会期望两个值. 在这种情况下是一个CGSize.现在可能是有意义的,但是我传了一个NSNumber(一个单一值)并且期待它可以同时设置X和Y的值.

这是一位天才(via @_tiagoalmeida):

它起效果了:

POPAnimatableProperty *constantProperty = [POPAnimatableProperty propertyWithName:@"constant" initializer:^(POPMutableAnimatableProperty *prop){  
        prop.readBlock = ^(NSLayoutConstraint *layoutConstraint, CGFloat values[]) {
            values[0] = [layoutConstraint constant];
        };
        prop.writeBlock = ^(NSLayoutConstraint *layoutConstraint, const CGFloat values[]) {
            [layoutConstraint setConstant:values[0]];
        };
    }];

POPSpringAnimation *constantAnimation = [POPSpringAnimation animation];  
constantAnimation.property = constantProperty;  
constantAnimation.fromValue = @(_layoutConstraint.constant);  
constantAnimation.toValue = @(200);  
[_layoutConstraint pop_addAnimation:constantAnimation forKey:@"constantAnimation"];

非常感谢 Jake Marsh (@jakemarsh).

仅仅只是一条小笔记,我没有注意到kPOPLayoutConstraintConstant.所以你不需要自己去创建一个自定义的POPAnimatableProperty.

在玩了几天的POP后,现在已经进入到了除了享受以外,我还可以做出共享的时刻了.

在我第一篇文章中(本文开头部分) ,我创建了一个自定义的属性给strokeStartstrokeEnd (两个都属于CAShapeLayer):

[POPAnimatableProperty propertyWithName:@"strokeStart" initializer:^(POPMutableAnimatableProperty *prop) {
        prop.readBlock = ^(id obj, CGFloat values[]) {
            values[0] = [obj strokeStart];
        };
        prop.writeBlock = ^(id obj, const CGFloat values[]) {
            [obj setStrokeStart:values[0]];
        };
}];

这就几行代码的工作,但是不再害怕了.“我第一次提交的代码请求” (希望不是最后一次) 已经被批准通过 并且已经合并到POP的主仓库.这意味着,我现在可以在CAShapeLayer上使用这两个属性而不用添加任何附加逻辑.

添加一个属性到POP只有三个简单的步骤(也许是4步或者5步). 假如有人想试一试:

1.在POPAnimatableProperty.h内给你的属性名称添加一个extern NSString修饰,努力保持一个好的命名习惯.就像kPOP<class name witout prefix><propertyName>这样.假如他是由超过一个值组成的话,可以这样kPOP<class name witout prefix><propertyName>XY. 在这之后去POPAnimatableProperty.m添加真实的值,就像这样:NSString * const kPop<class name witout prefix><propertyName> = @"<propertyName>".显然这并不是成文的规定,所以你不确定如何命名的话,去看看其他的属性是如何命名的.

2.通过这种方法添加的 读/写的block 将会可以运行,增加threshold值.你可以看到“其他的属性是如何做到的”

  1. 我不需要这样做,我添加的属性很简单.我想说,当你在读写一个新值时,使用辅助方法对你来说会更好.例如,你可以看一下kPOPViewBackgroundColor是如何做的:
{kPOPViewBackgroundColor,
    ^(UIView *obj, CGFloat values[]) {
      POPUIColorGetRGBAComponents(obj.backgroundColor, values);
    },
    ^(UIView *obj, const CGFloat values[]) {
      obj.backgroundColor = POPUIColorRGBACreate(values);
    },
    1.0
  },

这种情况下要确保POPUIColorGetRGBAComponentsPOPUIColorRGBACreate:

void POPUIColorGetRGBAComponents(UIColor *color, CGFloat components[])  
{
  return POPCGColorGetRGBAComponents(color.CGColor, components);
}

UIColor *POPUIColorRGBACreate(const CGFloat components[])  
{
  CGColorRef colorRef = POPCGColorRGBACreate(components);
  UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
  CGColorRelease(colorRef);
  return color;
}

这些辅助方法位于POPCGUtils,尽管在POPLayerExtras有更多.作为一个好市民,你可以创建其他的方法,这样人们就可以使用它们来模拟属性的行为.

1.添加的你属性到测试组!由于这些属性非常的天真,我仅仅添加它到POPAnimatablePropertyTests.mtestProvidedExistence,来保证它的实现方法实际存在.

2.尽可能更多测试,假如你做一些与众不同的,并且没有被默认的组覆盖.

正如我需要成长,我将会贡献更多的内容到这个仓库.

这儿有些关于旋转的实验:

下面是实现的代码:

POPSpringAnimation *anim = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotationX];  
anim.springBounciness = 20.0f;  
anim.dynamicsMass = 5.0f;  
anim.fromValue = @(degreesToRadians(degrees));  
anim.toValue = @(M_PI);  
[self.bundledLayers pop_addAnimation:anim forKey:@"rotationXAnimation"];

由于 Florien Kluger (@floriankugler) 对我之前的那个“示例”给出的建议,修改了一点儿地方.除了现有的之外,我并且可以在动画的时候抓住模块的顶部.这对于用户来说是一个很愉快的体验.

最开始的时候我禁用并且移除了手势,在下面的条件时:

1.UIGestureRecognizerStateEnded,在交互结束时,很快到动画结束的位置并禁用手势.

2.假如这个view被点击过,我会仅仅只让动画到结束位置.

我用了第二条,它依然是有意义的.第一条已经被修改了.所以,我现在做的就是:

UIGestureRecognizerStateEnded状态已经完成,我就设置下面的完成块:

^(POPAnimation *anim, BOOL finished) {
        if (finished) [self disableGestures];
    }

所以假如动画已经完成了,我将禁用手势.更有趣的部分在UIGestureRecognizerStateBegan状态:

if ([self.bundledLayers pop_animationForKey:RPPopRotationAnimation])  
{
  POPSpringAnimation *animation = [POPSpringAnimation animationWithPropertyNamed:RPPopRotationAnimation];
  [animation setCompletionBlock:^(POPAnimation *anim, BOOL finished) {

  CGFloat currentDegrees = [(NSNumber *)[self.bundledLayers valueForKeyPath:@"transform.rotation.x"] floatValue];
  [self applyRotationAnimationOnLayer:self.bundledLayers initialDegrees:currentDegrees toFinalDegrees:degrees completionBlock:nil];
            }];
}
else  
{
  [self applyRotationAnimationOnLayer:self.bundledLayers initialDegrees:0.0f toFinalDegrees:degrees completionBlock:nil];
}
  1. 检查是否有动画正在进行(可能由于UIGestureRecognizerStateEnded) 1.1假如是这样,就让我们”劫持”这个完成事件completionBlock 并且替换成一个新的转换效果,基于我触摸的位置.
  2. 假如没有动画的话,我会让消息视图仍然呈现折叠并且在应用一个转换效果从0.0f开始到我触摸的地方.

除了代替劫持掉completionBlock,你还能:

[animation setCompletionBlock:nil];
animation.toValue = @(degreesToRadians(degrees));

带来了不同的效果,但是依然是一个不错的. 首先第一个区别是,我会让动画完成并且应用一个新的转换效果. 第二个区别是,我不会允许动画到达原始的toValue位置.

PS:我的数学非常糟糕,所以基于我触摸点位置计算角度的逻辑在 “这里”.