Анимация изменения размера маски CALayer - PullRequest
27 голосов
/ 26 мая 2010

У меня есть подкласс UIView, который использует маску CAShapeLayer на своем CALayer.В маске используется отличная форма с тремя закругленными углами и вырезанным прямоугольником в оставшемся углу.

Когда я изменяю размер моего UIView, используя стандартный анимационный блок, сам UIView и его CALayerизменить размер просто отлично.Маска, однако, применяется мгновенно, что приводит к некоторым проблемам при рисовании.

Я пытался анимировать изменение размера маски с помощью CABasicAnimation, но мне не повезло, получая анимацию изменения размера.

Можно ли как-нибудь добиться анимированного эффекта изменения размера маски?Нужно ли мне избавиться от маски, или мне придется что-то изменить в способе рисования маски (используя - (void)drawInContext:(CGContextRef)ctx).

Приветствия, Алекс

Ответы [ 8 ]

42 голосов
/ 03 ноября 2012

Я нашел решение этой проблемы. Другие ответы частично верны и полезны.

Следующие пункты важны для понимания решения:

  • Свойство mask само по себе не анимируемо.
  • Поскольку маска является CALayer, ее можно анимировать самостоятельно.
  • Кадр не анимируемый, используйте границы и положение. Это может не относиться к вам (если вы не пытались анимировать кадр), но было проблемой для меня. (См. Apple QA 1620 )
  • Маска слоя представления не привязана к UIView, поэтому она не получит транзакцию основной анимации, которая применяется к слою представления.
  • Мы модифицируем CALayer напрямую, поэтому мы не можем ожидать, что UIView будет иметь какое-либо представление о том, что мы пытаемся сделать, поэтому анимация UIView не создаст основную транзакцию анимации для включения изменений в наши свойства.

Чтобы решить эту проблему, нам самим придется задействовать Core Animation, и мы не можем полагаться на блок анимации UIView, чтобы выполнить работу за нас.

Просто создайте CATransaction с той же продолжительностью, которую вы используете с [UIView animateWithDuration:...]. Это создаст отдельную анимацию, но если ваша длительность и функция замедления одинаковы, она должна точно анимироваться с другими анимациями в вашем блоке анимации.

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];
10 голосов
/ 20 марта 2014

Я использую CAShapeLayer, чтобы замаскировать UIView, установив self.layer.mask для этого слоя формы.

Чтобы анимировать маску всякий раз, когда изменяется размер представления, я переписал -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)

My -createMaskPath вычисляет CGPathRef, который я использую для маскировки вида. Я также обновляю путь маски в -layoutSubviews.

5 голосов
/ 28 марта 2014

Чтобы анимировать изменение границ слоя маски 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 голосов
/ 01 июня 2010

Свойство маски CALayer не может быть анимировано, что объясняет отсутствие удачи в этом направлении.

Зависит ли рисунок вашей маски от рамки / границ маски? (Можете ли вы предоставить некоторый код?) Есть ли в маске свойство needsDisplayOnBoundsChange?

Ура, Корин

2 голосов
/ 28 сентября 2010

Параметр маски не анимируется, но вы можете анимировать слой, который установлен как маска ...

Если вы анимируете свойство Path CAShapeLayer's, это должно анимировать маску. Я могу убедиться, что это работает из моих собственных проектов. Не уверен насчет использования не векторной маски. Вы пробовали анимировать свойство содержимого маски?

Спасибо, Jon

0 голосов
/ 11 апреля 2019

Swift 3 + ответ на основе решения Кекоа :

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 голосов
/ 03 января 2013

Можно анимировать смену маски.

Я предпочитаю использовать CAShapeLayer в качестве слоя маски. Анимировать изменение маски очень удобно с помощью свойства path.

Прежде чем анимировать любое изменение, поместите содержимое источника в экземпляр 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 голосов
/ 21 июня 2012

Я не смог найти никакого программного решения, поэтому я просто нарисовал изображение в формате png с правильной формой и значениями альфа и использовал его вместо этого. Таким образом, мне не нужно использовать маску ...

...