ios - 描画 - uibezierpath 円




アニメーション境界のアニメーションCAShapeLayerパスが変更されました (3)

boundsがアニメーション化されている間にCAShapeLayerアニメーション化pathのための発見されたソリューション:

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

私はCAShapeLayer(UIViewサブクラスのレイヤー)を持っていpathが、ビューのboundsサイズが変わるたびにそのpathが更新されるはずです。 このために、レイヤの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用です.iOS 7ではアニメーションの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に変換するのを忘れていCGPathRef ありがとう@antonioviero

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

  3. より一般的には、これが最善のアプローチですか? どの境界線のサイズにも依存するシェイプレイヤーを持つと、境界線の変更と同期してアニメートする必要がありますが、これはちょうど動作するはずですが、私は苦労しています。 私は良い方法がなければならないと感じています。

私が試したことの他のもの

  • pathプロパティのカスタムアニメーションオブジェクトを返すために、レイヤのactionForKey:またはビューのactionForLayer:forKey:をオーバーライドします。 私はこれが好ましい方法だと思いますが、囲むアニメーションブロックによって設定されるべきトランザクションプロパティを取得する方法が見つかりませんでした。 アニメーションブロック内から呼び出されても、 [CATransaction animationDuration ]などは常にデフォルト値を返します。

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

プロジェクト

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

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

更新

Jose Luis Piedrahita Nick Lockwoodによってこの記事を 指摘しました。 これは以下のアプローチを示唆しています:

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

  2. もしそうなら、層の "普通の"アニメーション化可能なプロパティ( @"bounds" )のsuperを呼び出すと、あなたがアニメーションブロックの中にいるかどうかが暗黙のうちに伝えられます。 ビュー(レイヤーのデリゲート)が有効なオブジェクト( nilまたはNSNullはない)を返す場合は、そうです。

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

これは実際にiOS 7.xの下で素晴らしい動作をします。 しかし、iOS 8の下では、 actionForLayer:forKey:から返されたオブジェクトは、標準の(文書化された) CA...AnimationはありませんCA...Animationなく、private _UIViewAdditiveAnimationActionクラス( NSObjectサブクラス)のインスタンスです。 このクラスのプロパティは文書化されていないので、パスアニメーションを簡単に作成することはできません。

_Edit:それはちょうどすべての後に動作するかもしれません。 Nickが彼の記事で述べたように、 backgroundColorようないくつかのプロパティは、まだ標準CA...Animation返すCA...Animation iOS上でのCA...Animation 8.私はそれを試してみる必要があります。


私は、UIVIewスプリングとCABasicAnimationの間の異なるタイミング機能のため、境界とパスアニメーションの同期が外れていると思います。

たぶんあなたはアニメーショントランスフォームを試すことができます、それはまた、(テストされていない)パスを変換する必要がありますアニメーションを完了した後、あなたはバインドを設定することができます。

もう1つの方法は、スナップショットのパスを取ってレイヤーのコンテンツとして設定し、バインドをアニメートし、コンテンツがアニメーションに従うようにすることです。


私はこれが古い質問であることを知っていますが、ロックウッズ氏のアプローチに似たような解決策を提供することができます。 残念ながら、ここのソースコードは速いので、ObjCに変換する必要があります。

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

良いニュースは、actionForLayer:forKey:実際にbackForceレイヤーの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)
    }

}






cashapelayer