Подтвердить что ты не робот

Анимация изменения размера маски CALayer

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

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

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

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

Cheers, Alex

4b9b3361

Ответ 1

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

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

  • Свойство 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];

Ответ 2

Я использую 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.

Ответ 3

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

Соответствует ли чертеж вашей маски рамке/границам маски? (Можете ли вы предоставить какой-то код?) Имеет ли в маске набор свойствDisplayOnBoundsChange?

Cheers, Корин

Ответ 4

Анимировать изменение границ слоя маски 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

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

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

Спасибо, Джон

Ответ 6

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

Ответ 7

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

Я предпочитаю использовать 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];
}

Ответ 8

Быстрый ответ 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()