A Custom Layer Controller

From AwkwardTV
Revision as of 13:55, 17 April 2007 by Alan quatermain (talk | contribs) (Grrrr.... must stop reflexively typing Ctrl-X Ctrl-S)
Jump to: navigation, search

<Google>WIKI</Google>

In our second tutorial we'll pull back a little and cover the BRLayerController class, and what you can do with it. If this isn't your first experience with Back Row plugins, then you're probably already familiar with the BRMenuController or BRMediaMenuController, but those are both fairly specialized subclasses. By creating our own BRLayerController subclasses we can define our own content and layout in a more comprehensive manner.

Along the way we will also take a look at an anciliary class, BRImageManager, which enables us to dynamically download and cache images from the Internet, or indeed from any URL, remote or local, and we will see how to use the BRHeaderControl and BRImageControl classes.

By the end of this tutorial, we will have created a custom controller that will download an image in the background and display it when ready.

Prerequisites

BRLayerController

The BRLayerController class is used to present a significant part of the AppleTV user interface. Examples include the full-screen lists, alerts, and the settings facade. It is also used for items such as preview controllers and parade controllers, which show static or animated content to the left of a list. Specifically, a layer controller differs from a control in that it will handle user input in some intelligent way: where a layer draws content and a control collects layers and handles events, a layer controller is where application-specific logic is contained.

Let's start by examining the interface:

Interface

Setup & Contents

- (BRRenderScene *) scene;
- (BRRenderLayer *) masterLayer;
- (NSRect) masterLayerFrame;
- (void) resetLayout;
- (void) setControls: (NSArray *) controls;
- (void) addControl: (BRControl *) control;
- (NSArray *) controls;

Like the BRControl class, BRLayerControllers have a single master layer to which others are added. Unlike BRControl however, this layer is provided for you by the base class. There is no specific -frame or -setFrame: method here; the controller class is not by definition a UI class, so these operations are performed directly on the master layer, retrieved by calling -masterLayer. As with all controls and layers, however, an explicit reference to the scene is kept by the controller, and is used to redraw the scene when the layer changes.

The -resetLayout method will reset the frame of the master layer according to the -masterLayerFrame function. Both of these can be overridden in subclasses to implement useful behaviour. For instance, a particular subclass might have its masterLayerFrame return the frame of the entire scene (i.e. the entire screen itself), as opposed to the default implementation which will return the frame of the interface area of the scene. By default, the -resetLayout method will simply reset the frame of the master layer. Subclasses can override it to realign their own sublayers according to a potentially changed master frame.

The layer controller also contains a list of controls. While it can have layers added to it directly via [[self masterLayer] addSubLayer: layer], it is generally used to contain controls, which are added to a list, and which have their layers added to the master layer for you. Note that no method exists to remove a control; instead, you must use the following snippet to perform that task:

- (void) removeControl: (BRControl *) aControl
{
    NSMutableArray * controls = [NSMutableArray arrayWithArray: [self controls]];
    [[aControl layer] removeFromSuperlayer];
    [controls removeObject: aControl];
    [self setControls: [NSArray arrayWithArray: controls]];
}

Also note that if the control being removed is retained by the controller subclass outside of the control list, it will need to be released explicitly in addition to the code shown above.

The Controller Stack

- (void) setStack: (BRControllerStack *) stack;
- (BRControllerStack *) stack;

Controllers are placed onto a stack which is used to maintain the display list. Each time you select an item from a menu or click a button which will put up a new interface layer through a controller, then that controller is added to the current stack. When you press the Menu button, the current controller will be popped from the stack, revealing the one underneath.

The functions listed above are used for book-keeping within the controller, but you are unlikely to need to use those explicitly, because the layer controller class defines a number of callbacks used by the BRControllerStack class to notify a layer of impending status changes:

- (void) willBePushed;
- (void) wasPushed;
- (void) willBePopped;
- (void) wasPopped;
- (void) willBeBuried;
- (void) wasBuriedByPushingController: (BRLayerController *) controller;
- (void) willBeExhumed;
- (void) wasExhumedByPoppingController: (BRLayerController *) controller;

Here we see pairs of notifications -- one called in advance of the operation, one called after it has been performed. There is a set for when the controller is being pushed onto the top of the stack (being put onscreen) and when it is being popped (removed before deallocation). There are also items for when the controller is to be buried (another controller pushed on top of this one) and exhumed (controllers on top of this one were removed). Each of these can be overridden to performs specific duties, but in this example we will use only the -wasPushed and -willBePopped methods, which are generally the most useful points at which to perform our operations.

Animations

- (BRAnimation *) popAnimation;
- (BRAnimation *) pushAnimation;

By default, a layer controller offers a fade-to-black animation when popped and a fade-from-black animation when pushed. Subclasses can override these to implement specific effects; for instance the main menu provides a custom animation where the icon zooms up to the header of the incoming menu. We will cover animations in a later tutorial.

Miscellaneous

- (BOOL) brEventAction: (BREvent *) event;
- (void) addLabel: (NSString *) label;
- (void) removeLabel: (NSString *) label;
- (BOOL) isLabelled: (NSString *) label;
- (BOOL) isNetworkDependent;
- (BOOL) popsOnBury;

Layer controllers can handle events directly; this allows them to customize the response to the menu button, or to move focus amongst their controls.

Labels can be used to 'tag' controllers. The BRControllerStack class offers methods based on these, such as fetching a controller with a specific label from the stack (if there), or popping everything above a certain label.

A layer can mark itself as being network dependant, in which case it will be monitored by the framework, so that some action can be taken should the network status change.

It can also choose to be popped automatically if another controller is pushed on top of it, removing itself from the stack immediately.

BRImageManager

The BRImageManager class implements a useful cache for images pulled from the network, along with an asynchronous notification-based loading system for remote images. A number of its methods are based around the iTunes/AppleTV media library format, but there are some which are more generally useful:

- (BOOL) isImageAvailable: (NSString *) name;
- (NSString *) imageNameFromURL: (NSURL *) url;
- (NSString *) writeImageFromURL: (NSURL *) url;
- (void) writeImage: (CGImageRef) image named: (NSString *) name;
- (void) removeImageNamed: (NSString *) name;
- (CGImageRef) imageNamed: (NSString *) name;

Firstly we have a routine to check whether a named image is already present in the cache. If it is, then all well and good. Otherwise, we can use an NSURL object to go fetch the image and cache it. The -imageNameFromURL: routine can be used to glean the name that will be assigned, and the -writeImageFromURL: method will actually go and fetch the image in the background. This method also returns the name assigned to the image, as a convenience.

Images loaded manually can also be committed to the cache using -writeImage:named:, and any image can be removed with -removeImageNamed:.

To retrieve an existing image from the cache, use -imageNamed:.

Asynchronous loading is performed on another thread, and when this is complete a notification is posted on the main thread. The notification name is "BRAssetImageUpdated", it contains a nil object, and has the following items in its userInfo dictionary:

Key Value
BRMediaAssetKey The name of the image that has been updated.
BRImageKey A CGImageRef containing the image itself.

BRHeaderControl

The header control implements the item seen at the top of the menus and sundry other screens in Back Row. It contains a text layer and an image layer, and so can contain both header text and an icon:

- (void) setTitle: (NSString *) title;
- (NSString *) title;
- (void) setIcon: (BRTexture *) texture horizontalOffset: (float) offset kerningFactor: (float) kerning;

The title is obvious enough, but the icon can be slightly problematic, since it doesn't accept a CGImageRef, which is what we would ordinarily use to provide the content of a BRImageControl. Instead, the following code snippet can be used to initialize a texture to use for the icon:

// assume 'iconImage' has been created somewhere, and is valid, and that '_header' is our BRHeaderControl
BRTexture * tex = [BRBitmapTexture textureWithImage: iconImage context: [[self scene] resourceContext] mipmap: YES]
[_header setIcon: tex horizontalOffset: 0.0f, kerningFactor: 0.0f];

BRImageControl

The image control is a little more involved. It contains method for setting its content with a CGImageRef (with optional down-sampling) or with a BRTexture object. It also provides routines to set the amount of reflection of the image, the offset at which reflection occurs, and whether blending is enabled or disabled. Lastly, there are routines to rotate around the y axis (the CoverFlow effect) and to set the bounding size of the image.

- (NSSize) pixelBounds;
- (void) setImage: (CGImageRef) image;
- (void) setImage: (CGImageRef) image downsampleTo: (NSSize) size;
- (void) setTexture:(BRTexture *) texture;
- (BRTexture *) texture;
- (void) setReflectionAmount: (float) amount;
- (float) reflectionAmount;
- (void) setReflectionOffset: (float) offset;
- (float) reflectionOffset;
- (void) setDisableBlending: (BOOL) flag;
- (BOOL) disableBlending;
- (void) setYRotation: (float) rotation;
- (float) yRotation;
- (void) setBoundingSize:(NSSize) size;

These routines are all quite straightforward, although it should be noted that -setBoundingSize: will maintain the aspect ratio of the image itself, so you don't have to.

Putting It All Together

So, we've gone over the basic building blocks, now let's create our custom controller class. We'll have a header cell containing a title, and we will attempt to fetch an image from the internet. When that image arrives, we will change our header title and fade in the image.

The Interface

To start off, let's define our interface. To keep things simple, we'll have everything public (no internal methods in another category).

@interface ATVDelayedImageController : BRLayerController
{
    BRHeaderControl *_header;
    BRImageControl *_image;
    NSString *_imageName;
}

- (id) initWithScene: (BRRenderScene *) scene;
- (void) dealloc;
- (void) wasPushed;
- (void) willBePopped;
- (void) fadeInImage: (CGImageRef) image;
- (void) imageLoaded: (NSNotification *) note;
- (NSURL *) imageURL;

The class contains references to its header (so we can change the title later), the image (so we can update that and animate it), and the name of the image. We keep the name around so that we can compare it against the notification, since it's conceivable that the image manager might post a notification regarding a different image while we're waiting. There is also a method to return our hard-coded image URL.

Initialize/Deallocate

- (id) initWithScene: (BRRenderScene *) scene
{
    if ( [super initWithScene: scene] == nil )
        return ( nil );

    NSImageManager * mgr = [NSImageManager sharedInstance];
    NSURL * imageURL = [NSURL URLWithString: @"20060306.jpg"];
    _imageName = [mgr imageNameFromURL: ];