• Increase font size
  • Default font size
  • Decrease font size
Home Article Sections Cappuccino Views for Layouts 5-View Liquid

5-View Liquid

E-mail Print PDF

It's very common to have our web pages broken up into distinct regions (header, footer, navigation bars, etc.)  You can duplicate this type of behavior in Cappuccino using the CPView class.  In this article we'll build a 5-region page using CPViews where each region is tied to its window location.

First Pass

Take a look at the image to the right.  Each region is color coded so we can see them and the layout is "liquid" in the sense that the regions are "glued" to their respective window positions (e.g. the yellow right-hand side bar stays stuck to the right-hand side of the window) even as the window is resized.  Positioning was discussed in the official Automatic Layout Support tutorial, but we'll hopefully extend the concepts a bit.

CPViews offer a nice method for grouping user interface objects together and allows us to manipulate them all together to, for instance, hide them all or move them to a new location.  We'll create 5 CPView objects, set their colors, and attach them to the desired window locations.  Let's just jump in.  Here's the code (it's also attached as "5ViewLiquid-1.zip"):

@import <Foundation/CPObject.j>

@implementation AppController : CPObject
{
}

- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
  var theWindow   = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
  var contentView = [theWindow contentView];
  var bounds      = [contentView bounds];

  // All views will have this width or height
  var viewSize = 50;

  var topView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CPRectGetWidth(bounds), viewSize)];
  [topView setBackgroundColor:[CPColor redColor]];
  [topView setAutoresizingMask:CPViewWidthSizable];
  [contentView addSubview:topView];

  var bottomView = [[CPView alloc] initWithFrame:CGRectMake(0, CPRectGetHeight(bounds)-viewSize, CPRectGetWidth(bounds), viewSize)];
  [bottomView setBackgroundColor:[CPColor greenColor]];
  [bottomView setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
  [contentView addSubview:bottomView];

  var leftView = [[CPView alloc] initWithFrame:CGRectMake(0, viewSize, viewSize, CPRectGetHeight(bounds)-2*viewSize)];
  [leftView setBackgroundColor:[CPColor blueColor]];
  [leftView setAutoresizingMask:CPViewHeightSizable];
  [contentView addSubview:leftView];

  var rightView = [[CPView alloc] initWithFrame:CGRectMake(CPRectGetWidth(bounds)-viewSize, viewSize, viewSize, CPRectGetHeight(bounds)-2*viewSize)];
  [rightView setBackgroundColor:[CPColor yellowColor]];
  [rightView setAutoresizingMask:CPViewHeightSizable | CPViewMinXMargin];
  [contentView addSubview:rightView];
  
  var centerView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CPRectGetWidth(bounds)-2*viewSize, CPRectGetHeight(bounds)-2*viewSize)];
  [centerView setCenter:CGPointMake(CPRectGetWidth(bounds)/2, CPRectGetHeight(bounds)/2)];
  [centerView setBackgroundColor:[CPColor shadowColor]];
  [centerView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
  [contentView addSubview:centerView];

  [theWindow orderFront:self];
}
@end

This is actually surprisingly simple.  Let's take a look at the creation of the "header" section (lines 16 through 19).  This just creates the CPView for the header, calling it "topView".  Note the frame used in its initialization (line 16).  Its location is set to (0,0), its width is set to CPRectGetWidth(bounds) which is the width of the window, and its height is set to viewSize (the size we want to make everything).  Simple.  This gives us our section at the top, all that's left is to force it to resize with the window.  We do that in line 18 where we set its resizing mask to "width resizable".  The other views are nearly as straightforward.  The right view, for example, is only complicated by the fact that its height is the window height minus the height of the header and footer (2*viewSize).  To keep the right view stuck to the right-hand side of the window, we set the resizing mask to "CPViewHeightSizable | CPViewMinXMargin" which indicates the hight resizes with the window but that the area to the left of the view stretches (MinXMargin) rather than the view width.  Pretty simple actually.

Second Pass

Of course we can't leave well enough alone.  All good web pages have padding around the various sections but ours bump right up against each other.  Let's fix that.  Since the changes aren't that extensive there's no point in listing the entire AppController file here so grab it from the attachments.  It's in "5ViewLiquid-2.zip".

So what changes?  Here's an excerpt

  var viewSize = 50;
  var pad      = 3;

  topView = [[CPView alloc] initWithFrame:CGRectMake(pad, pad, 
                                                                                                 CPRectGetWidth(bounds)-2*pad, viewSize)];
  [topView setBackgroundColor:[CPColor redColor]];
  [topView setAutoresizingMask:CPViewWidthSizable];
  [contentView addSubview:topView];

  bottomView = [[CPView alloc] initWithFrame:CGRectMake(pad, CPRectGetHeight(bounds)-viewSize-pad, 
                                                CPRectGetWidth(bounds)-2*pad, viewSize)];
  [bottomView setBackgroundColor:[CPColor greenColor]];
  [bottomView setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
  [contentView addSubview:bottomView];

  leftView = [[CPView alloc] initWithFrame:CGRectMake(pad, viewSize+3*pad, 
                                          viewSize, CPRectGetHeight(bounds)-2*viewSize-6*pad)];
  [leftView setBackgroundColor:[CPColor blueColor]];
  [leftView setAutoresizingMask:CPViewHeightSizable];
  [contentView addSubview:leftView];

  rightView = [[CPView alloc] initWithFrame:CGRectMake(CPRectGetWidth(bounds)-viewSize-pad, viewSize+3*pad, 
                                                viewSize, CPRectGetHeight(bounds)-2*viewSize-6*pad)];
  [rightView setBackgroundColor:[CPColor yellowColor]];
  [rightView setAutoresizingMask:CPViewHeightSizable | CPViewMinXMargin];
  [contentView addSubview:rightView];

Well, more math.  We've defined a variable "pad" which is the number of pixels we want around each view (all sides of it).  Now we have the additional burden of calculating the offsets of each region.  For instance the topView now starts at the point (pad,pad) rather than (0,0), and its width is CPRectGetWidth(bounds)-2*pad (twice the pad value since we have padding on both left and right).  The right view is even worse with its height being adjusted by 2 views (top and bottom) and 6 pads (2 from the top, two from the bottom, and 2 from the right view).  Well, OK, it's not rocket science but you do have to count and/or play a bit and it more confusing when they all have different sizes.  Can we make this a little easier?  Maybe a bit.

Custom View Class

The CPView class doesn't support borders automatically so we'll have to build them by hand.  We can clean up the code a tiny bit by declaring our own CPView class (which we'll call the incredibly creative MyView).  Grab the code from the attachments in "5ViewLiquid-3.zip" which contains both the AppController.j and MyView.j.

Before the creation of, for instance, the right-hand view had a bunch of math computing offset and paddings.  It looked like this:

  rightView = [[CPView alloc] initWithFrame:CGRectMake(CPRectGetWidth(bounds)-viewSize-pad, viewSize+3*pad, 
                                                     viewSize, CPRectGetHeight(bounds)-2*viewSize-6*pad)];
  [rightView setBackgroundColor:[CPColor yellowColor]];
  [rightView setAutoresizingMask:CPViewHeightSizable | CPViewMinXMargin];
  [contentView addSubview:rightView];

We have to keep track of all the widths and paddings for all the regions.  Kind of a pain.  Instead let's create the MyView class like this:

@import <Foundation/CPObject.j>

@implementation MyView : CPClipView
{
  CPView internalView;
}

- (id)initWithFrame:(CGRect)aFrame
{
  return [self initWithFrame:aFrame andPad:0]
}


- (id)initWithFrame:(CGRect)aFrame andPad:(int)pad
{
  self = [super initWithFrame:aFrame];

  if (self) {
    var bounds = [self bounds];
    internalView = [[CPView alloc] initWithFrame:CGRectMake(pad, pad, 
                     CPRectGetWidth(bounds)-2*pad, CPRectGetHeight(bounds)-2*pad)]
    [internalView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
    [self addSubview:internalView];
  }
  
  return self;
}

- (CPView)getInternalView
{
  return internalView;
}
@end

What have we done here?  Well, we created a new flavor of view (called MyView) which has an embedded CPView.  When we create a MyView via initWithFrame:andPad: we create the internal view within the MyView region making sure we leave the correct padding.  Unfortunately this isn't a perfect solution.  Why?  Well, we now have one more level of "indirection" to use the view.  Take a look at the code snippet below:

  rightView = [[MyView alloc] initWithFrame:CGRectMake(CPRectGetWidth(bounds)-viewSize, viewSize, viewSize, CPRectGetHeight(bounds)-2*viewSize) andPad:pad];
  [[rightView getInternalView] setBackgroundColor:[CPColor yellowColor]];
  [rightView setAutoresizingMask:CPViewHeightSizable | CPViewMinXMargin];
  [contentView addSubview:rightView];

This is our new rightView creation code.  Notice there's no longer any math to handle the padding (the offsets from other views are still there though) which makes the code a bit easier to handle.  The down side is in line 3.  Whenever we want to use the new view we need to get access to the internal view via "[rightView getInternalView]".  Not a huge issue, but you'll have to decide if it's easier for you to handle the padding offsets directly.  There is one additional upside though.  You can set the background color of the main "container" view to a different value than the internal view.  This effectively makes a nice colored border around the view.  That is, in the code we do this:

[centerView setBackgroundColor:[CPColor orangeColor]];
[[centerView getInternalView] setBackgroundColor:[CPColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0]];

which makes the container orange and the internal frame grey; effectively putting an orange border around the (grey) center view.  Note: make sure you set the alpha to 1.0 on the internal frame color or you'll get "bleed through" of the backing view.

Attachments:
Download this file (5ViewLiquid-1.zip)5ViewLiquid-1.zip[ ]0 Kb
Download this file (5ViewLiquid-2.zip)5ViewLiquid-2.zip[ ]0 Kb
Download this file (5ViewLiquid-3.zip)5ViewLiquid-3.zip[ ]1 Kb
Last Updated ( Tuesday, 10 November 2009 05:42 )  

Design by i-cons.ch / etosha-namibia.ch