以下是消除闪烁的注意事项:
- 正如Caleb所指出的,核心动画会将图层旋转,使其正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];
所以,我需要做的第一件事是从图像变换中移除任何旋转,因为正如我所提到的,它会干扰“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];
}];
最后,我更新实例变量以反映图像视图的更改:
_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);
}