Core Animation实战六(专用图层),互联网公司面试真题

//choose a font

UIFont *font = [UIFont systemFontOfSize:15];

//set layer font

CFStringRef fontName = (__bridge CFStringRef)font.fontName;

CGFontRef fontRef = CGFontCreateWithFontName(fontName);

textLayer.font = fontRef;

textLayer.fontSize = font.pointSize;

CGFontRelease(fontRef);

//choose some text

NSString *text = @“Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis”;

//set layer text

textLayer.string = text;

}

@end

复制代码

图6.2 用CATextLayer来显示一个纯文本标签

如果你仔细看这个文本,你会发现一个奇怪的地方:这些文本有一些像素化了。这是因为并没有以Retina的方式渲染,第二章提到了这个contentScale属性,用来决定图层内容应该以怎样的分辨率来渲染。contentsScale并不关心屏幕的拉伸因素而总是默认为1.0。如果我们想以Retina的质量来显示文字,我们就得手动地设置CATextLayercontentsScale属性,如下:

textLayer.contentsScale = [UIScreen mainScreen].scale;

这样就解决了这个问题(如图6.3)

图6.3 设置contentsScale来匹配屏幕

CATextLayerfont属性不是一个UIFont类型,而是一个CFTypeRef类型。这样可以根据你的具体需要来决定字体属性应该是用CGFontRef类型还是CTFontRef类型(Core Text字体)。同时字体大小也是用fontSize属性单独设置的,因为CTFontRefCGFontRef并不像UIFont一样包含点大小。这个例子会告诉你如何将UIFont转换成CGFontRef

另外,CATextLayerstring属性并不是你想象的NSString类型,而是id类型。这样你既可以用NSString也可以用NSAttributedString来指定文本了(注意,NSAttributedString并不是NSString的子类)。属性化字符串是iOS用来渲染字体风格的机制,它以特定的方式来决定指定范围内的字符串的原始信息,比如字体,颜色,字重,斜体等。

富文本

iOS 6中,Apple给UILabel和其他UIKit文本视图添加了直接的属性化字符串的支持,应该说这是一个很方便的特性。不过事实上从iOS3.2开始CATextLayer就已经支持属性化字符串了。这样的话,如果你想要支持更低版本的iOS系统,CATextLayer无疑是你向界面中增加富文本的好办法,而且也不用去跟复杂的Core Text打交道,也省了用UIWebView的麻烦。

让我们编辑一下示例使用到NSAttributedString(见清单6.3).iOS 6及以上我们可以用新的NSTextAttributeName实例来设置我们的字符串属性,但是练习的目的是为了演示在iOS 5及以下,所以我们用了Core Text,也就是说你需要把Core Text framework添加到你的项目中。否则,编译器是无法识别属性常量的。

图6.4是代码运行结果(注意那个红色的下划线文本)

清单6.3 用NSAttributedString实现一个富文本标签。

复制代码

#import “DrawingView.h”

#import <QuartzCore/QuartzCore.h>

#import <CoreText/CoreText.h>

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *labelView;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//create a text layer

CATextLayer *textLayer = [CATextLayer layer];

textLayer.frame = self.labelView.bounds;

textLayer.contentsScale = [UIScreen mainScreen].scale;

[self.labelView.layer addSublayer:textLayer];

//set text attributes

textLayer.alignmentMode = kCAAlignmentJustified;

textLayer.wrapped = YES;

//choose a font

UIFont *font = [UIFont systemFontOfSize:15];

//choose some text

NSString *text = @“Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc \ elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis”;

//create attributed string

NSMutableAttributedString *string = nil;

string = [[NSMutableAttributedString alloc] initWithString:text];

//convert UIFont to a CTFont

CFStringRef fontName = (__bridge CFStringRef)font.fontName;

CGFloat fontSize = font.pointSize;

CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);

//set text attributes

NSDictionary *attribs = @{

(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,

(__bridge id)kCTFontAttributeName: (__bridge id)fontRef

};

[string setAttributes:attribs range:NSMakeRange(0, [text length])];

attribs = @{

(__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,

(__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),

(__bridge id)kCTFontAttributeName: (__bridge id)fontRef

};

[string setAttributes:attribs range:NSMakeRange(6, 5)];

//release the CTFont we created earlier

CFRelease(fontRef);

//set layer text

textLayer.string = string;

}

@end

复制代码

图6.4 用CATextLayer实现一个富文本标签。

行距和字距

有必要提一下的是,由于绘制的实现机制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不尽相同的。

二者的差异程度(由使用的字体和字符决定)总的来说挺小,但是如果你想正确的显示普通便签和CATextLayer就一定要记住这一点。

UILabel的替代品

我们已经证实了CATextLayerUILabel有着更好的性能表现,同时还有额外的布局选项并且在iOS 5上支持富文本。但是与一般的标签比较而言会更加繁琐一些。如果我们真的在需求一个UILabel的可用替代品,最好是能够在Interface Builder上创建我们的标签,而且尽可能地像一般的视图一样正常工作。

我们应该继承UILabel,然后添加一个子图层CATextLayer并重写显示文本的方法。但是仍然会有由UILabel-drawRect:方法创建的空寄宿图。而且由于CALayer不支持自动缩放和自动布局,子视图并不是主动跟踪视图边界的大小,所以每次视图大小被更改,我们不得不手动更新子图层的边界。

我们真正想要的是一个用CATextLayer作为宿主图层的UILabel子类,这样就可以随着视图自动调整大小而且也没有冗余的寄宿图啦。

就像我们在第一章『图层树』讨论的一样,每一个UIView都是寄宿在一个CALayer的示例上。这个图层是由视图自动创建和管理的,那我们可以用别的图层类型替代它么?一旦被创建,我们就无法代替这个图层了。但是如果我们继承了UIView,那我们就可以重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层。

清单6.4 演示了一个UILabel子类LayerLabelCATextLayer绘制它的问题,而不是调用一般的UILabel使用的较慢的-drawRect:方法。LayerLabel示例既可以用代码实现,也可以在Interface Builder实现,只要把普通的标签拖入视图之中,然后设置它的类是LayerLabel就可以了。

清单6.4 使用CATextLayerUILabel子类:LayerLabel

复制代码

#import “LayerLabel.h”

#import <QuartzCore/QuartzCore.h>

@implementation LayerLabel

  • (Class)layerClass

{

//this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer

return [CATextLayer class];

}

- (CATextLayer *)textLayer

{

return (CATextLayer *)self.layer;

}

- (void)setUp

{

//set defaults from UILabel settings

self.text = self.text;

self.textColor = self.textColor;

self.font = self.font;

//we should really derive these from the UILabel settings too

//but that’s complicated, so for now we’ll just hard-code them

[self textLayer].alignmentMode = kCAAlignmentJustified;

[self textLayer].wrapped = YES;

[self.layer display];

}

- (id)initWithFrame:(CGRect)frame

{

//called when creating label programmatically

if (self = [super initWithFrame:frame]) {

[self setUp];

}

return self;

}

- (void)awakeFromNib

{

//called when creating label using Interface Builder

[self setUp];

}

- (void)setText:(NSString *)text

{

super.text = text;

//set layer text

[self textLayer].string = text;

}

- (void)setTextColor:(UIColor *)textColor

{

super.textColor = textColor;

//set layer text color

[self textLayer].foregroundColor = textColor.CGColor;

}

- (void)setFont:(UIFont *)font

{

super.font = font;

//set layer font

CFStringRef fontName = (__bridge CFStringRef)font.fontName;

CGFontRef fontRef = CGFontCreateWithFontName(fontName);

[self textLayer].font = fontRef;

[self textLayer].fontSize = font.pointSize;

CGFontRelease(fontRef);

}

@end

复制代码

如果你运行代码,你会发现文本并没有像素化,而我们也没有设置contentsScale属性。把CATextLayer作为宿主图层的另一好处就是视图自动设置了contentsScale属性。

在这个简单的例子中,我们只是实现了UILabel的一部分风格和布局属性,不过稍微再改进一下我们就可以创建一个支持UILabel所有功能甚至更多功能的LayerLabel类(你可以在一些线上的开源项目中找到)。

如果你打算支持iOS 6及以上,基于CATextLayer的标签可能就有有些局限性。但是总得来说,如果想在app里面充分利用CALayer子类,用+layerClass来创建基于不同图层的视图是一个简单可复用的方法。

CATransformLayer

当我们在构造复杂的3D事物的时候,如果能够组织独立元素就太方便了。比如说,你想创造一个孩子的手臂:你就需要确定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。

当然是允许独立地移动每个区域的啦。以肘为指点会移动前臂和手,而不是肩膀。Core Animation图层很容易就可以让你在2D环境下做出这样的层级体系下的变换,但是3D情况下就不太可能,因为所有的图层都把他的孩子都平面化到一个场景中(第五章『变换』有提到)。

CATransformLayer解决了这个问题,CATransformLayer不同于普通的CALayer,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构,比如我的手臂示例。

用代码创建一个手臂需要相当多的代码,所以我就演示得更简单一些吧:在第五章的立方体示例,我们将通过旋转camara来解决图层平面化问题而不是像立方体示例代码中用的sublayerTransform。这是一个非常不错的技巧,但是只能作用域单个对象上,如果你的场景包含两个立方体,那我们就不能用这个技巧单独旋转他们了。

那么,就让我们来试一试CATransformLayer吧,第一个问题就来了:在第五章,我们是用多个视图来构造了我们的立方体,而不是单独的图层。我们不能在不打乱已有的视图层次的前提下在一个本身不是有寄宿图的图层中放置一个寄宿图图层。我们可以创建一个新的UIView子类寄宿在CATransformLayer(用+layerClass方法)之上。但是,为了简化案例,我们仅仅重建了一个单独的图层,而不是使用视图。这意味着我们不能像第五章一样在立方体表面显示按钮和标签,不过我们现在也用不到这个特性。

清单6.5就是代码。我们以我们在第五章使用过的相同基本逻辑放置立方体。但是并不像以前那样直接将立方面添加到容器视图的宿主图层,我们将他们放置到一个CATransformLayer中创建一个独立的立方体对象,然后将两个这样的立方体放进容器中。我们随机地给立方面染色以将他们区分开来,这样就不用靠标签或是光亮来区分他们。图6.5是运行结果。

清单6.5 用CATransformLayer装配一个3D图层体系

复制代码

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (CALayer *)faceWithTransform:(CATransform3D)transform

{

//create cube face layer

CALayer *face = [CALayer layer];

face.frame = CGRectMake(-50, -50, 100, 100);

//apply a random color

CGFloat red = (rand() / (double)INT_MAX);

CGFloat green = (rand() / (double)INT_MAX);

CGFloat blue = (rand() / (double)INT_MAX);

face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

//apply the transform and return

face.transform = transform;

return face;

}

- (CALayer *)cubeWithTransform:(CATransform3D)transform

{

//create cube layer

CATransformLayer *cube = [CATransformLayer layer];

//add cube face 1

CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 2

ct = CATransform3DMakeTranslation(50, 0, 0);

ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 3

ct = CATransform3DMakeTranslation(0, -50, 0);

ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 4

ct = CATransform3DMakeTranslation(0, 50, 0);

ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 5

ct = CATransform3DMakeTranslation(-50, 0, 0);

ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 6

ct = CATransform3DMakeTranslation(0, 0, -50);

ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//center the cube layer within the container

CGSize containerSize = self.containerView.bounds.size;

cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);

//apply the transform and return

cube.transform = transform;

return cube;

}

- (void)viewDidLoad

{

[super viewDidLoad];

//set up the perspective transform

CATransform3D pt = CATransform3DIdentity;

pt.m34 = -1.0 / 500.0;

self.containerView.layer.sublayerTransform = pt;

//set up the transform for cube 1 and add it

CATransform3D c1t = CATransform3DIdentity;

c1t = CATransform3DTranslate(c1t, -100, 0, 0);

CALayer *cube1 = [self cubeWithTransform:c1t];

[self.containerView.layer addSublayer:cube1];

//set up the transform for cube 2 and add it

CATransform3D c2t = CATransform3DIdentity;

c2t = CATransform3DTranslate(c2t, 100, 0, 0);

c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);

c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);

CALayer *cube2 = [self cubeWithTransform:c2t];

[self.containerView.layer addSublayer:cube2];

}

@end

复制代码

图6.5 同一视角下的俩不同变换的立方体

CAGradientLayer


CAGradientLayer是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。

基础渐变

我们将从一个简单的红变蓝的对角线渐变开始(见清单6.6).这些渐变色彩放在一个数组中,并赋给colors属性。这个数组成员接受CGColorRef类型的值(并不是从NSObject派生而来),所以我们要用通过bridge转换以确保编译正常。

CAGradientLayer也有startPointendPoint属性,他们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标是{1, 1}。代码运行结果如图6.6

清单6.6 简单的两种颜色的对角线渐变

复制代码

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//create gradient layer and add it to our container view

CAGradientLayer *gradientLayer = [CAGradientLayer layer];

gradientLayer.frame = self.containerView.bounds;

[self.containerView.layer addSublayer:gradientLayer];

//set gradient colors

gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];

//set gradient start and end points

gradientLayer.startPoint = CGPointMake(0, 0);

gradientLayer.endPoint = CGPointMake(1, 1);

}

@end

复制代码

图6.6 用CAGradientLayer实现简单的两种颜色的对角线渐变

多重渐变

如果你愿意,colors属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用locations属性来调整空间。locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个不同颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。

locations数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations的数组大小和colors数组大小一定要相同,否则你将会得到一个空白的渐变。

清单6.7展示了一个基于清单6.6的对角线渐变的代码改造。现在变成了从红到黄最后到绿色的渐变。locations数组指定了0.0,0.25和0.5三个数值,这样这三个渐变就有点像挤在了左上角。(如图6.7).

清单6.7 在渐变上使用locations

复制代码

- (void)viewDidLoad {

[super viewDidLoad];

//create gradient layer and add it to our container view

CAGradientLayer *gradientLayer = [CAGradientLayer layer];

gradientLayer.frame = self.containerView.bounds;

[self.containerView.layer addSublayer:gradientLayer];

//set gradient colors

gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id) [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];

//set locations

gradientLayer.locations = @[@0.0, @0.25, @0.5];

//set gradient start and end points

gradientLayer.startPoint = CGPointMake(0, 0);

gradientLayer.endPoint = CGPointMake(1, 1);

}

复制代码

图6.7 用locations构造偏移至左上角的三色渐变

CAReplicatorLayer


CAReplicatorLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。看上去演示能够更加解释这些,我们来写个例子吧。

重复图层(Repeating Layers)

清单6.8中,我们在屏幕的中间创建了一个小白色方块图层,然后用CAReplicatorLayer生成十个图层组成一个圆圈。instanceCount属性指定了图层需要重复多少次。instanceTransform指定了一个CATransform3D3D变换(这种情况下,下一图层的位移和旋转将会移动到圆圈的下一个点)。

变换是逐步增加的,每个实例都是相对于前一实例布局。这就是为什么这些复制体最终不会出现在同意位置上,图6.8是代码运行结果。

清单6.8 用CAReplicatorLayer重复图层

复制代码

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//create a replicator layer and add it to our view

CAReplicatorLayer *replicator = [CAReplicatorLayer layer];

replicator.frame = self.containerView.bounds;

[self.containerView.layer addSublayer:replicator];

//configure the replicator

replicator.instanceCount = 10;

//apply a transform for each instance

CATransform3D transform = CATransform3DIdentity;

transform = CATransform3DTranslate(transform, 0, 200, 0);

transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);

transform = CATransform3DTranslate(transform, 0, -200, 0);

replicator.instanceTransform = transform;

//apply a color shift for each instance

replicator.instanceBlueOffset = -0.1;

replicator.instanceGreenOffset = -0.1;

//create a sublayer and place it inside the replicator

CALayer *layer = [CALayer layer];

layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);

layer.backgroundColor = [UIColor whiteColor].CGColor;

[replicator addSublayer:layer];

}

@end

复制代码

图6.8 用CAReplicatorLayer创建一圈图层

注意到当图层在重复的时候,他们的颜色也在变化:这是用instanceBlueOffsetinstanceGreenOffset属性实现的。通过逐步减少蓝色和绿色通道,我们逐渐将图层颜色转换成了红色。这个复制效果看起来很酷,但是CAReplicatorLayer真正应用到实际程序上的场景比如:一个游戏中导弹的轨迹云,或者粒子爆炸(尽管iOS 5已经引入了CAEmitterLayer,它更适合创建任意的粒子效果)。除此之外,还有一个实际应用是:反射。

反射

使用CAReplicatorLayer并应用一个负比例变换于一个复制图层,你就可以创建指定视图(或整个视图层次)内容的镜像图片,这样就创建了一个实时的『反射』效果。让我们来尝试实现这个创意:指定一个继承于UIViewReflectionView,它会自动产生内容的反射效果。实现这个效果的代码很简单(见清单6.9),实际上用ReflectionView实现这个效果会更简单,我们只需要把ReflectionView的实例放置于Interface Builder(见图6.9),它就会实时生成子视图的反射,而不需要别的代码(见图6.10).

清单6.9 用CAReplicatorLayer自动绘制反射

复制代码

#import “ReflectionView.h”

#import <QuartzCore/QuartzCore.h>

@implementation ReflectionView

  • (Class)layerClass

{

return [CAReplicatorLayer class];

}

- (void)setUp

{

//configure replicator

CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;

layer.instanceCount = 2;

//move reflection instance below original and flip vertically

CATransform3D transform = CATransform3DIdentity;

CGFloat verticalOffset = self.bounds.size.height + 2;

transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);

transform = CATransform3DScale(transform, 1, -1, 0);

layer.instanceTransform = transform;

//reduce alpha of reflection layer

layer.instanceAlphaOffset = -0.6;

}

- (id)initWithFrame:(CGRect)frame

{

//this is called when view is created in code

if ((self = [super initWithFrame:frame])) {

[self setUp];

}

return self;

}

- (void)awakeFromNib

{

//this is called when view is created from a nib

[self setUp];

}

@end

复制代码

图6.9 在Interface Builder中使用ReflectionView

图6.10 ReflectionView自动实时产生反射效果。

开源代码ReflectionView完成了一个自适应的渐变淡出效果(用CAGradientLayer和图层蒙板实现),代码见https://github.com/nicklockwood/ReflectionView

CAScrollLayer


对于一个未转换的图层,它的bounds和它的frame是一样的,frame属性是由bounds属性自动计算而出的,所以更改任意一个值都会更新其他值。

但是如果你只想显示一个大图层里面的一小部分呢。比如说,你可能有一个很大的图片,你希望用户能够随意滑动,或者是一个数据或文本的长列表。在一个典型的iOS应用中,你可能会用到UITableView或是UIScrollView,但是对于独立的图层来说,什么会等价于刚刚提到的UITableViewUIScrollView呢?

在第二章中,我们探索了图层的contentsRect属性的用法,它的确是能够解决在图层中小地方显示大图片的解决方法。但是如果你的图层包含子图层那它就不是一个非常好的解决方案,因为,这样做的话每次你想『滑动』可视区域的时候,你就需要手工重新计算并更新所有的子图层位置。

这个时候就需要CAScrollLayer了。CAScrollLayer有一个-scrollToPoint:方法,它自动适应bounds的原点以便图层内容出现在滑动的地方。注意,这就是它做的所有事情。前面提到过,Core Animation并不处理用户输入,所以CAScrollLayer并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何iOS指定行为例如滑动反弹(当视图滑动超多了它的边界的将会反弹回正确的地方)。

让我们来用CAScrollLayer来常见一个基本的UIScrollView替代品。我们将会用CAScrollLayer作为视图的宿主图层,并创建一个自定义的UIView,然后用UIPanGestureRecognizer实现触摸事件响应。这段代码见清单6.10. 图6.11是运行效果:ScrollView显示了一个大于它的frameUIImageView

清单6.10 用CAScrollLayer实现滑动视图

复制代码

#import “ScrollView.h”

#import <QuartzCore/QuartzCore.h> @implementation ScrollView

  • (Class)layerClass

{

return [CAScrollLayer class];

}

- (void)setUp

{

//enable clipping

self.layer.masksToBounds = YES;

//attach pan gesture recognizer

UIPanGestureRecognizer *recognizer = nil;

recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];

[self addGestureRecognizer:recognizer];

}

- (id)initWithFrame:(CGRect)frame

{

//this is called when view is created in code

if ((self = [super initWithFrame:frame])) {

[self setUp];

}

return self;

}

- (void)awakeFromNib {

//this is called when view is created from a nib

[self setUp];

}

- (void)pan:(UIPanGestureRecognizer *)recognizer

{

//get the offset by subtracting the pan gesture

//translation from the current bounds origin

CGPoint offset = self.bounds.origin;

offset.x -= [recognizer translationInView:self].x;

offset.y -= [recognizer translationInView:self].y;

//scroll the layer

[(CAScrollLayer *)self.layer scrollToPoint:offset];

//reset the pan gesture translation

[recognizer setTranslation:CGPointZero inView:self];

}

@end

复制代码

图6.11 用UIScrollView创建一个凑合的滑动视图

不同于UIScrollView,我们定制的滑动视图类并没有实现任何形式的边界检查(bounds checking)。图层内容极有可能滑出视图的边界并无限滑下去。CAScrollLayer并没有等同于UIScrollViewcontentSize的属性,所以当CAScrollLayer滑动的时候完全没有一个全局的可滑动区域的概念,也无法自适应它的边界原点至你指定的值。它之所以不能自适应边界大小是因为它不需要,内容完全可以超过边界。

那你一定会奇怪用CAScrollLayer的意义到底何在,因为你可以简单地用一个普通的CALayer然后手动适应边界原点啊。真相其实并不复杂,UIScrollView并没有用CAScrollLayer,事实上,就是简单的通过直接操作图层边界来实现滑动。

CAScrollLayer有一个潜在的有用特性。如果你查看CAScrollLayer的头文件,你就会注意到有一个扩展分类实现了一些方法和属性:

- (void)scrollPoint:(CGPoint)p;

- (void)scrollRectToVisible:(CGRect)r;

@property(readonly) CGRect visibleRect;

看到这些方法和属性名,你也许会以为这些方法给每个CALayer实例增加了滑动功能。但是事实上他们只是放置在CAScrollLayer中的图层的实用方法。scrollPoint:方法从图层树中查找并找到第一个可用的CAScrollLayer,然后滑动它使得指定点成为可视的。scrollRectToVisible:方法实现了同样的事情只不过是作用在一个矩形上的。visibleRect属性决定图层(如果存在的话)的哪部分是当前的可视区域。如果你自己实现这些方法就会相对容易明白一点,但是CAScrollLayer帮你省了这些麻烦,所以当涉及到实现图层滑动的时候就可以用上了。

CATiledLayer


有些时候你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图。iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。载入大图可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。

能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048*2048,或4096*4096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU(见第12章『速度的曲调』,和第13章『高效绘图』,它更加详细地解释了软件绘制和硬件绘制)。

CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。让我们用实验来证明一下。

小片裁剪

这个示例中,我们将会从一个2048*2048分辨率的雪人图片入手。为了能够从CATiledLayer中获益,我们需要把这个图片裁切成许多小一些的图片。你可以通过代码来完成这件事情,但是如果你在运行时读入整个图片并裁切,那CATiledLayer这些所有的性能优点就损失殆尽了。理想情况下来说,最好能够逐个步骤来实现。

清单6.11 演示了一个简单的Mac OS命令行程序,它用CATiledLayer将一个图片裁剪成小图并存储到不同的文件中。

清单6.11 裁剪图片成小图的终端程序

复制代码

#import <AppKit/AppKit.h>

int main(int argc, const char * argv[])

{

@autoreleasepool{

//handle incorrect arguments

if (argc < 2) {

NSLog(@“TileCutter arguments: inputfile”);

return 0;

}

//input file

NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];

//tile size

CGFloat tileSize = 256; //output path

NSString *outputPath = [inputFile stringByDeletingPathExtension];

//load image

NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];

NSSize size = [image size];

NSArray *representations = [image representations];

if ([representations count]){

NSBitmapImageRep *representation = representations[0];

size.width = [representation pixelsWide];

size.height = [representation pixelsHigh];

}

NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height);

CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];

//calculate rows and columns

NSInteger rows = ceil(size.height / tileSize);

NSInteger cols = ceil(size.width / tileSize);

//generate tiles

for (int y = 0; y < rows; ++y) {

for (int x = 0; x < cols; ++x) {

//extract tile image

CGRect tileRect = CGRectMake(x*tileSize, y*tileSize, tileSize, tileSize);

CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);

//convert to jpeg data

NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];

NSData *data = [imageRep representationUsingType: NSJPEGFileType properties:nil];

CGImageRelease(tileImage);

//save file

NSString *path = [outputPath stringByAppendingFormat: @“_%02i_%02i.jpg”, x, y];

[data writeToFile:path atomically:NO];

}

}

}

return 0;

}

复制代码

这个程序将2048*2048分辨率的雪人图案裁剪成了64个不同的256*256的小图。(256*256是CATiledLayer的默认小图大小,默认大小可以通过tileSize属性更改)。程序接受一个图片路径作为命令行的第一个参数。我们可以在编译的scheme将路径参数硬编码然后就可以在Xcode中运行了,但是以后作用在另一个图片上就不方便了。所以,我们编译了这个程序并把它保存到敏感的地方,然后从终端调用,如下面所示:

> path/to/TileCutterApp path/to/Snowman.jpg

The app is very basic, but could easily be extended to support additional arguments such as tile size, or to export images in formats other than JPEG. The result of running it is a sequence of 64 new images, named as follows:

这个程序相当基础,但是能够轻易地扩展支持额外的参数比如小图大小,或者导出格式等等。运行结果是64个新图的序列,如下面命名:

Snowman_00_00.jpg

Snowman_00_01.jpg

Snowman_00_02.jpg

Snowman_07_07.jpg

既然我们有了裁切后的小图,我们就要让iOS程序用到他们。CATiledLayer很好地和UIScrollView集成在一起。除了设置图层和滑动视图边界以适配整个图片大小,我们真正要做的就是实现-drawLayer:inContext:方法,当需要载入新的小图时,CATiledLayer就会调用到这个方法。

清单6.12演示了代码。图6.12是代码运行结果。

清单6.12 一个简单的滚动CATiledLayer实现

复制代码

#import “ViewController.h”

#import <QuartzCore/QuartzCore.h>

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//add the tiled layer

CATiledLayer *tileLayer = [CATiledLayer layer];

tileLayer.frame = CGRectMake(0, 0, 2048, 2048);

tileLayer.delegate = self; [self.scrollView.layer addSublayer:tileLayer];

//configure the scroll view

self.scrollView.contentSize = tileLayer.frame.size;

//draw layer

[tileLayer setNeedsDisplay];

}

- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx

{

//determine tile coordinate

CGRect bounds = CGContextGetClipBoundingBox(ctx);

NSInteger x = floor(bounds.origin.x / layer.tileSize.width);

NSInteger y = floor(bounds.origin.y / layer.tileSize.height);

//load tile image

NSString *imageName = [NSString stringWithFormat: @“Snowman_%02i_%02i”, x, y];

NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@“jpg”];

UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];

//draw tile

UIGraphicsPushContext(ctx);

[tileImage drawInRect:bounds];

UIGraphicsPopContext();

}

@end

复制代码

图6.12 用UIScrollView滚动CATiledLayer

当你滑动这个图片,你会发现当CATiledLayer载入小图的时候,他们会淡入到界面中。这是CATiledLayer的默认行为。(你可能已经在iOS 6之前的苹果地图程序中见过这个效果)你可以用fadeDuration属性改变淡入时长或直接禁用掉。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多线程绘制,-drawLayer:inContext:方法可以在多个线程中同时地并发调用,所以请小心谨慎地确保你在这个方法中实现的绘制代码是线程安全的。

Retina小图

你也许已经注意到了这些小图并不是以Retina的分辨率显示的。为了以屏幕的原生分辨率来渲染CATiledLayer,我们需要设置图层的contentsScale来匹配UIScreenscale属性:

tileLayer.contentsScale = [UIScreen mainScreen].scale;

有趣的是,tileSize是以像素为单位,而不是点,所以增大了contentsScale就自动有了默认的小图尺寸(现在它是128*128的点而不是256*256).所以,我们不需要手工更新小图的尺寸或是在Retina分辨率下指定一个不同的小图。我们需要做的是适应小图渲染代码以对应安排scale的变化,然而:

//determine tile coordinate

CGRect bounds = CGContextGetClipBoundingBox(ctx);

CGFloat scale = [UIScreen mainScreen].scale;

NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);

NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

通过这个方法纠正scale也意味着我们的雪人图将以一半的大小渲染在Retina设备上(总尺寸是1024*1024,而不是2048*2048)。这个通常都不会影响到用CATiledLayer正常显示的图片类型(比如照片和地图,他们在设计上就是要支持放大缩小,能够在不同的缩放条件下显示),但是也需要在心里明白。

CAEmitterLayer


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

前15.PNG

前16.PNG

由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容,详细完整版的JavaScript面试题文档,或更多前端资料可以点此处免费获取

像素为单位,而不是点,所以增大了contentsScale就自动有了默认的小图尺寸(现在它是128*128的点而不是256*256).所以,我们不需要手工更新小图的尺寸或是在Retina分辨率下指定一个不同的小图。我们需要做的是适应小图渲染代码以对应安排scale的变化,然而:

//determine tile coordinate

CGRect bounds = CGContextGetClipBoundingBox(ctx);

CGFloat scale = [UIScreen mainScreen].scale;

NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);

NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

通过这个方法纠正scale也意味着我们的雪人图将以一半的大小渲染在Retina设备上(总尺寸是1024*1024,而不是2048*2048)。这个通常都不会影响到用CATiledLayer正常显示的图片类型(比如照片和地图,他们在设计上就是要支持放大缩小,能够在不同的缩放条件下显示),但是也需要在心里明白。

CAEmitterLayer


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-LET79m6O-1712087574898)]

[外链图片转存中…(img-eZ9g972o-1712087574898)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-Om7Rhu0I-1712087574898)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

[外链图片转存中…(img-RzyYErP3-1712087574899)]

[外链图片转存中…(img-dwsquYxw-1712087574899)]

由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容,详细完整版的JavaScript面试题文档,或更多前端资料可以点此处免费获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值