web-dev-qa-db-ja.com

CALayerのマスクサイズの変更をアニメーション化する

UIViewCAShapeLayerマスクを使用するCALayerサブクラスがあります。マスクは独特の形状を使用しており、3つの丸い角と、残りの角に切り抜かれた長方形があります。

標準のアニメーションブロックを使用してUIViewのサイズを変更すると、UIView自体とそのCALayerのサイズが適切に変更されます。ただし、マスクは即座に適用されるため、描画の問題が発生します。

CABasicAnimationを使用してマスクのサイズ変更をアニメーション化しようとしましたが、サイズ変更をアニメーション化することができませんでした。

どういうわけか、マスクにアニメーションのサイズ変更効果を実現できますか?マスクを取り除く必要がありますか、それとも現在マスクを描画する方法について何かを変更する必要がありますか(- (void)drawInContext:(CGContextRef)ctxを使用)。

乾杯、アレックス

28
Alex Repty

私はこの問題の解決策を見つけました。他の答えは部分的に正しく、役に立ちます。

ソリューションを理解するには、次の点が重要です。

  • マスクプロパティ自体はアニメートできません。
  • マスクはCALayerであるため、それ自体でアニメーション化できます。
  • フレームはアニメートできません。境界と位置を使用してください。これはあなたには当てはまらないかもしれませんが(フレームをアニメーション化しようとしていない場合)、私にとっては問題でした。 ( Apple QA 162 を参照)
  • ビューレイヤーのマスクはUIViewに関連付けられていないため、ビューのレイヤーに適用されるCoreAnimationトランザクションを受け取りません。
  • CALayerを直接変更しているため、UIViewが何をしようとしているのかを把握できないため、UIViewアニメーションはプロパティへの変更を含むコアアニメーショントランザクションを作成しません。

解決するには、Core Animationを自分で利用する必要があり、UIViewアニメーションブロックに依存して作業を行うことはできません。

[UIView animateWithDuration:...]で使用しているのと同じ期間でCATransactionを作成するだけです。これにより別のアニメーションが作成されますが、継続時間とイージング機能が同じである場合は、アニメーションブロック内の他のアニメーションと正確にアニメーション化する必要があります。

NSTimeInterval duration = 0.5;// match this to the value of the UIView animateWithDuration: call

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];

self.myView.layer.mask.position = CGPointMake(newX, 0);
self.myView.layer.mask.bounds = CGRectMake(0, 0, newWidth, newHeight);

[CATransaction commit];
44
Kekoa

CAShapeLayerを使用して、self.layer.maskをそのシェイプレイヤーに設定することにより、UIViewをマスクします。

ビューのサイズが変更されるたびにマスクをアニメーション化するには、アニメーション中に境界が変更された場合に、-setBounds:を上書きしてマスクレイヤーパスをアニメーション化します。

これが私がそれを実装した方法です:

- (void)setBounds:(CGRect)bounds
{
    [super setBounds:bounds];
    CAPropertyAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds"];

    // update the mask
    self.maskLayer.frame = self.layer.bounds;

    // if the bounds change happens within an animation, also animate the mask path
    if (!boundsAnimation) {
        self.maskLayer.path = [self createMaskPath];
    } else {
        // copying the original animation allows us to keep all animation settings
        CABasicAnimation *animation = [boundsAnimation copy];
        animation.keyPath = @"path";

        CGPathRef newPath = [self createMaskPath];
        animation.fromValue = (id)self.maskLayer.path;
        animation.toValue = (__bridge id)newPath;

        self.maskLayer.path = newPath;

        [self.maskLayer addAnimation:animation forKey:@"path"];
    }
}

(例では、self.maskLayerは `self.layer.maskに設定されています)

私の-createMaskPathは、ビューをマスクするために使用するCGPathRefを計算します。 -layoutSubviewsのマスクパスも更新します。

10
stigi

UIViewのマスクレイヤーの境界の変更をアニメーション化するには:サブクラスUIView、およびCATransactionを使用してマスクをアニメーション化します-Kekodasの回答に似ていますが、より一般的です:

@implementation UIMaskView

- (void) layoutSubviews {
    [super layoutSubviews];

    CAAnimation* animation = [self.layer animationForKey:@"bounds"];
    if (animation) {
        [CATransaction begin];
        [CATransaction setAnimationDuration:animation.duration];
    }

    self.layer.mask.bounds = self.layer.bounds;
    if (animation) {
        [CATransaction commit];
    }
}

@end
5
Nick H247

CALayerのマスクプロパティはアニメートできません。これは、その方向での運の欠如を説明しています。

マスクの描画は、マスクのフレーム/境界に依存しますか? (コードを提供できますか?)マスクにはneedsDisplayOnBoundsChangeプロパティが設定されていますか?

乾杯、コリン

5
Corin

マスクパラメータはアニメートしませんが、マスクとして設定されているレイヤーをアニメートできます。

CAShapeLayerのPathプロパティをアニメーション化すると、マスクがアニメーション化されます。これが自分のプロジェクトで機能することを確認できます。ただし、非ベクトルマスクの使用についてはよくわかりません。マスクのcontentsプロパティをアニメーション化してみましたか?

ありがとう、ジョン

2
Jon Hull

プログラムによる解決策が見つからなかったので、正しい形状とアルファ値でpng画像を描画し、代わりにそれを使用しました。そうすれば、マスクを使う必要はありません...

0
Ida

Swift 3 +Kekoaのソリューション に基づく回答:

let duration = 0.15 //match this to the value of the UIView.animate(withDuration:) call

CATransaction.begin()
CATransaction.setValue(duration, forKey: kCATransactionAnimationDuration)

myView.layer.mask.position = CGPoint(x: [new X], y: [new Y]) //just an example

CATransaction.commit()
0
Tamás Sengel

マスクの変更をアニメートすることが可能です。

CAShapeLayerをマスクレイヤーとして使用することを好みます。プロパティパスを使用してマスクの変更をアニメーション化すると非常に便利です。

変更をアニメーション化する前に、ソースのコンテンツをインスタンスCGImageRefにダンプし、アニメーション用の新しいレイヤーを作成します。アニメーション中に元のレイヤーを非表示にし、アニメーションが終了したときに表示します。

以下は、プロパティパスにキーアニメーションを作成するためのサンプルコードです。独自のパスアニメーションを作成する場合は、パスに常に同じ数のポイントがあることを確認してください。

- (CALayer*)_mosaicMergeLayer:(CGRect)bounds content:(CGImageRef)content isUp:(BOOL)isUp {

    CALayer* layer = [CALayer layer];
    layer.frame = bounds;
    layer.backgroundColor = [[UIColor clearColor] CGColor];
    layer.contents = (id)content;

    CAShapeLayer* maskLayer = [CAShapeLayer layer];
    maskLayer.fillColor = [[UIColor blackColor] CGColor];
    maskLayer.frame = bounds;
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    maskLayer.path = ( isUp ? [self _maskArrowUp:-bounds.size.height*2] : [self _maskArrowDown:bounds.size.height*2] );
    layer.mask = maskLayer;

    CAKeyframeAnimation* ani = [CAKeyframeAnimation animationWithKeyPath:@"path"];
    ani.removedOnCompletion = YES;
    ani.duration = 0.3f;
    ani.fillMode = kCAFillModeForwards;
    ani.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    NSArray* values = ( isUp ?
        [NSArray arrayWithObjects:
        (id)[self _maskArrowUp:0],
        (id)[self _maskArrowUp:-ceilf(bounds.size.height*1.2)],
        nil] 
    :       
        [NSArray arrayWithObjects:
        (id)[self _maskArrowDown:0],
        (id)[self _maskArrowDown:bounds.size.height],
        nil] 
    );
    ani.values = values;
    ani.delegate = self;
    [maskLayer addAnimation:ani forKey:nil];

    return layer;
}

- (void)_startMosaicMergeAni:(BOOL)up {

    CALayer* overlayer = self.aniLayer;
    CGRect bounds = overlayer.bounds;
    self.firstHalfAni = NO;

    CALayer* frontLayer = nil;
    frontLayer = [self _mosaicMergeLayer:bounds 
                                content:self.toViewSnapshot 
                                isUp:up];
    overlayer.contents = (id)self.fromViewSnapshot;
    [overlayer addSublayer:frontLayer];
}
0
Slavik