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

Анимировать путь CAShapeLayer при изменении анимированных границ

У меня есть CAShapeLayer (который является слоем подкласса UIView), чей path должен обновляться при изменении размера представления bounds. Для этого я переопределил метод layer setBounds: на reset путь:

@interface CustomShapeLayer : CAShapeLayer
@end

@implementation CustomShapeLayer

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

Это отлично работает, пока я не изменил границы. Я хотел бы, чтобы анимация анимации наряду с любыми изменениями анимированных границ (в блоке анимации UIView), чтобы слой формы всегда принимал его размер.

Поскольку свойство path по умолчанию не анимируется, я придумал следующее:

Переопределить метод слоя addAnimation:forKey:. В нем выясните, добавляется ли анимация границ в слой. Если это так, создайте вторую явную анимацию для анимации пути вдоль границ, скопировав все свойства из анимации границ, которая будет передана методу. В коде:

- (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"];
        }
    }
}

Я получил эту идею от чтения David Rönnqvist Статья в формате View-Layer Synergy. (Этот код для iOS 8. На iOS 7 кажется, что вам нужно проверить анимацию keyPath на @"bounds", а не `@ "bounds.size".)

Вызывающий код, который вызывает изменение анимированных ограничений вида, будет выглядеть так:

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

Вопросы

В основном это работает, но у меня есть две проблемы:

  • Иногда я вызываю авария (EXC_BAD_ACCESS) в CGPathApply() при запуске этой анимации, пока предыдущая анимация все еще продолжается. Я не уверен, имеет ли это какое-либо отношение к моей конкретной реализации. Изменить: неважно. Я забыл преобразовать UIBezierPath в CGPathRef. Спасибо @antonioviero!

  • При использовании стандартной анимации UIView spring анимация границ обзора и путь слоя немного не синхронизированы. То есть анимация пути также выполняется пружинистым способом, но она точно не соответствует ограничениям вида.

  • В целом, это лучший подход? Кажется, что наличие слоя формы, путь которого зависит от его размера границ и который должен анимироваться в синхронизации с любыми изменениями границ, является тем, что должен просто работать ™, но мне тяжело. Я считаю, что должен быть лучший способ.

Другие вещи, которые я пробовал

  • Переопределите слой actionForKey: или представление actionForLayer:forKey:, чтобы вернуть пользовательский объект анимации для свойства path. Я думаю, что это было бы предпочтительным способом, но я не нашел способ получить свойства транзакции, которые должны быть установлены приложением блока анимации. Даже если вызов изнутри блока анимации, [CATransaction animationDuration] и т.д. Всегда возвращают значения по умолчанию.

Есть ли способ (а) определить, что вы сейчас находитесь в блоке анимации, и (б) получить свойства анимации (продолжительность, анимационная кривая и т.д.), которые были установлены в этом блоке?

Проект

Здесь анимация: оранжевый треугольник - это путь слоя формы. Черная контур - это рамка представления, в которой расположен слой с фигурой.

Screenshot

Посмотрите пример проекта на GitHub. (Этот проект предназначен для iOS 8 и требует запуска Xcode 6, извините.)

Update

Хосе Луис Пьедрахита указал мне на в этой статье Ника Локвуда, что предполагает следующий подход:

  • Переопределите метод просмотра actionForLayer:forKey: или слоя actionForKey: и проверьте, является ли ключ, переданный этому методу, тем, который вы хотите оживить (@"path").

  • Если это так, вызов super в одном из "нормальных" анимационных свойств слоя (например, @"bounds") косвенно скажет вам, находитесь ли вы в блоке анимации. Если представление (делегат слоя) возвращает действительный объект (а не nil или NSNull), мы.

  • Задайте параметры (продолжительность, время и т.д.) для анимации пути из анимации, возвращенной из [super actionForKey:], и верните анимацию пути.

Это действительно отлично работает под iOS 7.x. Однако в iOS 8 объект, возвращаемый из actionForLayer:forKey:, не является стандартным (и документированным) CA...Animation, а экземпляром частного класса _UIViewAdditiveAnimationAction (подкласс NSObject). Поскольку свойства этого класса не документированы, я не могу их легко использовать для создания анимации пути.

_Edit: Или это может просто работать в конце концов. Как упоминал Ник в своей статье, некоторые свойства типа backgroundColor по-прежнему возвращают стандартный CA...Animation на iOS 8. Мне придется попробовать его. `

4b9b3361

Ответ 1

Я знаю, что это старый вопрос, но я могу предоставить вам решение, похожее на подход г-на Локвудса. К сожалению, исходный код здесь быстрый, поэтому вам нужно будет преобразовать его в 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)
    }

}

Ответ 2

Я думаю, что у вас проблемы, потому что вы одновременно играете с кадром слоя и его контуром.

Я бы просто пошел с CustomView, у которого был пользовательский drawRect: он рисует то, что вам нужно, а затем просто

[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

Я думаю, что он не синхронизирован между границами и анимацией пути, потому что существует другая функция синхронизации между UIVIew spring и CABasicAnimation.

Возможно, вы можете попробовать преобразовать анимат вместо этого, он также должен преобразовать путь (непроверенный), после того, как он завершил анимацию, вы можете установить привязку.

Еще один возможный способ - сделать снимок пути, установить его как содержимое слоя, а затем анимировать границу, затем содержимое должно следовать за анимацией.

Ответ 4

Найдено решение для анимации path of CAShapeLayer, в то время как bounds анимируется:

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