Статьи

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

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



Работа с классом UIView занимает центральное место в разработке iOS SDK. Представления имеют как визуальный аспект (то есть то, что вы видите на экране), так и обычно аспект управления (взаимодействие пользователя с помощью прикосновений и жестов). Визуальный аспект на самом деле обрабатывается классом, принадлежащим базовой платформе анимации, называемым CALayer (который, в свою очередь, реализуется через OpenGL, только здесь мы не хотим опускаться до этого уровня абстракции). Объекты слоя являются экземплярами класса CALayer или одного из его подклассов. Не углубляясь слишком глубоко в теорию (в конце концов, это должно быть практическое руководство!), Мы должны помнить о следующих моментах:

  • Каждое представление в iOS поддерживается слоем, который отвечает за его визуальный контент. Программно, это доступно как свойство слоя в представлении.
  • Существует параллельная иерархия слоев, соответствующая иерархии представления на экране. Это означает, что если (скажем) метка является подпредставлением для кнопки, то слой метки является подслоям для слоя кнопки. Строго говоря, этот параллелизм имеет место только до тех пор, пока мы не добавляем наши собственные подслои в смесь.
  • Некоторые свойства, которые мы устанавливаем для представлений (в частности, представления, связанные с внешним видом), в действительности являются свойствами нижележащего слоя. Однако слой предоставляет некоторые свойства, которые недоступны на уровне просмотра. В этом смысле слои являются более мощными, чем представления.
  • По сравнению с представлениями слои являются более «легкими» объектами. Поэтому, если для какого-то аспекта нашего приложения нам требуется визуализация без интерактивности, то слои, вероятно, являются более производительным вариантом.
  • CALayer состоит из растрового изображения. Хотя базовый класс довольно полезен, он также имеет несколько важных подклассов в Core Animation. В частности, есть CAShapeLayer который позволяет нам представлять фигуры с помощью векторных путей.

Итак, что мы можем достичь со слоями, которые мы не можем легко сделать непосредственно с видами? 3D, например, на чем мы сосредоточимся здесь. CALayer изощренные трехмерные эффекты и анимацию без необходимости опускаться до уровня OpenGL. Это цель этого урока: продемонстрировать интересный трехмерный эффект, который дает небольшой вкус того, чего мы можем достичь с помощью CALayer s.


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

Рисуемая часть приложения, которую я заимствую, здесь не очень важна, и мы вполне могли бы использовать любое изображение для демонстрации эффекта свертывания. Тем не менее, эффект создает очень хорошую визуальную метафору в контексте приложения для рисования, где действие зажимания открывает книгу с несколькими листами (содержащими наши предыдущие рисунки), которые мы можем просматривать. Эта метафора особенно заметна в приложении «Бумага». Хотя для целей данного руководства наша реализация будет проще и менее сложной, но она не слишком далека … и, конечно, вы можете взять то, что вы узнали из этого руководства, и сделать его еще лучше!


Напомним, что в системе координат iOS начало координат находится в верхнем левом углу экрана, причем ось X увеличивается вправо, а ось Y — вниз. Кадр представления описывает свой прямоугольник в системе координат своего суперпредставления. Слои могут быть запрошены для их кадра, но есть другой (предпочтительный) способ описания местоположения и размера слоя. Мы мотивируем их простым примером: представьте два слоя, A и B, в виде прямоугольных кусочков бумаги. Вы хотели бы сделать слой B подслоем A, чтобы закрепить B поверх A с помощью булавки, сохраняя прямые стороны параллельно. Штифт проходит через две точки, одну в A и одну в B. Знание местоположения этих двух точек дает нам эффективный способ описания положения B относительно A. Мы обозначим точку, которую булавка пробивает в A, » точка привязки «и точка в B» положение «. Взгляните на следующую фигуру, для которой мы будем делать математику:

Геометрия слоя

Эта фигура выглядит так, как будто многое происходит, но не волнуйтесь, мы рассмотрим ее постепенно:

  • Верхний график показывает иерархические отношения: фиолетовый слой (A, из нашего предыдущего обсуждения) сделан подслой синего слоя (B). Зелёный кружок с плюсом — это то, где A прикреплено к B. Положение (в системе координат A) задано как {32.5, 62.5}.
  • Теперь обратите ваше внимание на нижний рисунок. Опорная точка указана по-разному. Это относительно размера слоя B, так что верхний левый угол в {0.0, 0.0} и нижний правый угол в {1.0, 1.0}. Поскольку наш штифт составляет одну четвертую расстояния по ширине B и половину пути вниз, точка привязки составляет {0,25, 0,5}.
  • Зная размер B (50 x 45), мы можем теперь вычислить координату левого верхнего угла. Относительно верхнего левого угла B точка привязки составляет 0,25 x 50 = 12,5 точек в направлении x и 0,50 x 45 = 22,5 точек в направлении y. Вычтите их из координат позиции, и вы получите координаты происхождения B в системе A: {32.5 — 12.5, 62.5 — 22.5} = {20, 40}. Ясно, что кадр B — это {20, 40, 50, 45}.

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

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


Я уверен, что вы знакомы с концепцией преобразований, таких как масштабирование, перевод и вращение. В приложении «Фотографии» в iOS 6, если вы укажете два пальца, чтобы увеличить или уменьшить фотографию в альбоме, вы выполняете масштабное преобразование. Если вы делаете вращательное движение двумя пальцами, фотография поворачивается, и если вы перетаскиваете ее, сохраняя параллельные стороны, это перевод. Основная анимация и CALayer превосходят UIView , позволяя вам выполнять преобразования в 3D, а не только в 2D. Конечно, в 2013 году наши экраны iDevice по-прежнему были двумерными, поэтому в трехмерных преобразованиях используется некоторая геометрическая хитрость, чтобы обмануть наши глаза при интерпретации плоского изображения как трехмерного объекта (этот процесс ничем не отличается от изображения трехмерного объекта на чертеже линии, выполненном с помощью карандаш, правда). Чтобы иметь дело с 3-м измерением, нам нужно использовать ось z, которая, как мы представляем, проходит через экран нашего устройства и перпендикулярно ему.

Точка привязки важна, потому что точный результат того же самого примененного преобразования обычно будет отличаться в зависимости от этого. Это особенно важно — и наиболее легко понять — с помощью преобразования вращения на один и тот же угол, примененного к двум различным точкам привязки в прямоугольнике на рисунке ниже (красная точка и синяя точка). Обратите внимание, что вращение происходит в плоскости изображения (или вокруг оси z, если вы предпочитаете так думать).

Вращение относительно двух разных опорных точек

Итак, как мы реализуем эффект свертывания, который нам нужен? Хотя слои действительно крутые, вы не можете сложить их посередине! Решение — как я уверен, вы уже поняли — использовать два слоя, по одному для каждой страницы по обе стороны от сгиба. Исходя из того, что мы обсуждали ранее, давайте заранее проработаем геометрические свойства этих двух слоев:

Определение геометрии наших слоев
  • Мы выбрали точку вдоль «сгиба позвоночника», чтобы наша точка привязки для обеих наших слоев, потому что там (т.е. преобразовывают вращение) наша складка бывает. Вращение происходит вокруг вертикальной линии (т.е. оси Y) — убедитесь, что вы визуализируете это. Это нормально, вы могли бы сказать, но почему я выбрал среднюю точку позвоночника (вместо, скажем, точку внизу или вверху)? На самом деле, в данном конкретном случае, это не имеет значения, что касается вращения. Но мы также хотим сделать масштабное преобразование (делая слои немного меньше по мере их сгибания) и удерживая опорную точку в середине, чтобы книга оставалась красивой и центрированной при сворачивании. Это потому, что для масштабирования, точка совпадает с точкой остается анкерной исправленными в положении.
  • Точкой привязки для первого слоя является {1.0, 0.5} а для второго слоя — {0.0, 0.5} в соответствующих координатных пространствах. Убедитесь, что вы подтвердили это из рисунка, прежде чем продолжить!
  • Точка, лежащая ниже точки привязки в суперслое (т. Е. «Позиция»), является средней точкой, поэтому ее координаты: {width/2, height/2} . Помните, что свойство position находится в стандартных координатах, а не нормализовано.
  • Размер каждого из слоев: {width/2, height} .

Теперь мы знаем достаточно, чтобы написать код!

Создайте новый проект Xcode с шаблоном «Пустое приложение» и назовите его LayerFunTut . Сделайте это приложение для iPad и включите автоматический подсчет ссылок (ARC), но отключите параметры для базовых данных и модульных тестов. Сохрани это.

Новый проект

На открывшейся странице Target> Summary прокрутите вниз до «Поддерживаемые ориентации интерфейса» и выберите две горизонтальные ориентации.

Поддерживаемые ориентации

Прокрутите дальше вниз, пока не дойдете до «Связанные фреймворки и библиотеки», нажмите «+» и добавьте базовую платформу QuartzCore, которая требуется для Core Animation и CALayers.

Связывание платформы QuartzCore

Мы начнем с включения нашего приложения для рисования в проект. Создайте новый класс Objective C под названием CanvasView , сделав его подклассом UIView . Вставьте следующий код в CanvasView.h :

01
02
03
04
05
06
07
08
09
10
11
//
// CanvasView.h
//
 
#import <UIKit/UIKit.h>
 
@interface CanvasView : UIView
 
@property (nonatomic, strong) UIImage *incrementalImage;
 
@end

А затем в CanvasView.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
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//
// CanvasView.m
//
 
#import «CanvasView.h»
 
@implementation CanvasView
{
    UIBezierPath *path;
    CGPoint pts[5];
    uint ctr;
}
 
— (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        self.backgroundColor = [UIColor clearColor];
        [self setMultipleTouchEnabled:NO];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:6.0];
    }
    return self;
}
— (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        [self setMultipleTouchEnabled:NO];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:6.0];
    }
    return self;
}
 
— (void)drawRect:(CGRect)rect
{
    [self.incrementalImage drawInRect:rect];
    [[UIColor blueColor] setStroke];
    [path stroke];
}
— (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    ctr = 0;
    UITouch *touch = [touches anyObject];
    pts[0] = [touch locationInView:self];
}
— (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    ctr++;
    pts[ctr] = p;
    if (ctr == 4)
    {
        pts[3] = CGPointMake((pts[2].x + pts[4].x)/2.0, (pts[2].y + pts[4].y)/2.0);
        [path moveToPoint:pts[0]];
        [path addCurveToPoint:pts[3] controlPoint1:pts[1] controlPoint2:pts[2]];
        [self setNeedsDisplay];
        pts[0] = pts[3];
        pts[1] = pts[4];
        ctr = 1;
    }
}
— (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self drawBitmap];
    [self setNeedsDisplay];
    [path removeAllPoints];
    ctr = 0;
}
— (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}
— (void)drawBitmap
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    if (!self.incrementalImage)
    {
        UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds];
        [[UIColor clearColor] setFill];
        [rectpath fill];
    }
    [self.incrementalImage drawAtPoint:CGPointZero];
    [[UIColor blueColor] setStroke];
    [path stroke];
    self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}
@end

Как упоминалось ранее, это просто код из другого учебника, который я написал (с некоторыми незначительными изменениями). Не забудьте проверить это, если вы не уверены, как работает код. Для целей данного руководства важно, чтобы CanvasView позволял пользователю рисовать плавные мазки на экране. Мы объявили свойство с именем incrementalImage которое хранит растровую версию чертежа пользователя. Это изображение, которое мы будем «складывать» с помощью CALayer s.

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

Создайте новый класс Objective C с именем ViewController , сделайте его подклассом UIViewController и не проверяйте какие-либо параметры, которые отображаются.

Вставьте следующий код в ViewController.m

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
//
// ViewController.m
//
 
#import «ViewController.h»
#import «CanvasView.h»
#import «QuartzCore/QuartzCore.h»
 
#define D2R(x) (x * (M_PI/180.0)) // macro to convert degrees to radians
 
@interface ViewController ()
 
@end
 
@implementation ViewController
{
    CALayer *leftPage;
    CALayer *rightPage;
    UIView *curtainView;
}
 
 
— (void)loadView
{
    self.view = [[CanvasView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
}
 
— (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];
}
— (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.view.backgroundColor = [UIColor whiteColor];
     
    CGSize size = self.view.bounds.size;
    leftPage = [CALayer layer];
    rightPage = [CALayer layer];
    leftPage.anchorPoint = (CGPoint){1.0, 0.5};
    rightPage.anchorPoint = (CGPoint){0.0, 0.5};
    leftPage.position = (CGPoint){size.width/2.0, size.height/2.0};
    rightPage.position = (CGPoint){size.width/2.0, size.height/2.0};
    leftPage.bounds = (CGRect){0, 0, size.width/2.0, size.height};
    rightPage.bounds = (CGRect){0, 0, size.width/2.0, size.height};
    leftPage.backgroundColor = [UIColor whiteColor].CGColor;
    rightPage.backgroundColor = [UIColor whiteColor].CGColor;
    leftPage.borderWidth = 2.0;
    rightPage.borderWidth = 2.0;
    leftPage.borderColor = [UIColor darkGrayColor].CGColor;
    rightPage.borderColor = [UIColor darkGrayColor].CGColor;
     
    //leftPage.transform = makePerspectiveTransform();
    //rightPage.transform = makePerspectiveTransform();
    curtainView = [[UIView alloc] initWithFrame:self.view.bounds];
    curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
     
    [curtainView.layer addSublayer:leftPage];
    [curtainView.layer 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];
}
 
— (void)fold:(UITapGestureRecognizer *)gr
{
// drawing the &quot;incrementalImage&quot;
    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);
     
    leftPage.transform = CATransform3DScale(leftPage.transform, 0.95, 0.95, 0.95);
    rightPage.transform = CATransform3DScale(rightPage.transform, 0.95, 0.95, 0.95);
    leftPage.transform = CATransform3DRotate(leftPage.transform, D2R(7.5), 0.0, 1.0, 0.0);
    rightPage.transform = CATransform3DRotate(rightPage.transform, D2R(-7.5), 0.0, 1.0, 0.0);
     
    [self.view addSubview:curtainView];
}
 
— (void)unfold:(UITapGestureRecognizer *)gr
{
    leftPage.transform = CATransform3DIdentity;
    rightPage.transform = CATransform3DIdentity;
    // leftPage.transform = makePerspectiveTransform();
    // rightPage.transform = makePerspectiveTransform();
    [curtainView removeFromSuperview];
}
 
// UNCOMMENT LATER:
/*
  
 CATransform3D makePerspectiveTransform()
 {
     CATransform3D transform = CATransform3DIdentity;
     transform.m34 = 1.0 / -2000;
     return transform;
 }
 
*/
 
@end

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

Давайте кратко обсудим этот код:

  • Мы решили переопределить -viewDidAppear: метод вместо -viewDidLoad (к которому вы, возможно, привыкли), потому что, когда вызывается последний метод, границы представления по-прежнему для портретного режима, но наше приложение работает в ландшафтном режиме. Ко времени, когда вызывается viewDidAppear: границы были установлены правильно, и поэтому мы поместили наш код туда (мы временно добавили толстые границы, чтобы мы могли разглядеть левый и правый слои при применении к ним преобразований).
  • Мы добавили распознаватель жестов, который регистрирует касание, и для каждого касания это приводит к тому, что страницы становятся немного меньше (95% от их предыдущего размера) и заставляет их поворачиваться на 7,5 градусов. Знаки разные, потому что одна из страниц поворачивается по часовой стрелке, а другая — против часовой стрелки. Мы должны были бы пойти в математику, чтобы увидеть, какой знак соответствует какому направлению, но, поскольку есть только два варианта, проще просто написать код и проверить! Кстати, функции преобразования принимают углы в радианах, поэтому мы используем макрос D2R() для преобразования из радиан в градусы. Одним из важных наблюдений является то, что функции, которые принимают преобразование в своем аргументе (например, CATransform3DScale и CATransform3DRotate ), «объединяют» одно преобразование с другим (текущее значение свойства преобразования слоя). Другие функции, такие как CATransform3DMakeRotation , CATransform3DMakeScale , CATransform3DIdentity просто CATransform3DIdentity соответствующую матрицу преобразования. CATransform3DIdentity — это «преобразование идентичности», которое имеет слой при его создании. Это аналогично числу «1» в умножении в том, что применение преобразования идентичности к слою оставляет его преобразование неизменным, так же как умножение числа на единицу.
  • Что касается чертежа, мы устанавливаем свойство содержимого наших слоев в качестве изображения. Очень важно отметить, что мы установили прямоугольник содержимого (нормализованный между 0 и 1 по каждому измерению) так, чтобы на каждой странице отображалась только половина соответствующего ему изображения. Эта система нормализованных координат так же, как тот, который мы обсуждали ранее, когда речь идет о точке привязки, так что вы должны быть в состоянии выработать значения, которые мы использовали для каждой части изображения.
  • Объект curtainView просто действует как контейнер для слоев страницы (точнее, они сделаны подуровнями для нижележащего слоя curtainView). Помните, что мы уже вычислили расположение и геометрию слоев, и это относится к слою curtainView. Нажатие один раз добавляет этот вид поверх нашего холста и применяет преобразование к слою. Двойное касание удаляет его, чтобы снова открыть холст, а также возвращает преобразование слоев в преобразование идентичности.
  • Обратите внимание на использование CGImage здесь — а также CGColor ранее — вместо UIImage и UIColor . Это потому, что CALayer работает на уровне ниже UIKit и работает с «непрозрачными» типами данных (грубо говоря, не спрашивайте об их базовой реализации!), Которые определены в платформе Core Graphics. Классы Objective-C, такие как UIColor и UIImage могут рассматриваться как объектно-ориентированные оболочки вокруг их более примитивных версий CG. Для удобства многие объекты UIKit представляют свой базовый тип CG как свойство.

В файле AppDelegate.m замените весь код следующим (мы только добавили заголовочный файл ViewController и сделали экземпляр ViewController корневым контроллером представления):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
//
// AppDelegate.m
//
 
#import «AppDelegate.h»
#import «ViewController.h»
 
@implementation AppDelegate
 
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[ViewController alloc] init];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}
 
@end

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

Плоский

Не совсем тот эффект, к которому мы стремимся! В чем дело?


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

Давайте сначала немного поговорим о преобразованиях. Что они на самом деле? Говоря математически, вы должны знать, что если мы представляем точки в нашей форме в виде математических векторов, то геометрические преобразования, такие как масштаб, вращение и перемещение, представляются в виде матричных преобразований. Это означает, что если мы возьмем матрицу, представляющую преобразование, и умножим на вектор, представляющий точку в нашей фигуре, то результат умножения (также вектор) будет отображать, где эта точка заканчивается после преобразования. Мы не можем сказать больше здесь, не углубляясь в теорию (о которой действительно стоит узнать, если вы еще не знакомы с ней — особенно, если вы намереваетесь включить крутые 3D-эффекты в свои приложения!).

Как насчет кода? Ранее мы устанавливали геометрию слоя, устанавливая его anchorPoint , position и bounds . На экране мы видим геометрию слоя после того, как он был преобразован с помощью свойства transform . Обратите внимание на вызовы функций, которые выглядят как layer.transform = // .. Вот где мы устанавливаем преобразование, которое внутренне является просто struct представляющей матрицу 4 × 4 значений с плавающей запятой. Также обратите внимание, что функции CATransform3DScale и CATransform3DRotate принимают текущее преобразование слоя в качестве параметра. Это потому, что мы можем составить несколько преобразований вместе (что означает, что мы умножаем их матрицы вместе), в результате получается, что вы выполнили эти преобразования одно за другим. Обратите внимание, что мы говорим только об окончательном результате преобразования, а не о том, как Core Animation анимирует слой!

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

Раскомментируйте два закомментированных раздела в файле CATransform3D makePerspectiveTransform() функция CATransform3D makePerspectiveTransform() и строки leftPage.transform = makePerspectiveTransform(); rightPage.transform = makePerspectiveTransform(); и leftPage.transform = makePerspectiveTransform(); rightPage.transform = makePerspectiveTransform(); сборку снова. На этот раз трехмерный эффект выглядит более правдоподобным ,

Сложить с точки зрения

Также обратите внимание, что когда мы меняем свойство transform у CALayer , сделка сопровождается «бесплатной» анимацией. Это то, что мы хотим здесь — в отличие от слоя, претерпевающего резкую трансформацию — но иногда это не так.

Конечно, перспектива зашла так далеко, когда наш пример станет более сложным, мы тоже будем использовать тени! Мы также можем захотеть закруглить углы нашей «книги», и маскировка наших слоев страниц с помощью CAShapeLayer может помочь в этом. Кроме того, мы хотели бы использовать жест щипка для управления складыванием / разворачиванием, чтобы оно было более интерактивным. Все это будет рассмотрено во второй части этого учебного мини-сериала.

Я рекомендую вам поэкспериментировать с кодом, ссылаясь на документацию по API, и попытаться реализовать желаемый эффект самостоятельно (возможно, вы даже сделаете это лучше!).

Удачи с учебником, и спасибо за чтение!