12

ベジェ パスに沿って移動する UIImageView オブジェクトの位置のキー フレーム アニメーションを試しています。この写真は、アニメーション前の初期状態を示しています。青い線はパスです。最初はまっすぐ上に移動します。明るい緑色のボックスは最初のバウンディング ボックスまたは画像です。濃い緑色の「ゴースト」は移動中の画像です。

初期状態

rotationMode を に設定してアニメーションを開始するnilと、イメージはパス全体で期待どおりに同じ方向を保ちます。

しかし、rotationMode を に設定してアニメーションを開始するkCAAnimationRotateAutoと、画像はすぐに反時計回りに 90 度回転し、パス全体でこの向きを維持します。パスの最後に到達すると、正しい向きで再描画されます (最終的な場所に再配置した UIImageView が実際に表示されます)。

ここに画像の説明を入力

特にCAKeyframeAnimationのrotationMode状態に関するAppleドキュメントの場合、rotationModeが法線ではなくパスの接線に画像を向けることを素朴に期待していました

パスに沿ってアニメートするオブジェクトを回転させて、パスの接線に一致させるかどうかを決定します。

それで、ここでの解決策は何ですか?画像を事前に時計回りに 90 度回転させる必要がありますか? それとも、私が見逃しているものがありますか?

ご協力いただきありがとうございます。


3月2日編集

次のようなアフィン回転を使用して、パス アニメーションの前に回転ステップを追加しました。

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

パス アニメーションの後、次のように回転をリセットします。

theImage.transform = CGAffineTransformIdentity;

これにより、画像は期待どおりにパスをたどります。ただし、画像のちらつきという別の問題に直面しています。私はすでにこのSOの質問でちらつきの問題の解決策を探しています:

アニメーションの終了時に iOS CAKeyFrameAnimation スケーリングがちらつく

だから今、私は物事を良くしたか悪くしたかわかりません!


3月12日編集

Caleb は、はい、画像を事前に回転する必要があったと指摘しましたが、Rob は私の問題をほぼ完全に解決するすばらしいコード パッケージを提供してくれました。Rob がしなかった唯一のことは、アセットが水平方向ではなく垂直方向で描画されることを補うことでした。したがって、アニメーションを実行する前にアセットを 90 度事前回転する必要があります。でもねえ、物事を動かすために私がいくつかの仕事をしなければならないのは唯一の公平です.

したがって、私の要件に合わせてロブのソリューションをわずかに変更すると、次のようになります。

  1. UIView を追加するときは、事前に Rotate して、以下を設定することで追加された固有の回転に対抗しますrotationMode

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

  2. アニメーションの最後にその回転を保持する必要があるため、完了ブロックが定義された後にビューの変換を新しいスケール ファクターで爆破するのではなく、現在の変換に基づいてスケールを構築します。

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

これで、イメージが期待どおりのパスをたどるようにするために必要な作業はすべて完了です。


3月22日編集

ベジェ パスに沿ったオブジェクトの移動を示すデモ プロジェクトを GitHub にアップロードしました。コードはPathMoveにあります。

iOS でオブジェクトをベジエ パスに沿って移動するというブログにも書いています。

4

3 に答える 3

8

ここでの問題は、Core Animation の自動回転がビュ​​ーの水平軸をパスの接線と平行に保つことです。それがまさにそれがどのように機能するかです。

代わりに、ビューの垂直軸をパスの接線に沿わせたい場合は、現在行っているようにビューの内容を回転させるのが合理的です。

于 2012-03-03T15:00:22.593 に答える
6

ちらつきをなくすために知っておくべきことは次のとおりです。

  • 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);
}
于 2012-03-06T09:03:55.417 に答える
3

ここで、「rotationModeをYESに設定」してアニメーションを開始すると述べていますが、ドキュメントには、rotationModeはNSStringを使用して設定する必要があると記載されています...

特に:

これらの定数は、rotationModeプロパティによって使用されます。

NSString * const kCAAnimationRotateAuto

NSString * const kCAAnimationRotateAutoReverse

設定してみましたか:

keyframe.animationMode = kCAAnimationRotateAuto;

ドキュメントには次のように記載されています。

kCAAnimationRotateAuto:オブジェクトはパスの接線上を移動します。

于 2012-03-03T07:34:09.440 に答える