ちらつきをなくすために知っておくべきことは次のとおりです。
Caleb が指摘したように、Core Animation はレイヤーを回転させて、正の X 軸がパスの接線に沿うようにします。画像の「自然な」向きをそれで機能させる必要があります。したがって、それがサンプル画像の緑色の宇宙船であると仮定すると、回転が適用されていないときに宇宙船が右を指す必要があります。
回転を含む変換を設定すると、「kCAAnimationRotateAuto」によって適用される回転が妨げられます。アニメーションを適用する前に、変換から回転を削除する必要があります。
もちろん、アニメーションが完了したら、変換を再適用する必要があります。そしてもちろん、画像の外観にちらつきが見られないようにする必要があります。それは難しいことではありませんが、以下で説明するいくつかの秘密のソースが関係しています.
宇宙船がまだアニメーション化されておらず、静止している場合でも、宇宙船がパスの接線に沿って指し示すようにしたい場合があります。宇宙船の画像が右を向いているのにパスが上に向いている場合、90° の回転を含むように画像の変換を設定する必要があります。しかし、おそらく、その回転をハードコーディングしたくないでしょう。代わりに、パスを見て、その開始接線を把握したいでしょう。
ここで重要なコードのいくつかを示します。私のテスト プロジェクトは github にあります。ダウンロードして試してみると、いくつかの用途が見つかるかもしれません。緑色の「宇宙船」をタップするだけで、アニメーションが表示されます。
そのため、私のテスト プロジェクトでは、 をUIImageView
という名前のアクションに接続しましたanimate:
。タッチすると、画像が 8 の字の半分に沿って移動し、サイズが 2 倍になります。もう一度タッチすると、画像は 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
これは、アニメーションが完了したときにアニメーションがモデル レイヤーにアタッチされたままになることを意味します。つまり、プレゼンテーション レイヤーにアクセスできます。プレゼンテーション レイヤーから変換を取得し、アニメーションを削除して、変換をモデル レイヤーに適用します。これはすべてメイン スレッドで行われるため、これらのプロパティの変更はすべて 1 回の画面更新サイクルで行われるため、ちらつきはありません。
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;
}
ちらつきのない画像ビューのアニメーションは以上です。
そして今、スティーブ・ジョブズが言うように、One Last Thing. ビューをロードするとき、アニメーション化に使用する最初のパスの接線に沿って回転するように、画像ビューの変換を設定する必要があります。という名前のメソッドでそれを行いますreset
:
- (void)reset {
self.imageView.center = _path1.currentPoint;
self.imageView.transform = CGAffineTransformMakeRotation(startRadiansForPath(_path0));
_currentScale = 1;
_isReset = YES;
}
もちろん、そのstartRadiansForPath
機能にはトリッキーな部分が隠されています。それは本当に難しいことではありません。関数を使用しCGPathApply
てパスの要素を処理し、実際にサブパスを形成する最初の 2 点を選び出し、これらの 2 点によって形成される線の角度を計算します。(曲線パス セクションは 2 次ベジエ スプラインまたは 3 次ベジエ スプラインのいずれかであり、これらのスプラインには、スプラインの最初の点での接線が最初の点から次の制御点までの線であるというプロパティがあります。)
後世のために、説明なしでここにコードをダンプします。
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);
}