【问题标题】:iOS CAKeyframeAnimation rotationMode on UIImageViewUIImageView上的iOS CAKeyframeAnimation旋转模式
【发布时间】:2012-03-17 03:26:54
【问题描述】:

我正在试验 UIImageView 对象沿贝塞尔路径移动的位置的关键帧动画。这张图片显示了动画之前的初始状态。蓝线是路径 - 最初是直线向上移动,浅绿色框是初始边界框或图像,深绿色“幽灵”是我正在移动的图像:

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

但是,当我将rotationMode 设置为kCAAnimationRotateAuto 的动画开始时,图像立即逆时针旋转90 度,并在整个路径中保持这个方向。当它到达路径的末端时,它会以正确的方向重绘(它实际上显示了我在最终位置重新定位的 UIImageView)

我天真地期望旋转模式会将图像定向到路径的切线而不是法线,尤其是当 Apple 文档为 CAKeyframeAnimation rotationMode 状态时

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

那么这里的解决方案是什么?我是否必须将图像顺时针预旋转 90 度?还是我遗漏了什么?

感谢您的帮助。


3 月 2 日编辑

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

theImage.transform = CGAffineTransformRotate(theImage.transform,90.0*M_PI/180);

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

theImage.transform = CGAffineTransformIdentity;

这使图像以预期的方式跟随路径。但是,我现在遇到了图像闪烁的另一个问题。我已经在寻找解决此 SO 问题中闪烁问题的方法:

iOS CAKeyFrameAnimation scaling flickers at animation end

所以现在我不知道我让事情变得更好或更糟了!


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 上传了一个演示项目,该项目展示了对象沿贝塞尔路径的移动。代码在PathMove

我还在我的博客Moving objects along a bezier path in iOS 中写过它

【问题讨论】:

  • 这对我帮助很大!谢谢!

标签: ios rotation cakeyframeanimation


【解决方案1】:

您提到您使用“rotationMode 设置为 YES”开始动画,但文档指出应该使用 NSString 设置rotationMode...

特别是:

这些常量由 rotationMode 属性使用。

NSString * const kCAAnimationRotateAuto

NSString * const kCAAnimationRotateAutoReverse

你试过设置吗:

keyframe.animationMode = kCAAnimationRotateAuto;

文档说明:

kCAAnimationRotateAuto:对象沿路径的切线移动。

【讨论】:

  • 只是问题文本有误,我使用的是nilkCAAnimationRotateAuto。刚刚编辑了问题以反映这一点。
【解决方案2】:

这里的问题是核心动画的自动旋转保持视图的水平轴平行于路径的切线。这就是它的工作原理。

如果您希望视图的垂直轴跟随路径的切线,那么按照您当前的操作旋转视图的内容是合理的做法。

【讨论】:

  • OK .. 这就是它的工作原理。那我会把它归结为文档问题。但是当我使用仿射旋转时,我遇到了上面编辑中提到的其他 SO 问题的问题。我应该使用 CA 来进行预旋转而不是仿射旋转吗?还是在其他问题中我做错了什么?
  • 我从未测试过使用 CGAffineTransform 是否会有所作为,所以我不能说。直观地说,由于您已经在考虑使用 Core Animation 的层,因此使用 Core Animation 的转换可能是有意义的:[theView.layer setValue:[NSNumber numberWithFloat:M_PI/2] forKey:@"transform.rotation"];。但是似乎CGAffineTransform应该使用CALayer的转换能力来实现,所以可能没关系。我现在时间不多,但有机会我会看看你的其他问题。
  • 从我的另一个问题的答案中,我会说核心动画的旋转是要走的路。当我得到这个问题的可靠答案时,我也会尝试以同样的方式解决轮换问题。
  • 您知道 Apple 所要做的就是计算对象初始状态和路径起点之间的矢量角度,并使用该角度来确定对象与路径的方向。这就是我真正想要的.. sniff
【解决方案3】:

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

  • 正如 Caleb 所指出的,Core Animation 会旋转您的图层,使其正 X 轴位于您路径的切线。您需要使您的图像的“自然”方向与之配合使用。因此,假设在您的示例图像中这是一艘绿色飞船,您需要飞船在没有应用旋转时指向右侧。

  • 设置包含旋转的变换会干扰“kCAAnimationRotateAuto”应用的旋转。在应用动画之前,您需要从变换中移除旋转。

  • 当然,这意味着您需要在动画完成时重新应用转换。当然,您希望这样做而不会在图像外观上看到任何闪烁。这并不难,但其中涉及一些秘诀,我将在下面解释。

  • 您可能希望您的宇宙飞船一开始就指向路径的切线,即使宇宙飞船静止不动且尚未设置动画。如果您的飞船图像指向右侧,但您的路径向上,那么您需要将图像的变换设置为包括 90° 旋转。但也许您不想对旋转进行硬编码 - 相反,您想查看路径并找出它的起始切线。

我将在这里展示一些重要的代码。你可以找到my test project on 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];

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

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

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

    [UIView animateWithDuration:3 animations:^{

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

        // 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;

接下来是防止动画结尾闪烁的秘诀。回想一下,动画会影响您将它们附加到的“模型层”的属性(在本例中为theImage.layer)。相反,它们会更新“表示层”的属性,以反映屏幕上的实际内容。

首先我将removedOnCompletion 设置为NO 用于关键帧动画。这意味着动画完成后,动画将保持连接到模型层,这意味着我可以访问表示层。我从表示层获取变换,删除动画,并将变换应用到模型层。由于这一切都发生在主线程上,所以这些属性的变化都是在一个屏幕刷新周期内发生的,所以没有闪烁。

        positionAnimation.removedOnCompletion = NO;
        [CATransaction setCompletionBlock:^{
            CGAffineTransform finalTransform = [theImage.layer.presentationLayer affineTransform];
            [theImage.layer removeAnimationForKey:positionAnimation.keyPath];
            theImage.transform = finalTransform;
        }];

现在我已经设置了完成块,我实际上可以更改视图属性。执行此操作时,系统会自动将动画附加到图层。

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

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

        // Copy properties from UIView's animation.
        CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
        positionAnimation.duration = autoAnimation.duration;
        positionAnimation.fillMode = autoAnimation.fillMode;

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

        // Replace UIView's animation with my animation.
        [theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
    }];

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

    _currentScale = newScale;
    _isReset = !_isReset;
}

这就是没有闪烁的动画图像视图。

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

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

当然,棘手的一点隐藏在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);
}

【讨论】:

  • 感谢您提供此答案的详细信息。对此,我真的非常感激。但是,从我在您的项目中看到的情况来看,您预先绘制了指向右侧的资源,将其旋转到显示的路径切线并移除了动画上的旋转。在我的情况下,我需要使用有效地预先绘制的向上指向的资源,以便在动画中它们需要旋转 90 度,然后沿着路径移动。稍后我将使用您的解决方案,看看我能自己弄清楚什么,因为您已经给了我一个很好的起点。再次感谢。
  • 今天早上我花了很长时间才弄清楚我需要做什么才能满足我的要求。感谢您为帮助我所做的努力!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-14
  • 1970-01-01
  • 1970-01-01
  • 2011-04-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多