Pull to refresh

Переходы при помощи CoreAnimation: анимируем появление изображения

Reading time 6 min
Views 12K
Original author: Alex Staravoitau
Недавно я наткнулся на интересный концепт банковского приложения. Интересен он не только лишь тем, что выглядит значительно удобнее мобильного приложения любого банка, но и своими невероятными анимациями. Некоторые мне так понравились, что я решил незамедлительно их где-нибудь применить. В частности, мне показалась очень интересной анимация появления на экране фотографии пользователя и иконок управления его картой.

Вот тут.



Короче говоря, вот, чего было решено добиться (в слоу-мо!).

Как можно заметить, анимация состоит из двух частей: прорисовка обрамляющей окружности и раскрытие изображения. Обратите внимание, что само изображение не изменяет своего размера во время анимации, а меняется лишь его видимая часть — то есть анимируется в данном случае маска, которая нашу картинку закрывает. Кроме того, эти две анимации не стартуют одновременно: вторая часть, та, что с раскрытием маски, ждёт, пока окружность будет прорисована наполовину, и лишь потом раскрывает нам изображение.

Сделаем несколько допущений: во-первых, чтобы не тратить время на пошаговые инструкции по созданию проекта в Xcode, предположим, что он у вас уже есть и вы более-менее знакомы с его структурой. Во-вторых, код, который будет показан ниже, не сильно зависит от того, куда вы его поместите: в UIViewController или UIView, так что у вас есть пространство для творчества относительно того, где вы будете заниматься анимациями. Главное, что вам потребуется — это два @property:

@property (nonatomic, weak) IBOutlet UIView *viewToDrawOn;
@property (nonatomic, weak) IBOutlet UIView *imageToReveal;

… где imageToReveal — это подвью (subview) viewToDrawOn. Если вы знакомы с английским, то должны были уже догадаться, что viewToDrawOn — это объект UIView, на котором мы будем рисовать, а imageToReveal — это объект UIView (или даже UIImageView), который мы будем раскрывать маской. Тут они оба объявлены с ключевым словом IBOutlet, потому что я парень ленивый и не буду вам показывать, как создавать эти объекты — я их добавил в storyboard прямо в визуальном редакторе Interface Builder. По этой же причине они оба объявлены как weak — нет никакой необходимости в «сильных» (strong) ссылках на объекты UIView, которые в любом случае будут добавлены в иерархию вышестоящих UIView, что уже гарантирует, что эти объекты не будут удалены. Если моих слов о том, кто для кого в этом тандеме является подвью, не достаточно — вот вам картинка.

Кроме того, imageToReveal я спрятал в storyboard (галочка напротив hidden). Можно было бы сделать это и в коде, но я предпочёл применить максимум возможных кастомизаций в визуальном редакторе, сделав исходники максимально читабельными.

Наконец, переходим к исходному коду. Работать мы будем с CABasicAnimation. Она (он?) позволяет нам анимировать изменение некоторых свойств у объектов типа CALayer — и это нам очень на руку, так как и обрамляющая окружность, и маска, которая ограничивает отображение нашей картинки — это объекты типа CAShapeLayer. Начнём с окружности.

Рисуем обрамляющую окружность


Для начала мы создаём объект CAShapeLayer — он будет одним из слоёв нашего UIView, у которого будет добавлено что-то вроде графа, описывающего окружность. А затем мы, как в детских раскрасках, будем раскрашивать фигуры и контуры этого графа.

//Фигура для нашего рисунка — окружность
CAShapeLayer *circle = [CAShapeLayer layer];
//Нарисуем её до самых краёв нашего UIView
CGFloat radius = self.viewToDrawOn.frame.size.width / 2.0f;
circle.position = CGPointZero;
circle.path = [UIBezierPath bezierPathWithRoundedRect:self.viewToDrawOn.bounds
                                         cornerRadius:radius].CGPath;
//Настраиваем цвета, которыми будет закрашена окружность и её контур
circle.fillColor = [UIColor clearColor].CGColor;
//Не пугайтесь — просто использовалась небольшая категория UIColor для быстрой инициализации цвета из строки в Hex, её вы тоже найдёте в исходниках
circle.strokeColor = [UIColor colorWithHex:@"ffd800"].CGColor;
circle.lineWidth = 1.0f;
[self.viewToDrawOn.layer addSublayer:circle];

Теперь переходим непосредственно к анимации! Как я и обещал, мы будем анимировать одно из свойств CAShapeLayer, и в нашем случае это будет свойство strokeEnd, которое отвечает за точку на нашем графе, до которой нужно докрасить контур указанным цветом. Также мы настроим нашу анимацию: продолжительность, количество повторений и даже то, с какой скоростью будет развиваться анимация во времени (timingFunction).

//Создаём анимацию, анимируя конечную точку прорисовки контура, которая будет изменяться от 0 до 1 (где 1 — это полная окружность)
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
drawAnimation.duration = kAnimationDuration;
drawAnimation.repeatCount = 1.0;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

[circle addAnimation:drawAnimation forKey:@"drawOutlineAnimation"];

Раскрываем изображение


В целом ничего нового: мы снова создаём окружность и анимируем изменение одного из её свойств со временем. В данном случае мы будем изменять её радиус — с одного пикселя, когда вся окружность это лишь точка на экране, до половины длины стороны UIView, что соответствует полному раскрытому изображению (предполагая, что мы собираемся показать круглую картинку или иконку, как в том самом концепте банковского приложения). Ещё одно отличие от того, что мы делали ранее, состоит в том, что мы не будем обрисовывать объект CAShapeLayer, а будем использовать его как маску для изображения. Таким образом, мы будем видеть лишь ту часть изображения, которую обрежет маска, и чем больше маска — тем больше изображения сквозь неё будет видно. Представьте, что вы подсматриваете в щель в заборе: щель — это маска, и чем она больше, тем больше интимных подробностей происходящего за забором доступно для ваших любопытных глаз!

//Начальное и конечное значения радиуса маски
CGFloat initialRadius = 1.0f;
CGFloat finalRadius = self.imageToReveal.bounds.size.width / 2.0f;

//Создаём слой, который будет содержать маску
CAShapeLayer *revealShape = [CAShapeLayer layer];
revealShape.bounds = self.imageToReveal.bounds;
//Закрашиваем черным — подойдет любой цвет, кроме прозрачного
revealShape.fillColor = [UIColor blackColor].CGColor;

//Собственно фигура, которая будем служить маской: начальная и конечная
UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(CGRectGetMidX(self.imageToReveal.bounds) - initialRadius,
                                                                             CGRectGetMidY(self.imageToReveal.bounds) - initialRadius, initialRadius * 2, initialRadius * 2)
                                                     cornerRadius:initialRadius];
UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:self.imageToReveal.bounds
                                                   cornerRadius:finalRadius];
revealShape.path = startPath.CGPath;
revealShape.position = CGPointMake(CGRectGetMidX(self.imageToReveal.bounds) - initialRadius,
                                   CGRectGetMidY(self.imageToReveal.bounds) - initialRadius);

//Итак, теперь на изображение наложена маска, и видна будет лишь та его часть, которая совпадает с маской
self.imageToReveal.layer.mask = revealShape;

//Теперь анимация. Мы анимируем свойство path — это граф, описывающий фигуру, в нашем случае окружность. Анимация осуществит плавный переход от маленькой окружности до большой
CABasicAnimation *revealAnimationPath = [CABasicAnimation animationWithKeyPath:@"path"];
revealAnimationPath.fromValue = (__bridge id)(startPath.CGPath);
revealAnimationPath.toValue = (__bridge id)(endPath.CGPath);
revealAnimationPath.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
revealAnimationPath.duration = kAnimationDuration/2.0f;
revealAnimationPath.repeatCount = 1.0f;
//Для этой анимации мы также установим начальное время, так как она должна начаться лишь когда половина обрамляющей окружности уже прорисована
revealAnimationPath.beginTime = CACurrentMediaTime() + kAnimationDuration/2.0f;
revealAnimationPath.delegate = self;
//Так как анимация стартует с задержкой, нужно удостовериться, что свойство hidden у картинки изменится только когда маска уже применена, т.е. когда начнётся анимация маски
dispatch_time_t timeToShow = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kAnimationDuration/2.0f * NSEC_PER_SEC));
dispatch_after(timeToShow, dispatch_get_main_queue(), ^{
    self.imageToReveal.hidden = NO;
});

revealShape.path = endPath.CGPath;
[revealShape addAnimation:revealAnimationPath forKey:@"revealAnimation"];

Если кто-то мучается вопросом, почему я не объединил анимации в группу CAAnimationGroup (ведь многие настройки у них схожи), то это потому, что мы применяем эти анимации к разным слоям. Насколько мне известно, группировать анимации CAAnimation можно лишь при условии, что они работают с одним слоем. Кроме того — признаю, выставлять задержку для второй анимации это, возможно, не самое элегантное решение, так что если вы можете предложить что-то получше — не стесняйтесь!

В качестве последнего замечания — я бы не рекомендовал злоупотреблять масками во множестве разных UIView, так как они значительно замедлят работу вашего приложения, конечно, при условии, что этих масок будет очень много. Наиболее рациональным решением будет использовать уже обрезанное до окружности изображение, и убирать маску по окончании анимации.

Вот и всё, у вас должно было получиться следующее:



Если получилось что-то другое — мне искренне жаль, вы можете скачать проект Xcode и выяснить, что же пошло не так. Спасибо, и до новых встреч!
Tags:
Hubs:
+9
Comments 1
Comments Comments 1

Articles