您的位置:首页 > 其它

CATiledLayer讲解Part1

2015-11-02 16:23 351 查看
原文地址:http://www.mlsite.net/blog/?p=1857

Let’s take a look at a
CATiledLayer
demo. I first ran across the
CATiledLayer
class
when I was looking into a multithreaded, tiled, vector-graphics rendering solution for the Demineproject.
I didn’t pursue it at that time because it looked like it would be a bit of a job to understand and deploy, and I already had a workable rendering engine based on blitting.
Now, however, I’d like to return to it.

What I’m going to present today (you can download the complete project here)
is very much a work-in-progess. This demo shows how the
CATiledLayer
class
can be made to do certain things, but it doesn’t address (at least) two very important problems: how to zoom, and how to handle the hazards of multithreading. I’ll talk briefly about both, but a thorough discussion will have to wait for another day. Now, without
further preamble, let’s get started!


Project Setup

I’m going to begin with the vector graphics demo I did while developing Demine. Rather than recapitulate everything in that demo, I’m just going to incorporate it by reference; anything important that isn’t covered here is probably discussed in the earlier
article, which I encourage you to consult.

Let’s get started by grabbing the earlier project.
The first thing to do, since you’re probably building against SDK 4.0, is to update some project settings. (AAPL got very pushy about linking against the most recent SDK with 4.0.) First, open up the Project->Edit Project Settings dialog, select the “General”
tab, and set the “Base SDK for All Configurations” to “iPhone Device 4.0″. Next, switch to the “Build” tab, check that the “Configuration” drop-down is set to “All Configurations”, and set the “iPhone OS Deployment Target” (in the “Deployment” section) to
“iPhone OS 3.0″. (This will enable your app to run on iOS 3.0, if, like me, you haven’t upgraded your device yet.) Build and run the project, just to ensure the old stuff still works.


Tiled View

Now we’re going to add a tiled view class. Use Xcode’s “New File” feature to add a
UIView
subclass
called
TiledView
to the project. Add this method to the automatically
generated implementation file:
+ (Class)layerClass
{
return [CATiledLayer class];
}


This class method is the “magic” that customizes the layer inside instances of this class. Unfortunately, the reference to
CATiledLayer
requires
you both to
#import <QuartzCore/QuartzCore.h>
, and to add the
QuartzCore
framework
to the project. Make those changes, then rebuild to check that everything compiles.


Delegate Method

A
UIView
is automatically set as the
delegate
of
its
layer
, and the key method of a
CATiledLayer's
delegate
is
drawLayer:inContext:

let’s add one to our new class:
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
// Fetch clip box in *view* space; context's CTM is preconfigured for view space->tile space transform
CGRect box = CGContextGetClipBoundingBox(context);

// Calculate tile index
CGFloat contentsScale = [layer respondsToSelector:@selector(contentsScale)]?[layer contentsScale]:1.0;
CGSize tileSize = [(CATiledLayer*)layer tileSize];
CGFloat x = box.origin.x * contentsScale / tileSize.width;
CGFloat y = box.origin.y * contentsScale / tileSize.height;
CGPoint tile = CGPointMake(x, y);

// Clear background
CGContextSetFillColorWithColor(context, [[UIColor grayColor] CGColor]);
CGContextFillRect(context, box);

// Rendering the paths
CGContextSaveGState(context);
CGContextConcatCTM(context, [self transformForTile:tile]);
NSArray* pathGroups = [self pathGroupsForTile:tile];
for (PathGroup* pg in pathGroups)
{
CGContextSaveGState(context);

CGContextConcatCTM(context, pg.modelTransform);

for (Path* p in pg.paths)
{
[p renderToContext:context];
}

CGContextRestoreGState(context);
}
CGContextRestoreGState(context);

// Render label (Setup)
UIFont* font = [UIFont fontWithName:@"CourierNewPS-BoldMT" size:16];
CGContextSelectFont(context, [[font fontName] cStringUsingEncoding:NSASCIIStringEncoding], [font pointSize], kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1, -1));
CGContextSetFillColorWithColor(context, [[UIColor greenColor] CGColor]);

// Draw label
NSString* s = [NSString stringWithFormat:@"(%.1f, %.1f)",x,y];
CGContextShowTextAtPoint(context,
box.origin.x,
box.origin.y + [font pointSize],
[s cStringUsingEncoding:NSMacOSRomanStringEncoding],
[s lengthOfBytesUsingEncoding:NSMacOSRomanStringEncoding]);
}


A few quick remarks about this method:

This method can be divided into 3 basic sections:

Computing the tile index

Rendering the tile

Rendering the label

Tile index computation is only presented as a placeholder; the index is used in the label and passed to some other functions, but never used for anything important. (More on this below.)

The
layer's
tileSize
is
specified in pixels; this means that tiles will have different view space sizes on normal vs. high-resolution devices.

The
layer's
contentsScale
property
defines the relationship between view space points and backing store pixels. This property does not exist in runtime environments prior to iOS 4.0; in such environments it is always implicitly equal to 1.

The actual tile rendering code is essentially the same as that in the
drawRect:
method
of the
TileView
class that we’re replacing.

The label is rendered for debugging purposes only.

This
drawLayer:inContext:
method invokes (and assumes the existence
of) two instance methods:

- (CGAffineTransform)transformForTile:(CGPoint)tile


- (NSArray*)pathGroupsForTile:(CGPoint)tile


which we will now add to
TiledView
.


Extension Methods

The
transformForTile:
method is really a holdover from the earlier
implementation, which required us to generate a world space -> tile space transform for each tile. Since the
contexts
passed
to
drawLayer:inContext:
are pre-configured with a view space ->
tile space transform, we only need to provide an additional world space -> view space transform, which doesn’t vary by tile. Which is all by way of saying that this function doesn’t make a lot of sense, and will almost certainly disappear in future versions
of this demo.

Furthermore, since we’re not supporting zoom in this demo (despite its name!), this function doesn’t even do anything interesting; in the absence of zoom, the world space -> view space transform is just the identity:
- (CGAffineTransform)transformForTile:(CGPoint)tile
{
return CGAffineTransformIdentity;
}


The
pathGroupsForTile:
method would normally return the set of
PathGroups
that
overlap a particular tile. As before, however, we’re just going to return the same set of
PathGroups
for
every tile, and rely on clipping to do the right thing. The following function is a very slight re-write of the preexisting
pathGroupsForTileView:
method:
- (NSArray*)pathGroupsForTile:(CGPoint)tile
{
CGMutablePathRef	p;
Path*			path;
PathGroup*		pg = [[PathGroup new] autorelease];

// Path 1
path = [[Path new] autorelease];
p = CGPathCreateMutable();
CGPathAddEllipseInRect(p, NULL, CGRectMake(-95, -95, 190, 190));
path.path = p;
path.strokeWidth = 10;
CGPathRelease(p);
[pg.paths addObject:path];
// Path 2
path = [[Path new] autorelease];
p = CGPathCreateMutable();
CGPathAddRect(p, NULL, CGRectMake(-69.5, -51.5, 139, 103));
path.path = p;
path.strokeWidth = 5;
CGPathRelease(p);
[pg.paths addObject:path];

// Center it, at some reasonable fraction of the world size
// * The TiledView's bounds area always equals the size of view space
// * With no scaling, the size of view space equals the size of world space
// * The bounding box of the preceeding model is 200x200, centered about 0
CGFloat scaleToFit = self.bounds.size.width*.25/200.0;
CGAffineTransform s = CGAffineTransformMakeScale(scaleToFit, scaleToFit);
CGAffineTransform t = CGAffineTransformMakeTranslation(self.bounds.size.width/2.0, self.bounds.size.height/2.0);
pg.modelTransform =  CGAffineTransformConcat(s, t);

return [NSArray arrayWithObject:pg];
}


Note that I chose to identify the tile with a
CGPoint
, representing
a 2D index. This probably isn’t such a great choice, since, as mentioned previously, tiles can have different view space sizes depending on device resolution. It doesn’t matter right now (since I ignore the information anyway) but this is another thing that
will probably change in future versions of this demo.


Code Swap

Okay, now to swap out our old
TileView
approach for one based on
TiledView
.
(There’s no need to thank me for that naming convention, although I appreciate your kind thoughts.)

The swap is pretty straight-forward. First, open
zoomdemoViewController.xib
,
select the
UIView
inside the
UIScrollView
,
open the Identity Inspector, and change its Class Identity to
TiledView
.
Next, remove a ton of stuff from the
zoomdemoViewController
class:

Delete these members (and references to them):

tiles


extraTiles


tileBox


Delete these constants:

tileSize


labelTag


Delete these extension methods:

removeTiles


createTiles


addTileForFrame:


reload


Delete all
UIScrollView
delegate methods:

scrollViewDidScroll:


viewForZoomingInScrollView:


scrollViewDidEndZooming:withView:atScale:


Delete all
TileView
delegate methods (and, while you’re at it,
drop the
TileViewDelgate
protocol, and toss the
TileView
files
out of the project:

transformForTileView:


pathGroupsForTileView:


Add this line to the end of the
sizeContent
method:
[self.content.layer setNeedsDisplay];


This line is a little peculiar. Without it, the
TiledView
doesn’t
display anything at all when I run the demo on my actual device (iPhone 3G running iOS 3.1.3); in the the simulator (iOS 4.0), on the other hand, the demo works fine without it. Particularly unusual is that the
setNeedsDisplay
message
must be sent to the
layer
; the same message passed to the
content
view
has no effect.

Finally, to create a little visual interest (in the absence of zooming) recode
viewDidLoad
to
look like this:
- (void)viewDidLoad
{
[super viewDidLoad];

world = CGSizeMake(self.scrollView.frame.size.width*4, self.scrollView.frame.size.height*4);
scale = 1.0;

[self sizeContent];
self.scrollView.contentOffset = CGPointMake(1.5*self.scrollView.frame.size.width,
1.5*self.scrollView.frame.size.height);
[self.scrollView flashScrollIndicators];
}


(Don’t sweat the significance of the
world
and
scale
variables;
in the interests of avoiding synchronization problems, they’re not particularly connected to the behavior of the
TiledView
in
this version of the code.)

Build and run the project, and hey presto: multithreaded, tiled, vector-graphics rendering!


Remarks

Okay — that was a lot of ground to cover. I think the result is pretty nice, though: we’ve got a smooth-scrolling, asynchronously rendered view, into which we should be able to stuff arbitrary amounts of vector graphics (hopefully only at the cost of slower
rendering, and not a less-responsive UI) and there’s almost no code required to drive either the scrolling or the tiling. The bulk of the code is spent on simple rendering, and isn’t much more complex than it would be in the absence of the scrolling, tiling,
and asynchronous behavior.

That said, there are a fairly large number of things missing from, and issues unaddressed by, this demo. Together, they make it unready for production use. Briefly:

This code doesn’t support zooming. The
CATiledLayer
class does
have built-in zoom support, but that support seems to be for power-of-two zooming — as opposed to the (nearly) infinite, arbitrary resolution zoom we’ve seen before, and that you’d want to have with vector graphics. (I haven’t explored the zoom support in
detail, though.)

The best part of
CATiledLayer
— its multithreaded nature — is also
it’s most problematic. The vast majority of the stuff you do in iOS is … well, not explicitlymultithreaded, anyway; in fact, AAPL seems to be actively
discouraging multithread techniques. With
CATiledLayer
, you
must be prepared for your
drawLayer:inContext:
method to be called
from (multiple) background threads at any point in your main thread’s execution. This demo avoids synchronization issues because it never accesses mutable shared state from a background thread; this isn’t a viable solution in production.

This particular implementation of
CATiledLayer
will work properly
(i.e., in high-resolution) on high-resolution devices, but this isn’t automatically true for all implementations. A
CATiledLayer
must
be configured to use a high-resolution backing store. Also, since tiling is defined in pixel space, supporting code may need to be aware of the
CATiledLayer's
resolution.

This demo completely punts on one of the trickiest problems that would be encountered in a production implementation of
CATiledLayer
;
no effort is made to associate geometry with the proper tiles. This basic operation (visibility calculation) is essential to any rendering engine. Furthermore, this demo doesn’t provide any way for code outside the
TiledView
to change world
size, world scale, or model geometry.

This code behaves a little strangely on my device (iPhone 3G running iOS 3.1.3). In addition to the previously mentioned
setNeedsDisplay
business,
tiles don’t always properly (i.e. completely) fade-in on startup. I haven’t found any explanation for this latter glitch, nor can I screen-shot it (attempts to do so fix the glitch).


Acknowledgment

I want to acknowledge Bill Dudney’s work on this topic, which helped flesh out AAPL’s rather sparse documentation. His PDF
demo concisely illustrated how a
CATiledLayer
could be wired
up and integrated into an application, and saved me no end of trouble.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: