UIImageView上的iOS CAKeyframeAnimation rotationMode

Pet*_*r M 11 rotation cakeyframeanimation ios

我正在尝试沿着贝塞尔曲线路径移动的UIImageView对象位置的关键帧动画.该图显示了动画之前的初始状态.蓝线是路径 - 最初直线向上移动,浅绿色框是初始边界框或图像,深绿色"鬼"是我正在移动的图像:

初始状态

当我将rotationMode设置为启动动画时nil,图像将按预期在路径中保持相同的方向.

但是当我将rotationMode设置为启动动画时kCAAnimationRotateAuto,图像会立即逆时针旋转90度并保持此方向一直通过路径.当它到达路径的末尾时,它会以正确的方向重绘(实际上它显示了我在最终位置重新定位的UIImageView)

在此输入图像描述

我天真地期望rotationMode会将图像定向到路径的切线而不是正常,特别是当Apple文档用于CAKeyframeAnimation rotationMode状态时

确定沿路径设置动画的对象是否旋转以匹配路径切线.

那么这里的解决方案是什么?我是否必须将图像顺时针旋转90度?或者有什么我想念的东西?

谢谢你的帮助.


编辑3月2日

我使用仿射旋转在路径动画之前添加了一个旋转步骤,如:

theImage.transform = CGAffineTransformRotate(theImage.transform,90.0*M_PI/180);
Run Code Online (Sandbox Code Playgroud)

然后在路径动画之后,重置旋转:

theImage.transform = CGAffineTransformIdentity;
Run Code Online (Sandbox Code Playgroud)

这使得图像以预期的方式遵循路径.但是我现在遇到了图像闪烁的另一个问题.我已经在寻找这个SO问题中闪烁问题的解决方案:

iOS CAKeyFrameAnimation缩放在动画结束时闪烁

所以现在我不知道我是做了更好还是更坏!


编辑3月12日

虽然Caleb指出是的,但我必须预先旋转我的图像,Rob提供了一个很棒的代码包,几乎完全解决了我的问题.Rob没有做的唯一事情是补偿我的资产是用垂直方向而不是水平方向绘制的,因此在做动画之前还需要将它们预先旋转90度.但是,嘿,唯一公平的是我必须做一些工作才能让事情顺利进行.

所以我对Rob的解决方案的轻微修改是为了满足我的要求:

  1. 当我添加UIView时,我预先旋转它以通过设置rotationMode:来抵消添加的固有旋转:

    theImage.transform = CGAffineTransformMakeRotation(90*M_PI/180.0);

  2. 我需要在动画结束时保持旋转,所以不是在定义完成块之后用新的比例因子对视图的变换进行爆破,而是根据当前变换构建比例:

    theImage.transform = CGAffineTransformScale(theImage.transform,scaleFactor,scaleFactor);

而这就是我必须做的就是让我的形象像我预期的那样沿着这条路走!


编辑3月22日

我刚刚向GitHub上传了一个演示项目,该项目展示了沿着bezier路径移动对象的过程.代码可以在PathMove找到

我还在我的博客中写了一篇关于在iOS中沿着bezier路径移动对象的文章

Cal*_*leb 8

这里的问题是Core Animation的自动旋转使视图的水平轴保持与路径的切线平行.这就是它的工作原理.

如果您希望视图的垂直轴跟随路径的切线,则在您正在进行的操作时旋转视图的内容是合理的.


rob*_*off 6

以下是消除闪烁所需了解的内容:

  • 正如Caleb指出的那样,Core Animation会旋转您的图层,使其正X轴位于路径的切线上.您需要使图像的"自然"方向与之相配.因此,假设在您的示例图像中是绿色太空飞船,当没有应用旋转时,您需要太空飞船指向右侧.

  • 设置包含旋转的变换会干扰`kCAAnimationRotateAuto'所应用的旋转.在应用动画之前,您需要从变换中删除旋转.

  • 当然,这意味着您需要在动画完成时重新应用转换.当然,您希望在不看到图像外观闪烁的情况下这样做.这并不难,但有一些秘密的问题,我在下面解释.

  • 你可能想要你的宇宙飞船开始指向路径的切线,即使宇宙飞船坐着还没有动画.如果您的太空飞船图像指向右侧,但路径上升,则需要将图像的变换设置为包含90°旋转.但也许你不想对旋转进行硬编码 - 而是想要查看路径并找出它的起始切线.

我将在这里展示一些重要的代码.你可以在github上找到我的测试项目.您可能会在下载并试用它时找到一些用途.只需点击绿色"宇宙飞船"即可看到动画.

所以,在我的测试项目中,我已将我连接UIImageView到一个名为的动作animate:.当您触摸它时,图像沿着图8的一半移动并且尺寸加倍.当您再次触摸它时,图像沿着图8的另一半移动(返回到起始位置),并返回其原始大小.两个动画都使用kCAAnimationRotateAuto,因此图像指向路径的切线.

这是animate:我的开始,在那里我找出图像应该到达的路径,比例和目标点:

- (IBAction)animate:(id)sender {
    UIImageView* theImage = self.imageView;

    UIBezierPath *path = _isReset ? _path0 : _path1;
    CGFloat newScale = 3 - _currentScale;

    CGPoint destination = [path currentPoint];
Run Code Online (Sandbox Code Playgroud)

所以,我需要做的第一件事是从图像的变换中删除任何旋转,因为正如我所提到的,它会干扰kCAAnimationRotateAuto:

    // Strip off the image's rotation, because it interferes with `kCAAnimationRotateAuto`.
    theImage.transform = CGAffineTransformMakeScale(_currentScale, _currentScale);
Run Code Online (Sandbox Code Playgroud)

接下来,我进入UIView动画块,以便系统将动画应用于图像视图:

    [UIView animateWithDuration:3 animations:^{
Run Code Online (Sandbox Code Playgroud)

我为该位置创建关键帧动画并设置其几个属性:

        // Prepare my own keypath animation for the layer position.
        // The layer position is the same as the view center.
        CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        positionAnimation.path = path.CGPath;
        positionAnimation.rotationMode = kCAAnimationRotateAuto;
Run Code Online (Sandbox Code Playgroud)

接下来是防止动画结束时闪烁的秘诀.回想一下,动画不会影响您附加它们的"模型层"的属性(theImage.layer在本例中).相反,它们更新了"表示层"的属性,这反映了屏幕上实际显示的内容.

首先,我设置removedOnCompletionNO了关键帧动画.这意味着动画在动画完成时将保持附加到模型图层,这意味着我可以访问表示层.我从表示层获得变换,删除动画,并将变换应用于模型层.由于这一切都发生在主线程上,因此这些属性更改都发生在一个屏幕刷新周期中,因此没有闪烁.

        positionAnimation.removedOnCompletion = NO;
        [CATransaction setCompletionBlock:^{
            CGAffineTransform finalTransform = [theImage.layer.presentationLayer affineTransform];
            [theImage.layer removeAnimationForKey:positionAnimation.keyPath];
            theImage.transform = finalTransform;
        }];
Run Code Online (Sandbox Code Playgroud)

现在我已经设置了完成块,我实际上可以更改视图属性.当我这样做时,系统会自动将动画附加到图层.

        // UIView will add animations for both of these changes.
        theImage.transform = CGAffineTransformMakeScale(newScale, newScale);
        theImage.center = destination;
Run Code Online (Sandbox Code Playgroud)

我将自动添加的位置动画中的一些关键属性复制到我的关键帧动画中:

        // Copy properties from UIView's animation.
        CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
        positionAnimation.duration = autoAnimation.duration;
        positionAnimation.fillMode = autoAnimation.fillMode;
Run Code Online (Sandbox Code Playgroud)

最后我用关键帧动画替换自动添加的位置动画:

        // Replace UIView's animation with my animation.
        [theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
    }];
Run Code Online (Sandbox Code Playgroud)

Double-finally我更新我的实例变量以反映对图像视图的更改:

    _currentScale = newScale;
    _isReset = !_isReset;
}
Run Code Online (Sandbox Code Playgroud)

这就是为图像视图设置动画而没有闪烁.

而现在,正如史蒂夫乔布斯所说,"最后的一件事".当我加载视图时,我需要设置图像视图的变换,以便它旋转到指向我将用于为其设置动画的第一条路径的切线.我在一个名为的方法中这样做reset:

- (void)reset {
    self.imageView.center = _path1.currentPoint;
    self.imageView.transform = CGAffineTransformMakeRotation(startRadiansForPath(_path0));
    _currentScale = 1;
    _isReset = YES;
}
Run Code Online (Sandbox Code Playgroud)

当然,棘手的一点隐藏在该startRadiansForPath功能中.这真的不是那么难.我使用CGPathApply函数来处理路径的元素,挑出实际形成子路径的前两个点,并计算由这两个点形成的线的角度.(曲线路径部分是二次或三次贝塞尔样条曲线,并且这些样条曲线具有样条曲线第一点处的切线是从第一个点到下一个控制点的直线的特性.)

为了后人,我只是在没有解释的情况下转储代码:

typedef struct {
    CGPoint p0;
    CGPoint p1;
    CGPoint firstPointOfCurrentSubpath;
    CGPoint currentPoint;
    BOOL p0p1AreSet : 1;
} PathState;

static inline void updateStateWithMoveElement(PathState *state, CGPathElement const *element) {
    state->currentPoint = element->points[0];
    state->firstPointOfCurrentSubpath = state->currentPoint;
}

static inline void updateStateWithPoints(PathState *state, CGPoint p1, CGPoint currentPoint) {
    if (!state->p0p1AreSet) {
        state->p0 = state->currentPoint;
        state->p1 = p1;
        state->p0p1AreSet = YES;
    }
    state->currentPoint = currentPoint;
}

static inline void updateStateWithPointsElement(PathState *state, CGPathElement const *element, int newCurrentPointIndex) {
    updateStateWithPoints(state, element->points[0], element->points[newCurrentPointIndex]);
}

static void updateStateWithCloseElement(PathState *state, CGPathElement const *element) {
    updateStateWithPoints(state, state->firstPointOfCurrentSubpath, state->firstPointOfCurrentSubpath);
}

static void updateState(void *info, CGPathElement const *element) {
    PathState *state = info;
    switch (element->type) {
        case kCGPathElementMoveToPoint: return updateStateWithMoveElement(state, element);
        case kCGPathElementAddLineToPoint: return updateStateWithPointsElement(state, element, 0);
        case kCGPathElementAddQuadCurveToPoint: return updateStateWithPointsElement(state, element, 1);
        case kCGPathElementAddCurveToPoint: return updateStateWithPointsElement(state, element, 2);
        case kCGPathElementCloseSubpath: return updateStateWithCloseElement(state, element);
    }
}

CGFloat startRadiansForPath(UIBezierPath *path) {
    PathState state;
    memset(&state, 0, sizeof state);
    CGPathApply(path.CGPath, &state, updateState);
    return atan2f(state.p1.y - state.p0.y, state.p1.x - state.p0.x);
}
Run Code Online (Sandbox Code Playgroud)