以下是消除闪烁需要了解的内容:
正如 Caleb 指出的那样,Core Animation 会旋转您的图层,使其正 X 轴位于您路径的切线。您需要使您的图像的“自然”方向与之配合使用。因此,假设在您的示例图像中这是一艘绿色宇宙飞船,您需要宇宙飞船在没有应用旋转时指向右侧。
设置包含旋转的变换会干扰“kCAAnimationRotateAuto”应用的旋转。在应用动画之前,您需要从变换中删除旋转。
当然,这意味着您需要在动画完成时重新应用转换。当然,您希望这样做而不会在图像外观上看到任何闪烁。这并不难,但其中涉及到一些秘方,我将在下面解释。
您可能希望您的宇宙飞船开始沿着路径的切线指向,即使当宇宙飞船静止不动时还没有动画。如果您的飞船图像指向右侧,但您的路径向上,那么您需要将图像的变换设置为包括 90° 旋转。但也许您不想对旋转进行硬编码 - 相反,您想查看路径并找出它的起始切线。
我将在这里展示一些重要的代码。你可以在 github 上找到我的测试项目。您可能会在下载和试用它时发现一些用处。只需点击绿色的“宇宙飞船”即可查看动画。
因此,在我的测试项目中,我已将 my 连接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);
}