UIImageView上的iOS CAKeyframeAnimation rotationMode

我正在试验沿着贝塞尔path移动的UIImageView对象位置的关键帧animation。 此图显示了animation之前的初始状态。 蓝线是path – 最初是直线向上移动,浅绿色的框是最初的边界框或图像,深绿色的“鬼”是我正在移动的图像:

初始状态

当我将rotationMode设置为nil下启动animation时,图像始终保持与预期一样的方向。

但是,当我使用rotationMode设置为kCAAnimationRotateAutokCAAnimationRotateAutoanimation时,图像将立即逆时针旋转90度,并始终保持此方向通过path。 当它到达path的末端时,它会以正确的方向重新绘制(它实际上显示了我重新定位到最终位置的UIImageView)

在这里输入图像说明

我天真地期望rotationMode将图像定位到path的正切,而不是正常,特别是当Apple文档为CAKeyframeAnimation rotationMode状态

确定沿pathanimation的对象是否旋转以匹配path切线。

那么这里的解决scheme是什么? 我是否需要将图像顺时针旋转90度? 还是有什么我失踪?

谢谢你的帮助。


编辑3月2日

我在pathanimation之前使用仿射旋转添加了一个旋转步骤,如下所示:

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

然后在pathanimation之后,重置旋转:

 theImage.transform = CGAffineTransformIdentity; 

这使得图像以预期的方式跟随path。 不过,我现在遇到了图像闪烁的另一个问题。 我已经在这个SO问题中寻找闪烁问题的解决scheme:

animation结束时,iOS CAKeyFrameAnimation缩放闪烁

所以现在我不知道我是做得更好还是更糟!


3月12日编辑

虽然迦勒指出是的,我不得不预先旋转我的形象,罗布提供了一个很好的代码包,几乎完全解决了我的问题。 Rob唯一没有做的就是补偿我的资产是以垂直方向而不是水平方向绘制的,因此在做animation之前还需要将它们预先旋转90度。 但是,嘿,唯一公平的是我必须做一些工作才能使事情顺利进行。

所以我对Rob的解决scheme进行了一些细微的修改以适应我的要求:

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

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

  2. 我需要在animation结束时保持这个旋转,所以我没有在完成块被定义之后用新的比例因子对视图的变换进行爆破,而是基于当前的变换构build比例:

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

这就是我所需要做的就是让自己的形象像我期望的那样走上去!


3月22日编辑

我刚刚上传到GitHub上的一个演示项目,展示了一个沿着贝塞尔path移动的对象。 代码可以在PathMove中find

我还在我的博客中写过关于在iOS中沿着贝塞尔path移动对象的问题

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

如果你希望你的视图的垂直轴跟着path的切线,那么按照你现在正在做的那样旋转视图的内容是合理的。

以下是您需要了解的消除闪烁的方法:

  • 正如Caleb指出的那样,Core Animation旋转你的图层​​,使得它的正X轴沿着你的path的切线。 你需要使你的形象“自然”的方向工作。 所以,假设在你的图像中有一个绿色的宇宙飞船,你需要宇宙飞船指向没有旋转的宇宙飞船。

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

  • 当然这意味着当animation完成时你需要重新应用转换。 当然,你希望这样做,而不会看到图像的外观闪烁。 这并不难,但有一些秘密的酱油,我将在下面解释。

  • 你大概希望你的飞船开始指向沿着path的切线,即使当飞船坐着仍然没有animation。 如果您的飞船图像指向右侧,但您的path上升,则需要设置图像的变换以包含90°旋转。 但是也许你不想硬编码这个旋转 – 而是想看看这个path并找出它的起点。

我会在这里展示一些重要的代码。 你可以在github上find我的testing项目 。 您可能会发现一些用于下载并尝试的方法。 只要点击绿色的“飞船”看animation。

所以,在我的testing项目中,我已经将我的UIImageView连接到一个名为animate:的动作。 当你触摸它时,图像沿着数字8的一半移动,并且尺寸加倍。 再次触摸时,图像将沿着图8的另一半(返回到开始位置)移动,并返回到其原始大小。 这两个animation使用kCAAnimationRotateAuto ,所以图像沿着path的切线。

这里是animate:的开始animate:我在哪里弄清楚图像应该以什么样的path,比例和目的地点结束:

 - (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); 

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

  [UIView animateWithDuration:3 animations:^{ 

我为这个位置创build了关键帧animation并设置了它的一些属性:

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

接下来是在animation结束时防止闪烁的秘诀 。 回想一下,animation不会影响您附加到它们的“模型图层”的属性(在这种情况下为图片层)。 相反,他们更新了“表示层”的属性,这反映了屏幕上的实际情况。

所以首先,我将removedOnCompletion设置为NO作为关键帧animation。 这意味着animation完成后,animation将保持附加到模型层,这意味着我可以访问表示层。 我从表示层获取转换,删除animation,并将转换应用于模型图层。 由于这一切都发生在主线程上,这些属性更改都发生在一个屏幕刷新周期中,所以没有闪烁。

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

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

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

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

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

最后我用关键帧animationreplace了自动添加的位置animation:

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

双终于我更新我的实例variables,以反映对图像视图的更改:

  _currentScale = newScale; _isReset = !_isReset; } 

这是animation图像视图没有闪烁。

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

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

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

我只是在这里抛弃代码,没有解释,后代:

 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); } 

YOW提到您启动animation“rotationMode设置为YES”,但该文档指出,应使用NSString设置rotationMode …

尤其是:

这些常量由rotationMode属性使用。

NSString * const kCAAnimationRotateAuto

NSString * const kCAAnimationRotateAutoReverse

你试过设置:

keyframe.animationMode = kCAAnimationRotateAuto;

该文件指出:

kCAAnimationRotateAuto:对象沿path切线。