iOS-Core-Animation-Advanced-Techniques(二)

视觉效果和变换git

(四)视觉效果
算法

嗯,园和椭圆还不错,但若是是带圆角的矩形呢?数组

咱们如今能作到那样了么?app

史蒂芬·乔布斯框架

我 们在第三章『图层几何学』中讨论了图层的frame,第二章『寄宿图』则讨论了图层的寄宿图。可是图层不只仅能够是图片或是颜色的容器;还有一系列内建的 特性使得创造美丽优雅的使人深入的界面元素成为可能。在这一章,咱们将会探索一些可以经过使用CALayer属性实现的视觉效果。ide

圆角函数

圆角矩形是iOS的一个标志性审美特性。这在iOS的每个地方都获得了体现,不管是主屏幕图标,仍是警告弹框,甚至是文本框。按照这流行程度,你可能会认为必定有不借助Photoshop就能轻易建立圆角举行的方法。恭喜你,猜对了。布局

CALayer 有一个叫作conrnerRadius的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),可是你能够把它设置成任意值。默认情 况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,若是把masksToBounds设置成YES的话,图层里面的全部东西都会被截取。性能

我 们能够经过一个简单的项目来演示这个效果。在Interface Builder中,咱们放置一些视图,他们有一些子视图。并且这些子视图有一些超出了边界(如图4.1)。你可能没法看到他们超出了边界,由于在编辑界面 的时候,超出的部分老是被Interface Builder裁切掉了。不过,你相信我就行了 :)学习

4.1.jpg

图4.1 两个白色的大视图,他们都包含了小一些的红色视图。

然 后在代码中,咱们设置角的半径为20个点,并裁剪掉第一个视图的超出部分(见清单4.1)。技术上来讲,这些属性均可以在Interface Builder的探测板中分别经过『用户定义运行时属性』和勾选『裁剪子视图』(Clip Subviews)选择框来直接设置属性的值。不过,在这个示例中,代码可以表示得更清楚。图4.2是运行代码的结果

清单4.1 设置cornerRadius和masksToBounds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{???
   [ super  viewDidLoad];
   //set the corner radius on our layers
   self.layerView1.layer.cornerRadius = 20.0f;
   self.layerView2.layer.cornerRadius = 20.0f;
   //enable clipping on the second layer
   self.layerView2.layer.masksToBounds = YES;
}
@end

4.2.png

右图中,红色的子视图沿角半径被裁剪了

如你所见,右边的子视图沿边界被裁剪了。

单独控制每一个层的圆角曲率也不是不可能的。若是想建立有些圆角有些直角的图层或视图时,你可能须要一些不一样的方法。好比使用一个图层蒙板(本章稍后会讲到)或者是CAShapeLayer(见第六章『专用图层』)。

图层边框

CALayer另外两个很是有用属性就是borderWidth和borderColor。两者共同定义了图层边的绘制样式。这条线(也被称做stroke)沿着图层的bounds绘制,同时也包含图层的角。

borderWidth是以点为单位的定义边框粗细的浮点数,默认为0.borderColor定义了边框的颜色,默认为黑色。

borderColor 是CGColorRef类型,而不是UIColor,因此它不是Cocoa的内置对象。不过呢,你确定也清楚图层引用了borderColor,虽然属性 声明并不能证实这一点。CGColorRef在引用/释放时候的行为表现得与NSObject极其类似。可是Objective-C语法并不支持这一作 法,因此CGColorRef属性即使是强引用也只能经过assign关键字来声明。

边框是绘制在图层边界里面的,并且在全部子内容以前,也在子图层以前。若是咱们在以前的示例中(清单4.2)加入图层的边框,你就能看到究竟是怎么一回事了(如图4.3).

清单4.2 加上边框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@implementation ViewController
- (void)viewDidLoad
{
   [ super  viewDidLoad];
   //set the corner radius on our layers
   self.layerView1.layer.cornerRadius = 20.0f;
   self.layerView2.layer.cornerRadius = 20.0f;
   //add a border to our layers
   self.layerView1.layer.borderWidth = 5.0f;
   self.layerView2.layer.borderWidth = 5.0f;
   //enable clipping on the second layer
   self.layerView2.layer.masksToBounds = YES;
}
@end

4.3.png

图4.3 给图层增长一个边框

仔细观察会发现边框并不会把寄宿图或子图层的形状计算进来,若是图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明蒙板,边框仍然会沿着图层的边界绘制出来(如图4.4).

4.4.png

图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容

阴影

iOS的另外一个常见特性呢,就是阴影。阴影每每能够达到图层深度暗示的效果。也可以用来强调正在显示的图层和优先级(好比说一个在其余视图以前的弹出框),不过有时候他们只是单纯的装饰目的。

给 shadowOpacity属性一个大于默认值(也就是0)的值,阴影就能够显示在任意图层之下。shadowOpacity是一个必须在0.0(不可 见)和1.0(彻底不透明)之间的浮点数。若是设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可使用 CALayer的另外三个属性:shadowColor,shadowOffset和shadowRadius。

显而易见,shadowColor属性控制着阴影的颜色,和borderColor和backgroundColor同样,它的类型也是CGColorRef。阴影默认是黑色,大多数时候你须要的阴影也是黑色的(其余颜色的阴影看起来是否是有一点点奇怪。。)。

shadowOffset属性控制着阴影的方向和距离。它是一个CGSize的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。shadowOffset的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。

为 什么要默认向上的阴影呢?尽管Core Animation是从图层套装演变而来(能够认为是为iOS建立的私有动画框架),可是呢,它倒是在Mac OS上面世的,前面有提到,两者的Y轴是颠倒的。这就致使了默认的3个点位移的阴影是向上的。在Mac上,shadowOffset的默认值是阴影向下 的,这样你就能理解为何iOS上的阴影方向是向上的了(如图4.5).

4.5.png

图4.5 在iOS(左)和Mac OS(右)上shadowOffset的表现。

苹果更倾向于用户界面的阴影应该是垂直向下的,因此在iOS把阴影宽度设为0,而后高度设为一个正值不失为一个作法。

shadowRadius属性控制着阴影的模糊度,当它的值是0的时候,阴影就和视图同样有一个很是肯定的边界线。当值愈来愈大的时候,边界线看上去就会愈来愈模糊和天然。苹果自家的应用设计更偏向于天然的阴影,因此一个非零值再合适不过了。

一般来说,若是你想让视图或控件很是醒目独立于背景以外(好比弹出框遮罩层),你就应该给shadowRadius设置一个稍大的值。阴影越模糊,图层的深度看上去就会更明显(如图4.6).

4.6.png

图4.6 大一些的阴影位移和角半径会增长图层的深度即视感

阴影裁剪

和图层边框不一样,图层的阴影继承自内容的外形,而不是根据边界和角半径来肯定。为了计算出阴影的形状,Core Animation会将寄宿图(包括子视图,若是有的话)考虑在内,而后经过这些来完美搭配图层形状从而建立一个阴影(见图4.7)。

4.7.png

图4.7 阴影是根据寄宿图的轮廓来肯定的

当阴影和裁剪扯上关系的时候就有一个头疼的限制:阴影一般就是在Layer的边界以外,若是你开启了masksToBounds属性,全部从图层中突出来的内容都会被才剪掉。若是咱们在咱们以前的边框示例项目中增长图层的阴影属性时,你就会发现问题所在(见图4.8).

4.8.png

图4.8 maskToBounds属性裁剪掉了阴影和内容

从技术角度来讲,这个结果是能够是能够理解的,但确实又不是咱们想要的效果。若是你想沿着内容裁切,你须要用到两个图层:一个只画阴影的空的外图层,和一个用masksToBounds裁剪内容的内图层。

若是咱们把以前项目的右边用单独的视图把裁剪的视图包起来,咱们就能够解决这个问题(如图4.9).

409.jpg

图4.9 右边,用额外的阴影转换视图包裹被裁剪的视图

咱们只把阴影用在最外层的视图上,内层视图进行裁剪。清单4.3是代码实现,图4.10是运行结果。

清单4.3 用一个额外的视图来解决阴影裁切的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@property (nonatomic, weak) IBOutlet UIView *shadowView;
@end
@implementation ViewController
?
- (void)viewDidLoad
{
   [ super  viewDidLoad];
   //set the corner radius on our layers
   self.layerView1.layer.cornerRadius = 20.0f;
   self.layerView2.layer.cornerRadius = 20.0f;
   //add a border to our layers
   self.layerView1.layer.borderWidth = 5.0f;
   self.layerView2.layer.borderWidth = 5.0f;
   //add a shadow to layerView1
   self.layerView1.layer.shadowOpacity = 0.5f;
   self.layerView1.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
   self.layerView1.layer.shadowRadius = 5.0f;
   //add same shadow to shadowView (not layerView2)
   self.shadowView.layer.shadowOpacity = 0.5f;
   self.shadowView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
   self.shadowView.layer.shadowRadius = 5.0f;
   //enable clipping on the second layer
   self.layerView2.layer.masksToBounds = YES;
}
@end

4.10.jpg

图4.10 右边视图,不受裁切阴影的阴影视图。

shadowPath属性

咱们已经知道图层阴影并不老是方的,而是从图层内容的形状继承而来。这看上去不错,可是实时计算阴影也是一个很是消耗资源的,尤为是图层有多个子图层,每一个图层还有一个有透明效果的寄宿图的时候。

如 果你事先知道你的阴影形状会是什么样子的,你能够经过指定一个shadowPath来提升性能。shadowPath是一个CGPathRef类型(一个 指向CGPath的指针)。CGPath是一个Core Graphics对象,用来指定任意的一个矢量图形。咱们能够经过这个属性单独于图层形状以外指定阴影的形状。

图4.11 展现了同一寄宿图的不一样阴影设定。如你所见,咱们使用的图形很简单,可是它的阴影能够是你想要的任何形状。清单4.4是代码实现。

4.10.jpg

图4.11 用shadowPath指定任意阴影形状

清单4.4 建立简单的阴影形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
   [ super  viewDidLoad];
   //enable layer shadows
   self.layerView1.layer.shadowOpacity = 0.5f;
   self.layerView2.layer.shadowOpacity = 0.5f;
   //create a square shadow
   CGMutablePathRef squarePath = CGPathCreateMutable();
   CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
   self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath);
   ? //create a circular shadow
   CGMutablePathRef circlePath = CGPathCreateMutable();
   CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
   self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath);
}
@end

若是是一个举行或是圆,用CGPath会至关简单明了。可是若是是更加复杂一点的图形,UIBezierPath类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。

图层蒙板

通 过masksToBounds属性,咱们能够沿边界裁剪图形;经过cornerRadius属性,咱们还能够设定一个圆角。可是有时候你但愿展示的内容不 是在一个矩形或圆角矩形。好比,你想展现一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。

使用一个32位有alpha通道的png图片一般是建立一个无矩形视图最方便的方法,你能够给它指定一个透明蒙板来实现。可是这个方法不能让你以编码的方式动态地生成蒙板,也不能让子图层或子视图裁剪成一样的形状。

CALayer 有一个属性叫作mask能够解决这个问题。这个属性自己就是个CALayer类型,有和其余图层同样的绘制和布局属性。它相似于一个子图层,相对于父图层 (即拥有该属性的图层)布局,可是它却不是一个普通的子图层。不一样于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。

mask图层的Color属性是可有可无的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其余的则会被抛弃。(如图4.12)

若是mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此之外的一切都会被隐藏起来。

4.12.png

图4.12 把图片和蒙板图层做用在一块儿的效果

我 们将代码演示一下这个过程,建立一个简单的项目,经过图层的mask属性来做用于图片之上。为了简便一些,咱们用Interface Builder来建立一个包含UIImageView的图片图层。这样咱们就只要代码实现蒙板图层了。清单4.5是最终的代码,图4.13是运行后的结 果。

清单4.5 应用蒙板图层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
- (void)viewDidLoad
{
   [ super  viewDidLoad];
   //create mask layer
   CALayer *maskLayer = [CALayer layer];
   maskLayer.frame = self.layerView.bounds;
   UIImage *maskImage = [UIImage imageNamed:@ "Cone.png" ];
   maskLayer.contents = (__bridge id)maskImage.CGImage;
   //apply mask to image layer?
   self.imageView.layer.mask = maskLayer;
}
@end

4.13.png

图4.13 使用了mask以后的UIImageView

CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的均可以做为mask属性,这意味着你的蒙板能够经过代码甚至是动画实时生成。

拉伸过滤

最后咱们再来谈谈minificationFilter和magnificationFilter属性。总得来说,当咱们视图显示一个图片的时候,都应该正确地显示这个图片(意即:以正确的比例和正确的1:1像素显示在屏幕上)。缘由以下:

  • 可以显示最好的画质,像素既没有被压缩也没有被拉伸。

  • 能更好的使用内存,由于这就是全部你要存储的东西。

  • 最好的性能表现,CPU不须要为此额外的计算。

不过有时候,显示一个非真实大小的图片确实是咱们须要的效果。好比说一个头像或是图片的缩略图,再好比说一个能够被拖拽和伸缩的大图。这些状况下,为同一图片的不一样大小存储不一样的图片显得又不切实际。

当图片须要显示不一样的大小的时候,有一种叫作拉伸过滤的算法就起到做用了。它做用于原图的像素上并根据须要生成新的像素显示在屏幕上。

事实上,重绘图片大小也没有一个统一的通用算法。这取决于须要拉伸的内容,放大或是缩小的需求等这些因素。CALayer为此提供了三种拉伸过滤方法,他们是:

  • kCAFilterLinear

  • kCAFilterNearest

  • kCAFilterTrilinear

minification(缩 小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法,它在大多数状况下都 表现良好。双线性滤波算法经过对多个像素取样最终生成新的值,获得一个平滑的表现不错的拉伸。可是当放大倍数比较大的时候图片就模糊不清了。

kCAFilterTrilinear 和kCAFilterLinear很是类似,大部分状况下两者都看不出来有什么差异。可是,较双线性滤波算法而言,三线性滤波算法存储了多个大小状况下的 图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而获得最后的结果。

这个方法的好处在于算法可以从一系列已经接近于最终大小的图片中获得想要的结果,也就是说不要对不少像素同步取样。这不只提升了性能,也避免了小几率因舍入错误引发的取样失灵的问题

103.jpg

图4.14 对于大图来讲,双线性滤波和三线性滤波表现得更出色

kCAFilterNearest是一种比较武断的方法。从名字不难看出,这个算法(也叫最近过滤)就是取最近的单像素点而无论其余的颜色。这样作很是快,也不会使图片模糊。可是,最明显的效果就是,会使得压缩图片更糟,图片放大以后也显得块状或是马赛克严重。

102.jpg

图4.15 对于没有斜线的小图来讲,最近过滤算法要好不少

总 的来讲,对于比较小的图或者是差别特别明显,极少斜线的大图,最近过滤算法会保留这种差别明显的特质以呈现更好的结果。可是对于大多数的图尤为是有不少斜 线或是曲线轮廓的图片来讲,最近过滤算法会致使更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差别。

让咱们来实验一下。咱们对第三章的时钟项目改动一下,用LCD风格的数字方式显示。咱们用简单的像素字体(一种用像素构成字符的字体,而非矢量图形)创造数字显示方式,用图片存储起来,并且用第二章介绍过的拼合技术来显示(如图4.16)。

101.jpg

图4.16 一个简单的运用拼合技术显示的LCD数字风格的像素字体

我 们在Interface Builder中放置了六个视图,小时、分钟、秒钟各两个,图4.17显示了这六个视图是如何在Interface Builder中放置的。若是每一个都用一个淡出的outlets对象就会显得太多了,因此咱们就用了一个IBOutletCollection对象把他们 和控制器联系起来,这样咱们就能够以数组的方式访问视图了。清单4.6是代码实现。

清单4.6 显示一个LCD风格的时钟

1
2
3
4
5
6
7
8
9
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
@interface ViewController ()
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews;
@property (nonatomic, weak) NSTimer *timer;
??
@end
@implementation ViewController
- (void)viewDidLoad
{
   [ super  viewDidLoad];  //get spritesheet image
   UIImage *digits = [UIImage imageNamed:@ "Digits.png" ];
   //set up digit views
   for  (UIView *view  in  self.digitViews) {
     //set contents
     view.layer.contents = (__bridge id)digits.CGImage;
     view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0);
     view.layer.contentsGravity = kCAGravityResizeAspect;
   }
   //start timer
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
   //set initial clock time
   [self tick];
}
- (void)setDigit:(NSInteger)digit forView:(UIView *)view
{
   //adjust contentsRect to select correct digit
   view.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0);
}
- (void)tick
{
   //convert time to hours, minutes and seconds
   NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar];
   NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
   ?
   NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
   //set hours
   [self setDigit:components.hour / 10 forView:self.digitViews[0]];
   [self setDigit:components.hour % 10 forView:self.digitViews[1]];
   //set minutes
   [self setDigit:components.minute / 10 forView:self.digitViews[2]];
   [self setDigit:components.minute % 10 forView:self.digitViews[3]];
   //set seconds
   [self setDigit:components.second / 10 forView:self.digitViews[4]];
   [self setDigit:components.second % 10 forView:self.digitViews[5]];
}
@end

如图4.18,这样作的确起了效果,可是图片看起来模糊了。看起来默认的kCAFilterLinear选项让咱们失望了。

4.18.png

图4.18 一个模糊的时钟,由默认的kCAFilterLinear引发

为了能像图4.19中那样,咱们须要在for循环中加入以下代码:

1
view.layer.magnificationFilter = kCAFilterNearest;

4.19.png

图4.19 设置了最近过滤以后的清晰显示

组透明

UIView有一个叫作alpha的属性来肯定视图的透明度。CALayer有一个等同的属性叫作opacity,这两个属性都是影响子层级的。也就是说,若是你给一个图层设置了opacity属性,那它的子图层都会受此影响。

iOS 常见的作法是把一个空间的alpha值设置为0.5(50%)以使其看上去呈现为不可用状态。对于独立的视图来讲还不错,可是当一个控件有子视图的时候就 有点奇怪了,图4.20展现了一个内嵌了UILabel的自定义UIButton;左边是一个不透明的按钮,右边是50%透明度的相同按钮。咱们能够注意 到,里面的标签的轮廓跟按钮的背景很不搭调。

4.20.png

图4.20 右边的渐隐按钮中,里面的标签清晰可见

这 是由透明度的混合叠加形成的,当你显示一个50%透明度的图层时,图层的每一个像素都会通常显示本身的颜色,另外一半显示图层下面的颜色。这是正常的透明度的 表现。可是若是图层包含一个一样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层自己的颜色,另外的25%则来自背景色。

在咱们的示例中,按钮和表情都是白色背景。虽然他们都死50%的可见度,可是合起来的可见度是75%,因此标签所在的区域看上去就没有周围的部分那么透明。因此看上去子视图就高粱了,使得这个显示效果都糟透了。

理 想情况下,当你设置了一个图层的透明度,你但愿它包含的整个图层树像一个总体同样的透明效果。你能够经过设置Info.plist文件中的 UIViewGroupOpacity为YES来达到这个效果,可是这个设置会影响到这个应用,整个app可能会受到不良影响。若是 UIViewGroupOpacity并未设置,iOS 6和之前的版本会默认为NO(也许之后的版本会有一些改变)。

另外一个方法就是,你能够设置CALayer的一个叫作shouldRasterize属性(见清单4.7)来实现组透明的效果,若是它被设置为YES,在应用透明度以前,图层及其子图层都会被整合成一个总体的图片,这样就没有透明度混合的问题了(如图4.21)。

为 了启用shouldRasterize属性,咱们设置了图层的rasterizationScale属性。默认状况下,全部图层拉伸都是1.0, 因此若是你使用了shouldRasterize属性,你就要确保你设置了rasterizationScale属性去匹配屏幕,以防止出现Retina 屏幕像素化的问题。

当shouldRasterize和UIViewGroupOpacity一块儿的时候,性能问题就出现了(咱们在第12章『速度』和第15章『图层性能』将作出介绍),可是性能碰撞都本地化了(译者注:这句话须要再翻译)。

清单4.7 使用shouldRasterize属性解决组透明问题

1
2
3
4
5
6
7
8
9
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
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (UIButton *)customButton
{
   //create button
   CGRect frame = CGRectMake(0, 0, 150, 50);
   UIButton *button = [[UIButton alloc] initWithFrame:frame];
   button.backgroundColor = [UIColor whiteColor];
   button.layer.cornerRadius = 10;
   //add label
   frame = CGRectMake(20, 10, 110, 30);
   UILabel *label = [[UILabel alloc] initWithFrame:frame];
   label.text = @ "Hello World" ;
   label.textAlignment = NSTextAlignmentCenter;
   [button addSubview:label];
   return  button;
}
- (void)viewDidLoad
{
   [ super  viewDidLoad];
   //create opaque button
   UIButton *button1 = [self customButton];
   button1.center = CGPointMake(50, 150);
   [self.containerView addSubview:button1];
   //create translucent button
   UIButton *button2 = [self customButton];
   ?
   button2.center = CGPointMake(250, 150);
   button2.alpha = 0.5;
   [self.containerView addSubview:button2];
   //enable rasterization for the translucent button
   button2.layer.shouldRasterize = YES;
   button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}
@end

4.21.png

图4.21 修正后的图

总结

这一章介绍了一些能够经过代码应用到图层上的视觉效果,好比圆角,阴影和蒙板。咱们也了解了拉伸过滤器和组透明。

在第五章,『变换』中,咱们将会研究图层变化和3D转换。
--------------------------------------------------------------------------------------------------------------------------------------------------------

(五)变换

很不幸,没人能告诉你母体是什么,你只能本身体会 -- 骇客帝国

在 第四章“可视效果”中,咱们研究了一些加强图层和它的内容显示效果的一些技术,在这一章中,咱们将要研究能够用来对图层旋转,摆放或者扭曲的 CGAffineTransform,以及能够将扁平物体转换成三维空间对象的CATransform3D(而不是仅仅对圆角矩形添加下沉阴影)。

仿射变换

在 第三章“图层几何学”中,咱们使用了UIView的transform属性旋转了钟的指针,但并无解释背后运做的原理,实际上UIView的 transform属性是一个CGAffineTransform类型,用于在二维空间作旋转,缩放和平移。CGAffineTransform是一个可 以和二维空间向量(例如CGPoint)作乘法的3X2的矩阵(见图5.1)。

5.1.jpeg

图5.1 用矩阵表示的CGAffineTransform和CGPoint

用 CGPoint的每一列和CGAffineTransform矩阵的每一行对应元素相乘再求和,就造成了一个新的CGPoint类型的结果。要解释一下图 中显示的灰色元素,为了能让矩阵作乘法,左边矩阵的列数必定要和右边矩阵的行数个数相同,因此要给矩阵填充一些标志值,使得既可让矩阵作乘法,又不改变 运算结果,而且不必存储这些添加的值,由于它们的值不会发生变化,可是要用来作运算。

所以,一般会用3×3(而不是2×3)的矩阵来作二维变换,你可能会见到3行2列格式的矩阵,这是所谓的以列为主的格式,图5.1所示的是以行为主的格式,只要能保持一致,用哪一种格式都无所谓。

当 对图层应用变换矩阵,图层矩形内的每个点都被相应地作变换,从而造成一个新的四边形的形状。CGAffineTransform中的“仿射”的意思是无 论变换矩阵用什么值,图层中平行的两条线在变换以后任然保持平行,CGAffineTransform能够作出任意符合上述标注的变换,图5.2显示了一 些仿射的和非仿射的变换:

5.2.jpeg

图5.2 仿射和非仿射变换

建立一个CGAffineTransform

对 矩阵数学作一个全面的阐述就超出本书的讨论范围了,不过若是你对矩阵彻底不熟悉的话,矩阵变换可能会使你感到畏惧。幸运的是,Core Graphics提供了一系列函数,对彻底没有数学基础的开发者也可以简单地作一些变换。以下几个函数都建立了一个CGAffineTransform实 例:

1
2
3
CGAffineTransformMakeRotation(CGFloat angle) 
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

旋转和缩放变换均可以很好解释--分别旋转或者缩放一个向量的值。平移变换是指每一个点都移动了向量指定的x或者y值--因此若是向量表明了一个点,那它就平移了这个点的距离。

咱们用一个很简单的项目来作个demo,把一个原始视图旋转45度角度(图5.3)

5.3.jpeg

图5.3 使用仿射变换旋转45度角以后的视图

UIView能够经过设置transform属性作变换,但实际上它只是封装了内部图层的变换。

CALayer 一样也有一个transform属性,但它的类型是CATransform3D,而不是CGAffineTransform,本章后续将会详细解释。 CALayer对应于UIView的transform属性叫作affineTransform,清单5.1的例子就是使用 affineTransform对图层作了45度顺时针旋转。

清单5.1 使用affineTransform对图层旋转45度

1
2
3
4
5
6
7
8
9
10
11
12
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //rotate the layer 45 degrees
     CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
     self.layerView.layer.affineTransform = transform;
}
@end

注意咱们使用的旋转常量是M_PI_4,而不是你想象的45,由于iOS的变换函数使用弧度而不是角度做为单位。弧度用数学常量pi的倍数表示,一个pi表明180度,因此四分之一的pi就是45度。

C的数学函数库(iOS会自动引入)提供了pi的一些简便的换算,M_PI_4因而就是pi的四分之一,若是对换算不太清楚的话,能够用以下的宏作换算:

1
2
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0) 
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)

混合变换

Core Graphics提供了一系列的函数能够在一个变换的基础上作更深层次的变换,若是作一个既要缩放又要旋转的变换,这就会很是有用了。例以下面几个函数:

1
2
3
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)     
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)      
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

当操纵一个变换的时候,初始生成一个什么都不作的变换很重要--也就是建立一个CGAffineTransform类型的空值,矩阵论中称做单位矩阵,Core Graphics一样也提供了一个方便的常量:

1
CGAffineTransformIdentity

最后,若是须要混合两个已经存在的变换矩阵,就可使用以下方法,在两个变换的基础上建立一个新的变换:

1
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

咱们来用这些函数组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右移动200个像素(清单5.2)。图5.4显示了图层变换最后的结果。

清单5.2 使用若干方法建立一个复合变换

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad
{
     [ super  viewDidLoad];  //create a new transform
     CGAffineTransform transform = CGAffineTransformIdentity;  //scale by 50%
     transform = CGAffineTransformScale(transform, 0.5, 0.5);  //rotate by 30 degrees
     transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);  //translate by 200 points
     transform = CGAffineTransformTranslate(transform, 200, 0);
     //apply transform to layer
     self.layerView.layer.affineTransform = transform;
}

5.4.jpeg

图5.4 顺序应用多个仿射变换以后的结果

图 5.4中有些须要注意的地方:图片向右边发生了平移,但并无指定距离那么远(200像素),另外它还有点向下发生了平移。缘由在于当你按顺序作了变换, 上一个变换的结果将会影响以后的变换,因此200像素的向右平移一样也被旋转了30度,缩小了50%,因此它其实是斜向移动了100像素。

这意味着变换的顺序会影响最终的结果,也就是说旋转以后的平移和平移以后的旋转结果可能不一样。

剪切变换

Core Graphics为你提供了计算变换矩阵的一些方法,因此不多须要直接设置CGAffineTransform的值。除非须要建立一个斜切的变换,Core Graphics并无提供直接的函数。

斜切变换是放射变换的第四种类型,较于平移,旋转和缩放并不经常使用(这也是Core Graphics没有提供相应函数的缘由),但有些时候也会颇有用。咱们用一张图片能够很直接的说明效果(图5.5)。也许用“倾斜”描述更加恰当,具体作变换的代码见清单5.3。

5.5.jpeg

图5.5 水平方向的斜切变换

清单5.3 实现一个斜切变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation ViewController
CGAffineTransform CGAffineTransformMakeShear(CGFloat x, CGFloat y)
{
     CGAffineTransform transform = CGAffineTransformIdentity;
     transform.c = -x;
     transform.b = y;
     return  transform;
}
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //shear the layer at a 45-degree angle
     self.layerView.layer.affineTransform = CGAffineTransformMakeShear(1, 0);
}
@end

3D变换

CG的前缀告诉咱们,CGAffineTransform类型属于Core Graphics框架,Core Graphics其实是一个严格意义上的2D绘图API,而且CGAffineTransform仅仅对2D变换有效。

在第三章中,咱们提到了zPosition属性,能够用来让图层靠近或者远离相机(用户视角),transform属性(CATransform3D类型)能够真正作到这点,即让图层在3D空间内移动或者旋转。

和CGAffineTransform相似,CATransform3D也是一个矩阵,可是和2x3的矩阵不一样,CATransform3D是一个能够在3维空间内作变换的4x4的矩阵(图5.6)。

5.6.jpeg

图5.6 对一个3D像素点作CATransform3D矩阵变换

和 CGAffineTransform矩阵相似,Core Animation提供了一系列的方法用来建立和组合CATransform3D类型的矩阵,和Core Graphics的函数相似,可是3D的平移和旋转多处了一个z参数,而且旋转函数除了angle以外多出了x,y,z三个参数,分别决定了每一个坐标轴方 向上的旋转:

1
2
3
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) 
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

你应该对X轴和Y轴比较熟悉了,分别以右和下为正方向(回忆第三章,这是iOS上的标准结构,在Mac OS,Y轴朝上为正方向),Z轴和这两个轴分别垂直,指向视角外为正方向(图5.7)。

5.7.jpeg

图5.7 X,Y,Z轴,以及围绕它们旋转的方向

由图所见,绕Z轴的旋转等同于以前二维空间的仿射旋转,可是绕X轴和Y轴的旋转就突破了屏幕的二维空间,而且在用户视角看来发生了倾斜。

举个例子:清单5.4的代码使用了CATransform3DMakeRotation对视图内的图层绕Y轴作了45度角的旋转,咱们能够把视图向右倾斜,这样会看得更清晰。

结果见图5.8,但并不像咱们期待的那样。

清单5.4 绕Y轴旋转图层

1
2
3
4
5
6
7
8
9
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //rotate the layer 45 degrees along the Y axis
     CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
     self.layerView.layer.transform = transform;
}
@end

5.8.jpeg

图5.8 绕y轴旋转45度的视图

看起来图层并无被旋转,而是仅仅在水平方向上的一个压缩,是哪里出了问题呢?

其实彻底没错,视图看起来更窄其实是由于咱们在用一个斜向的视角看它,而不是透视。

透视投影

在真实世界中,当物体原理咱们的时候,因为视角的缘由看起来会变小,理论上说远离咱们的视图的边要比靠近视角的边跟短,但实际上并无发生,而咱们当前的视角是等距离的,也就是在3D变换中任然保持平行,和以前提到的仿射变换相似。

在等距投影中,远处的物体和近处的物体保持一样的缩放比例,这种投影也有它本身的用处(例如建筑绘图,颠倒,和伪3D视频),但当前咱们并不须要。

为了作一些修正,咱们须要引入投影变换(又称做z变换)来对除了旋转以外的变换矩阵作一些修改,Core Animation并无给咱们提供设置透视变换的函数,所以咱们须要手动修改矩阵值,幸运的是,很简单:

CATransform3D的透视效果经过一个矩阵中一个很简单的元素来控制:m34。m34(图5.9)用于按比例缩放X和Y的值来计算到底要离视角多远。

5.9.jpeg

图5.9 CATransform3D的m34元素,用来作透视

m34的默认值是0,咱们能够经过设置m34为-1.0 / d来应用透视效果,d表明了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不须要,大概估算一个就行了。

因 为视角相机实际上并不存在,因此能够根据屏幕上的显示效果自由决定它的防止的位置。一般500-1000就已经很好了,但对于特定的图层有时候更小后者更 大的值会看起来更舒服,减小距离的值会加强透视效果,因此一个很是微小的值会让它看起来更加失真,然而一个很是大的值会让它基本失去透视效果,对视图应用 透视的代码见清单5.5,结果见图5.10。

清单5.5 对变换应用透视效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //create a new transform
     CATransform3D transform = CATransform3DIdentity;
     //apply perspective
     transform.m34 = - 1.0 / 500.0;
     //rotate by 45 degrees along the Y axis
     transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
     //apply to layer
     self.layerView.layer.transform = transform;
}
@end

5.10.jpeg

图5.10 应用透视效果以后再次对图层作旋转

消亡点

当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,因而全部的物体最后都汇聚消失在同一个点。

在现实中,这个点一般是视图的中心(图5.11),因而为了在应用中建立拟真效果的透视,这个店应该聚在屏幕中点,或者至少是包含全部3D对象的视图中点。

5.11.jpeg

图5.11 消亡点

Core Animation定义了这个点位于变换图层的anchorPoint(一般位于图层中心,但也有例外,见第三章)。这就是说,当图层发生变换时,这个点永远位于图层变换以前anchorPoint的位置。

当 改变一个图层的position,你也改变了它的消亡点,作3D变换的时候要时刻记住这一点,当你视图经过调整m34来让它更加有3D效果,应该首先把它 放置于屏幕中央,而后经过平移来把它移动到指定位置(而不是直接改变它的position),这样全部的3D图层都共享一个消亡点。

sublayerTransform属性

如 果有多个视图或者图层,每一个都作3D变换,那就须要分别设置相同的m34值,而且确保在变换以前都在屏幕中央共享同一个position,若是用一个函数 封装这些操做的确会更加方便,但仍然有限制(例如,你不能在Interface Builder中摆放视图),这里有一个更好的方法。

CALayer有一个属性叫作sublayerTransform。它也是CATransform3D类型,但和对一个图层的变换不一样,它影响到全部的子图层。这意味着你能够一次性对包含这些图层的容器作变换,因而全部的子图层都自动继承了这个变换方法。

相 较而言,经过在一个地方设置透视变换会很方便,同时它会带来另外一个显著的优点:消亡点被设置在容器图层的中点,从而不须要再对子图层分别设置了。这意味着 你能够随意使用position和frame来放置子图层,而不须要把它们放置在屏幕中点,而后为了保证统一的消亡点用变换来作平移。

咱们来用一个demo举例说明。这里用Interface Builder并排放置两个视图(图5.12),而后经过设置它们容器视图的透视变换,咱们能够保证它们有相同的透视和消亡点,代码见清单5.6,结果见图5.13。

5.12.jpg

图5.12 在一个视图容器内并排放置两个视图

清单5.6 应用sublayerTransform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //apply perspective transform to container
     CATransform3D perspective = CATransform3DIdentity;
     perspective.m34 = - 1.0 / 500.0;
     self.containerView.layer.sublayerTransform = perspective;
     //rotate layerView1 by 45 degrees along the Y axis
     CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
     self.layerView1.layer.transform = transform1;
     //rotate layerView2 by 45 degrees along the Y axis
     CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
     self.layerView2.layer.transform = transform2;
}

5.13.jpeg

图5.13 经过相同的透视效果分别对视图作变换

背面

咱们既然能够在3D场景下旋转图层,那么也能够从背面去观察它。若是咱们在清单5.4中把角度修改成M_PI(180度)而不是当前的M_PI_4(45度),那么将会把图层彻底旋转一个半圈,因而彻底背对了相机视角。

那么从背部看图层是什么样的呢,见图5.14

5.14.jpeg

图5.14 视图的背面,一个镜像对称的图片

如你所见,图层是双面绘制的,反面显示的是正面的一个镜像图片。

但这并非一个很好的特性,由于若是图层包含文本或者其余控件,那用户看到这些内容的镜像图片固然会感到困惑。另外也有可能形成资源的浪费:想象用这些图层造成一个不透明的固态立方体,既然永远都看不见这些图层的背面,那为何浪费GPU来绘制它们呢?

CALayer有一个叫作doubleSided的属性来控制图层的背面是否要被绘制。这是一个BOOL类型,默认为YES,若是设置为NO,那么当图层正面从相机视角消失的时候,它将不会被绘制。

扁平化图层

若是对包含已经作过变换的图层的图层作反方向的变换将会发什么什么呢?是否是有点困惑?见图5.15

5.15.jpeg

图5.15 反方向变换的嵌套图层

注意作了-45度旋转的内部图层是怎样抵消旋转45度的图层,从而恢复正常状态的。

若是内部图层相对外部图层作了相反的变换(这里是绕Z轴的旋转),那么按照逻辑这两个变换将被相互抵消。

验证一下,相应代码见清单5.7,结果见5.16

清单5.7 绕Z轴作相反的旋转变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *outerView;
@property (nonatomic, weak) IBOutlet UIView *innerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //rotate the outer layer 45 degrees
     CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
     self.outerView.layer.transform = outer;
     //rotate the inner layer -45 degrees
     CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
     self.innerView.layer.transform = inner;
}
@end

5.16.jpeg

图5.16 旋转后的视图

运 行结果和咱们预期的一致。如今在3D状况下再试一次。修改代码,让内外两个视图绕Y轴旋转而不是Z轴,再加上透视效果,以便咱们观察。注意不能用 sublayerTransform属性,由于内部的图层并不直接是容器图层的子图层,因此这里分别对图层设置透视变换(清单5.8)。

清单5.8 绕Y轴相反的旋转变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //rotate the outer layer 45 degrees
     CATransform3D outer = CATransform3DIdentity;
     outer.m34 = -1.0 / 500.0;
     outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0);
     self.outerView.layer.transform = outer;
     //rotate the inner layer -45 degrees
     CATransform3D inner = CATransform3DIdentity;
     inner.m34 = -1.0 / 500.0;
     inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0);
     self.innerView.layer.transform = inner;
}

预期的效果应该如图5.17所示。

5.17.jpeg

图5.17 绕Y轴作相反旋转的预期结果。

但其实这并非咱们所看到的,相反,咱们看到的结果如图5.18所示。发什么了什么呢?内部的图层仍然向左侧旋转,而且发生了扭曲,但按道理说它应该保持正面朝上,而且显示正常的方块。

这 是因为尽管Core Animation图层存在于3D空间以内,但它们并不都存在同一个3D空间。每一个图层的3D场景实际上是扁平化的,当你从正面观察一个图层,看到的实际上 由子图层建立的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。

5.18.jpeg

图5.18 绕Y轴作相反旋转的真实结果

相似的,当你在玩一个3D游戏,实际上仅仅是把屏幕作了一次倾斜,或许在游戏中能够看见有一面墙在你面前,可是倾斜屏幕并不可以看见墙里面的东西。全部场景里面绘制的东西并不会随着你观察它的角度改变而发生变化;图层也是一样的道理。

这使得用Core Animation建立很是复杂的3D场景变得十分困难。你不可以使用图层树去建立一个3D结构的层级关系--在相同场景下的任何3D表面必须和一样的图层保持一致,这是由于每一个的父视图都把它的子视图扁平化了。

至少当你用正常的CALayer的时候是这样,CALayer有一个叫作CATransformLayer的子类来解决这个问题。具体在第六章“特殊的图层”中将会具体讨论。

固体对象

如今你懂得了在3D空间的一些图层布局的基础,咱们来试着建立一个固态的3D对象(其实是一个技术上所谓的空洞对象,但它以固态呈现)。咱们用六个独立的视图来构建一个立方体的各个面。

在 这个例子中,咱们用Interface Builder来构创建方体的面(图5.19),咱们固然能够用代码来写,可是用Interface Builder的好处是能够方便的在每个面上添加子视图。记住这些面仅仅是包含视图和控件的普通的用户界面元素,它们彻底是咱们界面交互的部分,而且当 把它折成一个立方体以后也不会改变这个性质。

62.jpg

图5.19 用Interface Builder对立方体的六个面进行布局

这 些面视图并无放置在主视图当中,而是松散地排列在根nib文件里面。咱们并不关心在这个容器中如何摆放它们的位置,由于后续将会用图层的 transform对它们进行从新布局,而且用Interface Builder在容器视图以外摆放他们可让咱们容易看清楚它们的内容,若是把它们一个叠着一个都塞进主视图,将会变得很难看。

咱们把一个有颜色的UILabel放置在视图内部,是为了清楚的辨别它们之间的关系,而且UIButton被放置在第三个面视图里面,后面会作简单的解释。

具体把视图组织成立方体的代码见清单5.9,结果见图5.20

清单5.9 建立一个立方体

1
2
3
4
5
6
7
8
9
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
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
     //get the face view and add it to the container
     UIView *face = self.faces[index];
     [self.containerView addSubview:face];
     //center the face view within the container
     CGSize containerSize = self.containerView.bounds.size;
     face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
     // apply the transform
     face.layer.transform = transform;
}
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //set up the container sublayer transform
     CATransform3D perspective = CATransform3DIdentity;
     perspective.m34 = -1.0 / 500.0;
     self.containerView.layer.sublayerTransform = perspective;
     //add cube face 1
     CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
     [self addFace:0 withTransform:transform];
     //add cube face 2
     transform = CATransform3DMakeTranslation(100, 0, 0);
     transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
     [self addFace:1 withTransform:transform];
     //add cube face 3
     transform = CATransform3DMakeTranslation(0, -100, 0);
     transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
     [self addFace:2 withTransform:transform];
     //add cube face 4
     transform = CATransform3DMakeTranslation(0, 100, 0);
     transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
     [self addFace:3 withTransform:transform];
     //add cube face 5
     transform = CATransform3DMakeTranslation(-100, 0, 0);
     transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
     [self addFace:4 withTransform:transform];
     //add cube face 6
     transform = CATransform3DMakeTranslation(0, 0, -100);
     transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
     [self addFace:5 withTransform:transform];
}
@end

5.20.jpeg

图5.20 正面朝上的立方体

从这个角度看立方体并非很明显;看起来只是一个方块,为了更好地欣赏它,咱们将更换一个不一样的视角。

旋转这个立方体将会显得很笨重,由于咱们要单独对每一个面作旋转。另外一个简单的方案是经过调整容器视图的sublayerTransform去旋转照相机。

添加以下几行去旋转containerView图层的perspective变换矩阵:

1
2
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); 
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

这就对相机(或者相对相机的整个场景,你也能够这么认为)绕Y轴旋转45度,而且绕X轴旋转45度。如今从另外一个角度去观察立方体,就能看出它的真实面貌(图5.21)。

5.21.jpeg

图5.21 从一个边角观察的立方体

光亮和阴影

现 在它看起来更像是一个立方体没错了,可是对每一个面之间的链接仍是很难分辨。Core Animation能够用3D显示图层,可是它对光线并无概念。若是想让立方体看起来更加真实,须要本身作一个阴影效果。你能够经过改变每一个面的背景颜 色或者直接用带光亮效果的图片来调整。

若是须要动态地建立光线效果,你能够根据每一个视图的方向应用不一样的alpha值作出半透明的阴影图 层,但为了计算阴影图层的不透明度,你须要获得每一个面的正太向量(垂直于表面的向量),而后根据一个想象的光源计算出两个向量叉乘结果。叉乘表明了光源和 图层之间的角度,从而决定了它有多大程度上的光亮。

清单5.10实现了这样一个结果,咱们用GLKit框架来作向量的计算(你须要引入 GLKit库来运行代码),每一个面的CATransform3D都被转换成GLKMatrix4,而后经过GLKMatrix4GetMatrix3函数 得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,而后能够用它来获得正太向量的值。

结果如图5.22所示,试着调整LIGHT_DIRECTION和AMBIENT_LIGHT的值来切换光线效果

清单5.10 对立方体的表面应用动态的光线效果

1
2
3
4
5
6
7
8
9
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
#import "ViewController.h" 
#import 
#import
#define LIGHT_DIRECTION 0, 1, -0.5 
#define AMBIENT_LIGHT 0.5
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)applyLightingToFace:(CALayer *)face
{
     //add lighting layer
     CALayer *layer = [CALayer layer];
     layer.frame = face.bounds;
     [face addSublayer:layer];
     //convert the face transform to matrix
     //(GLKMatrix4 has the same structure as CATransform3D)
     CATransform3D transform = face.transform;
     GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
     GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
     //get face normal
     GLKVector3 normal = GLKVector3Make(0, 0, 1);
     normal = GLKMatrix3MultiplyVector3(matrix3, normal);
     normal = GLKVector3Normalize(normal);
     //get dot product with light direction
     GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
     float dotProduct = GLKVector3DotProduct(light, normal);
     //set lighting layer opacity
     CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
     UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
     layer.backgroundColor = color.CGColor;
}
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
     //get the face view and add it to the container
     UIView *face = self.faces[index];
     [self.containerView addSubview:face];
     //center the face view within the container
     CGSize containerSize = self.containerView.bounds.size;
     face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
     // apply the transform
     face.layer.transform = transform;
     //apply lighting
     [self applyLightingToFace:face.layer];
}
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //set up the container sublayer transform
     CATransform3D perspective = CATransform3DIdentity;
     perspective.m34 = -1.0 / 500.0;
     perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
     perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
     self.containerView.layer.sublayerTransform = perspective;
     //add cube face 1
     CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
     [self addFace:0 withTransform:transform];
     //add cube face 2
     transform = CATransform3DMakeTranslation(100, 0, 0);
     transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
     [self addFace:1 withTransform:transform];
     //add cube face 3
     transform = CATransform3DMakeTranslation(0, -100, 0);
     transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
     [self addFace:2 withTransform:transform];
     //add cube face 4
     transform = CATransform3DMakeTranslation(0, 100, 0);
     transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
     [self addFace:3 withTransform:transform];
     //add cube face 5
     transform = CATransform3DMakeTranslation(-100, 0, 0);
     transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
     [self addFace:4 withTransform:transform];
     //add cube face 6
     transform = CATransform3DMakeTranslation(0, 0, -100);
     transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
     [self addFace:5 withTransform:transform];
}
@end

5.22.jpeg

图5.22 动态计算光线效果以后的立方体

点击事件

你应该能注意到如今能够在第三个表面的顶部看见按钮了,点击它,什么都没发生,为何呢?

这 并非由于iOS在3D场景下正确地处理响应事件,其实是能够作到的。问题在于视图顺序。在第三章中咱们简要提到过,点击事件的处理由视图在父视图中的 顺序决定的,并非3D空间中的Z轴顺序。当给立方体添加视图的时候,咱们其实是按照一个顺序添加,因此按照视图/图层顺序来讲,4,5,6在3的前 面。

即便咱们看不见4,5,6的表面(由于被1,2,3遮住了),iOS在事件响应上仍然保持以前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体同样。

你 也许认为把doubleSided设置成NO能够解决这个问题,由于它再也不渲染视图后面的内容,但实际上并不起做用。由于背对相机而隐藏的视图仍然会响应 点击事件(这和经过设置hidden属性或者设置alpha为0而隐藏的视图不一样,那两种方式将不会响应事件)。因此即便禁止了双面渲染仍然不能解决这个 问题(虽然因为性能问题,仍是须要把它设置成NO)。

这里有几种正确的方案:把除了表面3的其余视图userInteractionEnabled属性都设置成NO来禁止事件传递。或者简单经过代码把视图3覆盖在视图6上。不管怎样均可以点击按钮了(图5.23)。

5.23.jpeg

图5.23 背景视图再也不阻碍按钮,咱们能够点击它了

总结

这 一章涉及了一些2D和3D的变换。你学习了一些矩阵计算的基础,以及如何用Core Animation建立3D场景。你看到了图层背后究竟是如何呈现的,而且知道了不能把扁平的图片作成真实的立体效果,最后咱们用demo说明了触摸事件 的处理,视图中图层添加的层级顺序会比屏幕上显示的顺序更有意义。

第六章咱们会研究一些Core Animation提供不一样功能的具体的CALayer子类。

相关文章
相关标签/搜索