web-dev-qa-db-ja.com

アニメーション化された境界の変更でCAShapeLayerパスをアニメーション化する

CAShapeLayer(UIViewサブクラスのレイヤー)があり、ビューのpathサイズが変更されるたびにboundsを更新する必要があります。このために、レイヤーの_setBounds:_メソッドをオーバーライドしてパスをリセットしました。

_@interface CustomShapeLayer : CAShapeLayer
@end

@implementation CustomShapeLayer

- (void)setBounds:(CGRect)bounds
{
    [super setBounds:bounds];
    self.path = [[self shapeForBounds:bounds] CGPath];
}
_

これは、境界の変更をアニメーション化するまでうまく機能します。 (UIViewアニメーションブロックで)アニメーション化された境界の変更に沿ってパスをアニメーション化して、シェイプレイヤーが常にそのサイズに合わせられるようにしたいと考えています。

pathプロパティはデフォルトではアニメーション化しないため、これを思いつきました。

レイヤーの_addAnimation:forKey:_メソッドをオーバーライドします。その中で、境界アニメーションがレイヤーに追加されているかどうかを確認します。その場合は、メソッドに渡される境界アニメーションからすべてのプロパティをコピーして、境界に沿ってパスをアニメーション化する2つ目の明示的なアニメーションを作成します。コードで:

_- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key
{
    [super addAnimation:animation forKey:key];

    if ([animation isKindOfClass:[CABasicAnimation class]]) {
        CABasicAnimation *basicAnimation = (CABasicAnimation *)animation;

        if ([basicAnimation.keyPath isEqualToString:@"bounds.size"]) {
            CABasicAnimation *pathAnimation = [basicAnimation copy];
            pathAnimation.keyPath = @"path";
            // The path property has not been updated to the new value yet
            pathAnimation.fromValue = (id)self.path;
            // Compute the new value for path
            pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath];

            [self addAnimation:pathAnimation forKey:@"path"];
        }
    }
}
_

このアイデアは、DavidRönnqvistの View-Layer Synergyの記事 を読んで得ました。 (このコードはiOS 8用です。iOS7では、アニメーションのkeyPathを `@" bounds.size "ではなく_@"bounds"_に対してチェックする必要があるようです。)

ビューのアニメーション化された境界の変更をトリガーする呼び出しコードは、次のようになります。

_[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
    self.customShapeView.bounds = newBounds;
} completion:nil];
_

ご質問

これはほとんど機能しますが、2つの問題があります。

  1. 前のアニメーションの進行中にこのアニメーションをトリガーすると、CGPathApply()で(_EXC_BAD_ACCESS_)がクラッシュすることがあります。これが私の特定の実装と関係があるかどうかはわかりません。 編集:気にしないでください。 UIBezierPathCGPathRefに変換するのを忘れました。 ありがとう@antonioviero

  2. 標準のUIViewスプリングアニメーションを使用すると、ビューの境界とレイヤーのパスのアニメーションがわずかに同期しなくなります。つまり、パスアニメーションも弾力的な方法で実行されますが、ビューの境界に正確には従いません。

  3. より一般的に、これは最善のアプローチですか?パスが境界サイズに依存し、任意のオブジェクトと同期してアニメーション化する必要があるシェイプレイヤーがあるようです境界の変更はうまくいくはずですが™、私は苦労しています。もっと良い方法があるはずだと思います。

私が試した他のこと

  • pathプロパティのカスタムアニメーションオブジェクトを返すには、レイヤーの_actionForKey:_またはビューの_actionForLayer:forKey:_をオーバーライドします。これが望ましい方法だと思いますが、外側のアニメーションブロックで設定する必要があるトランザクションプロパティを取得する方法が見つかりませんでした。アニメーションブロック内から呼び出された場合でも、_[CATransaction animationDuration_]などは常にデフォルト値を返します。

(a)現在アニメーションブロック内にいることを確認する方法、および(b)そこに設定されているアニメーションプロパティ(期間、アニメーションカーブなど)を取得する方法はありますか?ブロック?

事業

アニメーションは次のとおりです。オレンジ色の三角形は、シェイプレイヤーのパスです。黒のアウトラインは、シェイプレイヤーをホストするビューのフレームです。

Screenshot

GitHubのサンプルプロジェクトをご覧ください 。 (このプロジェクトはiOS 8用であり、実行するにはXcode 6が必要です。申し訳ありません。)

更新

Jose Luis Piedrahita 私を指摘この記事はNick Lockwood で、次のアプローチを示唆しています。

  1. ビューの_actionForLayer:forKey:_またはレイヤーの_actionForKey:_メソッドをオーバーライドし、このメソッドに渡されたキーがアニメーション化するものであるかどうかを確認します(_@"path"_)。

  2. その場合、レイヤーの「通常の」アニメート可能なプロパティ(_@"bounds"_など)の1つでsuperを呼び出すと、アニメーションブロック内にいるかどうかが暗黙的に通知されます。ビュー(レイヤーのデリゲート)が(nilまたはNSNullではなく)有効なオブジェクトを返す場合、そのとおりです。

  3. _[super actionForKey:]_から返されたアニメーションからパスアニメーションのパラメーター(期間、タイミング関数など)を設定し、パスアニメーションを返します。

これは確かにiOS 7.xでうまく機能します。ただし、iOS 8では、_actionForLayer:forKey:_から返されるオブジェクトは標準(および文書化)_CA...Animation_ではなく、プライベート__UIViewAdditiveAnimationAction_クラス(NSObjectサブクラス)のインスタンスです。このクラスのプロパティはドキュメント化されていないため、パスアニメーションを作成するためにそれらを簡単に使用することはできません。

_Edit:または、結局はうまくいくかもしれません。 Nickが彼の記事で述べたように、backgroundColorのようないくつかのプロパティは、iOS 8でも標準の_CA...Animation_を返します。試してみる必要があります。`

40
Ole Begemann

私はこれが古い質問であることを知っていますが、ロックウッド氏のアプローチと同様に見える解決策を提供できます。悲しいことに、ここのソースコードはSwiftなので、ObjCに変換する必要があります。

前述のように、レイヤーがビューのバッキングレイヤーである場合は、ビュー自体のCAActionをインターセプトできます。ただし、これは、たとえば、バッキングレイヤーが複数のビューで使用されている場合には便利ではありません。

良いニュースは、actionForLayer:forKey:が実際にバッキングレイヤーのactionForKey:を呼び出すことです。

これはactionForKeyにあります。これらの呼び出しをインターセプトして、パスが変更されたときのアニメーションを提供できるバッキングレイヤーにあります。

Swiftで記述されたレイヤーの例は次のとおりです。

class AnimatedBackingLayer: CAShapeLayer
{

    override var bounds: CGRect
    {
        didSet
        {
            if !CGRectIsEmpty(bounds)
            {
                path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath
            }
        }
    }

    override func actionForKey(event: String) -> CAAction?
    {
        if event == "path"
        {
            if let action = super.actionForKey("backgroundColor") as? CABasicAnimation
            {
                let animation = CABasicAnimation(keyPath: event)
                animation.fromValue = path
                // Copy values from existing action
                animation.autoreverses = action.autoreverses
                animation.beginTime = action.beginTime
                animation.delegate = action.delegate
                animation.duration = action.duration
                animation.fillMode = action.fillMode
                animation.repeatCount = action.repeatCount
                animation.repeatDuration = action.repeatDuration
                animation.speed = action.speed
                animation.timingFunction = action.timingFunction
                animation.timeOffset = action.timeOffset
                return animation
            }
        }
        return super.actionForKey(event)
    }

}
6
Andy

レイヤーのフレームとパスを同時に遊んでいるので問題があると思います。

私は、カスタムのdrawRectを持つCustomViewを使用します。

[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
    self.customView.bounds = newBounds;
} completion:nil];

またはパスを使用する必要はまったくありません

これが私がこのアプローチを使って得たものです https://dl.dropboxusercontent.com/u/73912254/triangle.mov

3

pathがアニメーション化されているときにCAShapeLayerboundsをアニメーション化するための解決策が見つかりました:

typedef UIBezierPath *(^PathGeneratorBlock)();

@interface AnimatedPathShapeLayer : CAShapeLayer

@property (copy, nonatomic) PathGeneratorBlock pathGenerator;

@end

@implementation AnimatedPathShapeLayer

- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
    if ([key rangeOfString:@"bounds.size"].location == 0) {
        CAShapeLayer *presentationLayer = self.presentationLayer;
        CABasicAnimation *pathAnim = [anim copy];
        pathAnim.keyPath = @"path";
        pathAnim.fromValue = (id)[presentationLayer path];
        pathAnim.toValue = (id)self.pathGenerator().CGPath;
        self.path = [presentationLayer path];
        [super addAnimation:pathAnim forKey:@"path"];
    }
    [super addAnimation:anim forKey:key];
}

- (void)removeAnimationForKey:(NSString *)key {
    if ([key rangeOfString:@"bounds.size"].location == 0) {
        [super removeAnimationForKey:@"path"];
    }
    [super removeAnimationForKey:key];
}

@end

//

@interface ShapeLayeredView : UIView

@property (strong, nonatomic) AnimatedPathShapeLayer *layer;

@end

@implementation ShapeLayeredView

@dynamic layer;

+ (Class)layerClass {
    return [AnimatedPathShapeLayer class];
}

- (instancetype)initWithGenerator:(PathGeneratorBlock)pathGenerator {
    self = [super init];
    if (self) {
        self.layer.pathGenerator = pathGenerator;
    }
    return self;
}

@end
2
k06a

境界とパスアニメーションが同期していないのは、UIVIewスプリングとCABasicAnimationのタイミング関数が異なるためです。

代わりに、アニメーション変換を試すこともできます。パスも変換する必要があります(テストされていない)。アニメーションが終了したら、境界を設定できます。

もう1つの可能な方法は、パスのスナップショットを取り、それをレイヤーのコンテンツとして設定し、境界をアニメーション化することです。コンテンツはアニメーションに従う必要があります。

1