Статьи

Создание 3D-анимации складывания страницы: полировка сгиба страницы

Этот мини-сериал из двух частей научит вас создавать впечатляющий эффект сворачивания страниц с помощью Core Animation. В этой статье вы узнаете шаги, необходимые для полировки созданного ранее сгиба страницы, и создадите более интерактивный опыт сворачивания с помощью жестов.


Вступление

В первой части этого урока , состоящего из двух частей , мы взяли существующее приложение для рисования от руки, которое позволяло пользователю рисовать на экране пальцем, и реализовали эффект 3D-свертывания на холсте, который создавал визуальное впечатление существа книги. сложенный вдоль его позвоночника. Мы достигли этого, используя CALayer и преобразования. Мы подключили этот эффект к жесту касания, который заставлял сворачивание происходить с приращениями, плавно анимируя между одним приращением и следующим.

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

Отправной точкой для этой части будет проект Xcode, который мы закончили в конце первого урока . Давайте продолжим!


Напомним, что каждая из левой и правой страниц была экземпляром CALayer . Они были прямоугольной формы, располагались над левой и правой половинами холста и примыкали к вертикальной линии, проходящей через центр. Мы нарисовали содержимое (т.е. эскиз от руки пользователя) левой и правой половин холста на этих двух страницах.

Несмотря на то, что CAlayer при создании начинается с прямоугольных границ (например, UIView s), одна из классных вещей, которые мы можем сделать со слоями, это обрезать их форму в соответствии с «маской», чтобы они больше не ограничивались прямоугольностью! Как определяется эта маска? CALayer есть свойство mask которое является CALayer чей альфа-канал контента описывает маску, которая будет использоваться. Если мы используем «мягкую» маску (альфа-канал имеет дробные значения), мы можем сделать слой частично прозрачным. Если мы используем «жесткую» маску (то есть с альфа-значениями, равными нулю или единице), мы можем «обрезать» слой так, чтобы он достиг четко определенной формы по нашему выбору.

Эффект маскировки слоя с мягкой против жесткой маски

Мы могли бы использовать внешнее изображение, чтобы определить нашу маску. Однако, поскольку наша маска имеет определенную форму (прямоугольник с некоторыми закругленными углами), есть лучший способ сделать это в коде. Чтобы указать форму для нашей маски, мы используем подкласс CALayer именем CAShapeLayer . CAShapeLayer — это слои, которые могут иметь любую форму, заданную векторным путем непрозрачного типа Core Graphics CGPathRef . Мы можем либо напрямую создать этот путь с помощью Core Graphics API, основанного на C, либо — что более удобно — мы можем создать объект UIBezierPath с помощью UIBezierPath Objective-C UIKit. UIBezierPath предоставляет базовый объект CGPathRef через его свойство CGPath которое может быть назначено CAShapeLayer path нашего CAShapeLayer , и этот слой формы, в свою очередь, может быть назначен маской нашего CALayer . К счастью для нас, UIBezierPath можно инициализировать многими интересными предопределенными фигурами, включая прямоугольник с закругленными углами (где мы выбираем, какой угол (углы) округлять).

Добавьте следующий код, скажем, после строки rightPage.transform = makePerspectiveTransform(); в ViewController.m viewDidAppear: метод:

01
02
03
04
05
06
07
08
09
10
11
12
// rounding corners
 
UIBezierPath *leftPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect:leftPage.bounds byRoundingCorners:UIRectCornerTopLeft|UIRectCornerBottomLeft cornerRadii:CGSizeMake(25., 25.0)];
UIBezierPath *rightPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect:rightPage.bounds byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight cornerRadii:CGSizeMake(25.0, 25.0)];
CAShapeLayer *leftPageRoundedCornersMask = [CAShapeLayer layer];
CAShapeLayer *rightPageRoundedCornersMask = [CAShapeLayer layer];
leftPageRoundedCornersMask.frame = leftPage.bounds;
rightPageRoundedCornersMask.frame = rightPage.bounds;
leftPageRoundedCornersMask.path = leftPageRoundedCornersPath.CGPath;
rightPageRoundedCornersMask.path = rightPageRoundedCornersPath.CGPath;
leftPage.mask = leftPageRoundedCornersMask;
rightPage.mask = rightPageRoundedCornersMask;

Код должен быть понятен. Путь Безье имеет форму прямоугольника того же размера, что и маскируемый слой, и имеет соответствующие углы, закругленные (верхний левый и нижний левый для левой страницы, верхний правый и нижний правый для правой страницы).

Постройте проект и запустите. Углы страницы должны быть закруглены … круто!

До и после округления

Нанести тень также легко, но есть некоторая загвоздка, когда нам нужна тень после того, как мы применили маску (как мы только что сделали). Мы столкнемся с этим со временем!

CALayer имеет shadowPath который является (как вы уже догадались) CGPathRef и определяет форму тени. У тени есть несколько свойств, которые мы можем установить: ее цвет, ее смещение (в основном, в какую сторону и как далеко она падает от слоя), ее радиус (определяющий ее степень и размытость) и ее непрозрачность.

На самом деле следует отметить, что не обязательно устанавливать shadowPath поскольку система рисования будет обрабатывать тень от составного альфа-канала слоя. Однако это менее эффективно и обычно приводит к снижению производительности, поэтому всегда рекомендуется устанавливать свойство shadowPath когда это возможно.

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

01
02
03
04
05
06
07
08
09
10
leftPage.shadowPath = [UIBezierPath bezierPathWithRect:leftPage.bounds].CGPath;
rightPage.shadowPath = [UIBezierPath bezierPathWithRect:rightPage.bounds].CGPath;
 
leftPage.shadowRadius = 100.0;
leftPage.shadowColor = [UIColor blackColor].CGColor;
leftPage.shadowOpacity = 0.9;
 
rightPage.shadowRadius = 100.0;
rightPage.shadowColor = [UIColor blackColor].CGColor;
rightPage.shadowOpacity = 0.9;

Постройте и запустите код. К сожалению, тень не будет отброшена, и результат будет выглядеть точно так же, как и раньше. Что с этим ?!

Чтобы понять, что происходит, закомментируйте первый блок кода, который мы написали в этом руководстве, но оставьте теневой код на месте. Теперь строим и запускаем. Хорошо, мы потеряли закругленные углы, но теперь наши слои отбрасывают туманную тень на вид позади них, усиливая ощущение глубины.

Что происходит, когда мы устанавливаем CALayer маски CALayer , область рендеринга слоя обрезается до области маски. Поэтому тени (которые естественным образом отбрасываются от слоя) не визуализируются и, следовательно, не появляются.

Мы не можем получить тени, если мы используем маску

Прежде чем мы попытаемся решить эту проблему, обратите внимание, что тень для правой страницы отбрасывается сверху левой страницы. Это связано с тем, что leftPage был добавлен к представлению перед rightPage , поэтому первый фактически «отстает» от последнего в порядке рисования (даже если они оба слоя одного уровня). Помимо переключения порядка, в котором два слоя были добавлены в суперслой, мы могли бы изменить свойство zPosition для слоев, чтобы явно указать порядок рисования, назначив меньшее значение с плавающей точкой слою, который мы хотели нарисовать первым. Мы были бы готовы к более сложной реализации, если бы вообще хотели отказаться от этого эффекта, но, поскольку (к счастью) он дает хороший затененный эффект нашей странице, мы довольны тем, что есть!


Чтобы решить эту проблему, мы будем использовать два слоя для представления каждой страницы, один для создания теней, а другой для отображения нарисованного содержимого в форме области. С точки зрения иерархии, мы добавим теневые слои как прямой подслой к слою фонового представления. Затем мы добавим слои содержимого к теневым слоям. Следовательно, теневые слои будут удваиваться как «контейнеры» для слоя контента. Все наши геометрические преобразования (связанные с эффектом перелистывания страниц) будут применены к теневым слоям. Поскольку слои содержимого будут отображаться относительно их контейнеров, нам не нужно будет применять к ним какие-либо преобразования.

Как только вы поняли это, тогда написание кода относительно просто. Но из-за всех изменений, которые мы делаем, модифицировать предыдущий код будет грязно, поэтому я предлагаю вам заменить весь код в viewDidAppear: следующим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
— (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.view.backgroundColor = [UIColor whiteColor];
     
    leftPageShadowLayer = [CAShapeLayer layer];
    rightPageShadowLayer = [CAShapeLayer layer];
    leftPageShadowLayer.anchorPoint = CGPointMake(1.0, 0.5);
    rightPageShadowLayer.anchorPoint = CGPointMake(0.0, 0.5);
    leftPageShadowLayer.position = CGPointMake(self.view.bounds.size.width/2, self.view.bounds.size.height/2);
    rightPageShadowLayer.position = CGPointMake(self.view.bounds.size.width/2, self.view.bounds.size.height/2);
    leftPageShadowLayer.bounds = CGRectMake(0, 0,
                                            self.view.bounds.size.width/2, self.view.bounds.size.height);
    rightPageShadowLayer.bounds = CGRectMake(0, 0, self.view.bounds.size.width/2, self.view.bounds.size.height);
     
    UIBezierPath *leftPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect:leftPageShadowLayer.bounds byRoundingCorners:UIRectCornerTopLeft|UIRectCornerBottomLeft cornerRadii:CGSizeMake(25., 25.0)];
    UIBezierPath *rightPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect:rightPageShadowLayer.bounds byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight cornerRadii:CGSizeMake(25.0, 25.0)];
     
    leftPageShadowLayer.shadowPath = leftPageRoundedCornersPath.CGPath;
    rightPageShadowLayer.shadowPath = rightPageRoundedCornersPath.CGPath;
     
    leftPageShadowLayer.shadowColor = [UIColor blackColor].CGColor;
    leftPageShadowLayer.shadowRadius = 100.0;
    leftPageShadowLayer.shadowOpacity = 0.9;
     
    rightPageShadowLayer.shadowColor = [UIColor blackColor].CGColor;
    rightPageShadowLayer.shadowRadius = 100;
    rightPageShadowLayer.shadowOpacity = 0.9;
     
     
     
    leftPage = [CALayer layer];
    rightPage = [CALayer layer];
    leftPage.frame = leftPageShadowLayer.bounds;
    rightPage.frame = rightPageShadowLayer.bounds;
    leftPage.backgroundColor = [UIColor whiteColor].CGColor;
    rightPage.backgroundColor = [UIColor whiteColor].CGColor;
    leftPage.borderColor = [UIColor darkGrayColor].CGColor;
    rightPage.borderColor = [UIColor darkGrayColor].CGColor;
    leftPage.transform = makePerspectiveTransform();
    rightPage.transform = makePerspectiveTransform();
     
    CAShapeLayer *leftPageRoundedCornersMask = [CAShapeLayer layer];
    CAShapeLayer *rightPageRoundedCornersMask = [CAShapeLayer layer];
    leftPageRoundedCornersMask.frame = leftPage.bounds;
    rightPageRoundedCornersMask.frame = rightPage.bounds;
    leftPageRoundedCornersMask.path = leftPageRoundedCornersPath.CGPath;
    rightPageRoundedCornersMask.path = rightPageRoundedCornersPath.CGPath;
    leftPage.mask = leftPageRoundedCornersMask;
    rightPage.mask = rightPageRoundedCornersMask;
     
    leftPageShadowLayer.transform = makePerspectiveTransform();
    rightPageShadowLayer.transform = makePerspectiveTransform();
    curtainView = [[UIView alloc] initWithFrame:self.view.bounds];
    curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
     
    [curtainView.layer addSublayer:leftPageShadowLayer];
    [curtainView.layer addSublayer:rightPageShadowLayer];
    [leftPageShadowLayer addSublayer:leftPage];
    [rightPageShadowLayer addSublayer:rightPage];
    UITapGestureRecognizer *foldTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fold:)];
    [self.view addGestureRecognizer:foldTap];
    UITapGestureRecognizer *unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(unfold:)];
    unfoldTap.numberOfTouchesRequired = 2;
    [self.view addGestureRecognizer:unfoldTap];
}

Вам также необходимо добавить две переменные экземпляра, соответствующие двум теневым слоям. Измените код в начале раздела @implementation образом:

1
2
3
4
5
6
7
8
9
@implementation ViewController
{
    CALayer *leftPage;
    CALayer *rightPage;
    UIView *curtainView;
     
    CAShapeLayer *leftPageShadowLayer;
    CAShapeLayer *rightPageShadowLayer;
}

Исходя из нашего предыдущего обсуждения, вы должны найти простой код для подражания.

Напомним, что ранее я упоминал, что слои, содержащие нарисованный контент, будут содержаться в качестве подслоев для слоя, генерирующего тень. Поэтому нам нужно изменить наши методы fold: и unfold: для выполнения необходимого преобразования теневых слоев.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
— (void)fold:(UITapGestureRecognizer *)gr
{
    // drawing the «incrementalImage» bitmap into our layers
    CGImageRef imgRef = ((CanvasView *)self.view).incrementalImage.CGImage;
    leftPage.contents = (__bridge id)imgRef;
    rightPage.contents = (__bridge id)imgRef;
    leftPage.contentsRect = CGRectMake(0.0, 0.0, 0.5, 1.0);
    rightPage.contentsRect = CGRectMake(0.5, 0.0, 0.5, 1.0);
     
    leftPageShadowLayer.transform = CATransform3DScale(leftPageShadowLayer.transform, 0.95, 0.95, 0.95);
    rightPageShadowLayer.transform = CATransform3DScale(rightPageShadowLayer.transform, 0.95, 0.95, 0.95);
    leftPageShadowLayer.transform = CATransform3DRotate(leftPageShadowLayer.transform, D2R(7.5), 0.0, 1.0, 0.0);
    rightPageShadowLayer.transform = CATransform3DRotate(rightPageShadowLayer.transform, D2R(-7.5), 0.0, 1.0, 0.0);
    [self.view addSubview:curtainView];
}
 
— (void)unfold:(UITapGestureRecognizer *)gr
{
    leftPageShadowLayer.transform = CATransform3DIdentity;
    rightPageShadowLayer.transform = CATransform3DIdentity;
    leftPageShadowLayer.transform = makePerspectiveTransform();
    rightPageShadowLayer.transform = makePerspectiveTransform();
    [curtainView removeFromSuperview];
}

Создайте и запустите приложение, чтобы проверить наши страницы с закругленными углами и тенью!

Обе тени и закругленные углы

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


Все выглядит хорошо, визуально говоря, но касание не совсем реалистичный жест для метафоры сворачивания книги. Если мы думаем о приложениях для iPad, таких как бумага , складывание и развертывание осуществляется жестом. Давайте реализуем это сейчас!

Реализуйте следующий метод в ViewController.m :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
— (void)foldWithPinch:(UIPinchGestureRecognizer *)p
{
    if (p.state == UIGestureRecognizerStateBegan) // …………… (A)
    {
        self.view.userInteractionEnabled = NO;
        CGImageRef imgRef = ((CanvasView *)self.view).incrementalImage.CGImage;
        leftPage.contents = (__bridge id)imgRef;
        rightPage.contents = (__bridge id)imgRef;
        leftPage.contentsRect = CGRectMake(0.0, 0.0, 0.5, 1.0);
        rightPage.contentsRect = CGRectMake(0.5, 0.0, 0.5, 1.0);
        leftPageShadowLayer.transform = CATransform3DIdentity;
        rightPageShadowLayer.transform = CATransform3DIdentity;
        leftPageShadowLayer.transform = makePerspectiveTransform();
        rightPageShadowLayer.transform = makePerspectiveTransform();
        [self.view addSubview:curtainView];
    }
    float scale = p.scale > 0.48 ?
    scale = scale < 1.0 ?
    // SOME CODE WILL GO HERE (1)
    leftPageShadowLayer.transform = CATransform3DIdentity;
    rightPageShadowLayer.transform = CATransform3DIdentity;
    leftPageShadowLayer.transform = makePerspectiveTransform();
    rightPageShadowLayer.transform = makePerspectiveTransform();
    leftPageShadowLayer.transform = CATransform3DScale(leftPageShadowLayer.transform, scale, scale, scale);
    rightPageShadowLayer.transform = CATransform3DScale(rightPageShadowLayer.transform, scale, scale, scale);
    leftPageShadowLayer.transform = CATransform3DRotate(leftPageShadowLayer.transform, (1.0 — scale) * M_PI, 0.0, 1.0, 0.0);
    rightPageShadowLayer.transform = CATransform3DRotate(rightPageShadowLayer.transform, -(1.0 — scale) * M_PI, 0.0, 1.0, 0.0);
    // SOME CODE WILL GO HERE (2)
    if (p.state == UIGestureRecognizerStateEnded) // ……………………… (C)
    {
        // SOME CODE CHANGES HERE LATER (3)
        self.view.userInteractionEnabled = YES;
        [curtainView removeFromSuperview];
    }
}

Краткое объяснение, касающееся кода, в отношении обозначений A, B и C, указанных в коде:

  1. Когда жест пинча распознается (на что указывает его свойство state принимающее значение UIGestureRecognizerStateBegan ), мы начинаем подготовку к анимации сгиба. Утверждение self.view.userInteractionEnabled = NO; гарантирует, что любые дополнительные прикосновения, которые происходят во время жеста сжатия, не будут вызывать рисование в представлении холста. Оставшийся код должен быть вам знаком. Мы просто сбрасываем преобразования слоя.
  2. Свойство scale прижима определяет отношение расстояния между пальцами по отношению к началу прижима. Я решил ограничить значение, которое мы будем использовать для вычисления масштабирования наших страниц и преобразований поворота между 0.48 . The condition scale is so that a "reverse pinch" (the user moving his fingers further apart than the start of the pinch, corresponding to p.scale > 1.0 ) has no effect. The condition p.scale > 0.48 is so that when the inter-finger distance becomes approximately half of what it was at the start of the pinch, our folding animation is completed and any further pinch has no effect. I choose 0.48 instead of 0.50 because of the way I calculate the turning angle of the layer's rotational transform. With a value of 0.48 the rotation angle will be slightly less than 90 degrees, so the book won't completely fold and hence won't become invisible. 0.48 . The condition scale is so that a "reverse pinch" (the user moving his fingers further apart than the start of the pinch, corresponding to p.scale > 1.0 ) has no effect. The condition p.scale > 0.48 is so that when the inter-finger distance becomes approximately half of what it was at the start of the pinch, our folding animation is completed and any further pinch has no effect. I choose 0.48 instead of 0.50 because of the way I calculate the turning angle of the layer's rotational transform. With a value of 0.48 the rotation angle will be slightly less than 90 degrees, so the book won't completely fold and hence won't become invisible.
  3. После того, как пользователь заканчивает пинч, мы удаляем представление, представляющее наши анимированные слои, из представления холста (как и раньше) и восстанавливаем интерактивность холста.

Добавьте код для добавления распознавателя пинча в конце viewDidAppear:

1
2
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(foldWithPinch:)];
[self.view addGestureRecognizer:pinch];

Вы можете избавиться от всего кода, связанного с двумя распознавателями касаний, из ViewController.m, потому что они нам больше не понадобятся.

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

В принципе, наше складывание-разворачивание на основе пинча работает, но вы обязательно заметите (особенно если тестируете на физическом устройстве), что анимация медленная и отстает от пинча, что ослабляет иллюзию, что пинч управляет анимацией складывания-раскладывания. Так что же происходит?


Помните неявную анимацию преобразования, которой мы наслаждались «бесплатно», когда анимация управлялась касанием? Что ж, получается, что та же неявная анимация стала помехой! Обычно, когда мы хотим управлять анимацией с помощью жеста, такого как перетаскивание вида по экрану с помощью перетаскивания (панорамирования) (или в нашем случае с нашим приложением), мы хотим отменить неявную анимацию. Давайте удостоверимся, что мы понимаем, почему, учитывая случай, когда пользователь перетаскивает вид по экрану пальцем:

  1. view находится в позиции p0 для начала (т.е. view.position = p0 где p0 = (x0, y0) и является CGPoint )
  2. Когда UIKit в следующий раз проверяет прикосновение пользователя к экрану, он перетаскивает палец в другую позицию, скажем, p1 .
  3. Включается неявная анимация, в результате чего система анимации начинает анимировать изменение положения view с p0 на p1 с «расслабленной» продолжительностью 0,25 секунды. Однако анимация только началась, и пользователь уже перетащил палец в новую позицию, p2 . Анимация должна быть отменена, а новая начинается в направлении позиции p2.
  4. …и так далее и тому подобное!
Неявная анимация мешает

Поскольку жест панорамирования пользователя (в нашем гипотетическом примере) по сути является непрерывным, нам просто нужно изменить положение представления в соответствии с жестом пользователя, чтобы сохранить иллюзию анимации ответа! Точно такой же аргумент применяется для нашей складки страницы с пинч-ситуацией здесь. Я упомянул только пример перетаскивания, потому что казалось, что проще объяснить, что происходит.

Существуют разные способы отключения неявной анимации. Мы выберем самый простой способ, который включает в себя упаковку нашего кода в блок CATransaction и [CATransaction setDisableActions:YES] метода класса [CATransaction setDisableActions:YES] . Так что же такое CATransaction ? Проще говоря, CATransaction «связывает» изменения свойств, которые необходимо анимировать, и последовательно обновляет их значения на экране, обрабатывая все аспекты синхронизации. По сути, он выполняет всю тяжелую работу, связанную с рендерингом нашей анимации для нас! Даже если мы еще не использовали явно транзакцию анимации, неявная транзакция всегда присутствует при выполнении любого кода, связанного с анимацией. Теперь нам нужно CATransaction анимацию в специальный блок CATransaction .

В pinchToFold: добавьте эти строки:

1
2
[CATransaction begin];
[CATransaction setDisableActions:YES];

На сайте комментария // SOME CODE WILL GO HERE (1) ,
добавьте строку:

1
[CATransaction commit];

Если вы создадите и запустите приложение сейчас, вы заметите, что складывание-раскладывание гораздо более плавное и гибкое!

Еще одна проблема, связанная с анимацией, которую мы должны решить, заключается в том, что наш метод unfolding: метод не оживляет восстановление книги до ее сплющенного состояния, когда жест жеста заканчивается. Вы можете жаловаться, что в нашем коде мы на самом деле не беспокоились об обратном transform в блоке «then» нашего if (p.state == UIGestureRecognizerStateEnded) . Но вы можете попробовать вставить операторы, которые сбрасывают преобразование слоя, перед оператором [canvasView removeFromSuperview]; чтобы увидеть, изменяются ли слои, изменяющие это свойство (спойлер: они не будут!).

Причина, по которой слои не будут анимировать изменение свойства, заключается в том, что любое изменение свойства, которое мы могли бы выполнить в этом блоке кода, будет объединено в той же (неявной) CATransaction что и код для удаления представления размещения слоя ( canvasView ) из экран. Удаление представления произойдет немедленно — в конце концов, это не анимированное изменение — и никакой анимации в любых подпредставлениях (или подслоях, добавленных к его слою) не произойдет.

Опять же, явный блок CATransaction приходит нам на помощь! CATransaction имеет блок завершения, который выполняется только после любых изменений свойств, которые появляются после завершения анимации.

Измените код, следуя предложению if (p.state == UIGestureRecognizerStateEnded) , чтобы оператор if (p.state == UIGestureRecognizerStateEnded) следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
if (p.state == UIGestureRecognizerStateEnded)
{
 
    [CATransaction begin];
    [CATransaction setCompletionBlock:^{
        self.view.userInteractionEnabled = YES;
        [curtainView removeFromSuperview];
    }];
    [CATransaction setAnimationDuration:0.5/scale];
    leftPageShadowLayer.transform = CATransform3DIdentity;
    rightPageShadowLayer.transform = CATransform3DIdentity;
    [CATransaction commit];
     
}

Обратите внимание, что я решил изменить продолжительность анимации обратно пропорционально scale так что чем больше степень сгиба в момент окончания жеста, тем больше времени должно занять откат анимации.

Здесь важно понять, что только после анимации изменений transform код в блоке completionBlock выполнен. Класс CATransaction имеет другие свойства, которые вы можете использовать для настройки анимации именно так, как вы этого хотите. Я предлагаю вам взглянуть на документацию для получения дополнительной информации.

Сборка и запуск. Наконец, наша анимация не только хорошо выглядит, но и правильно реагирует на взаимодействие с пользователем!


Я надеюсь, что этот урок убедил вас, что базовые слои анимации — это реалистичный выбор для достижения довольно сложных трехмерных эффектов и анимации без особых усилий. С некоторой оптимизацией, вы должны иметь возможность анимировать несколько сотен слоев на экране одновременно, если вам нужно. Отличный вариант использования Core Animation — это включение крутого 3D-перехода при переключении с одного вида на другой в вашем приложении. Я также чувствую, что Core Animation может быть жизнеспособным вариантом для создания простых словесных или карточных игр.

Несмотря на то, что наш урок состоял из двух частей, мы едва поцарапали поверхность слоев и анимации (но, надеюсь, это хорошо!). Есть и другие интересные подклассы CALayer которые у нас не было возможности взглянуть. Анимация сама по себе огромная тема. Я рекомендую посмотреть доклады WWDC на эти темы, такие как «Основная анимация на практике» (сеансы 424 и 424, WWDC 2010), «Основные принципы анимации» (сессия 421, WWDC 2011), «Производительность приложения iOS — графика и анимация» (Сессия 238, WWDC 2012) и «Оптимизация производительности 2D-графики и анимации» (Сессия 506, WWDC 2012), а затем копаться в документации. Приятного обучения и написания приложений!