Анимировать путь 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. Мне придется попробовать его. `
Ответы
Ответ 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