Creating Custom List Skins for the BlackBerry PlayBook

Disclaimer: I’m fairly certain this isn’t best practice. There is a ton of stuff going on with List and CellRenderer that I don’t fully understand. I’m hoping to sit down with the QNX team at some point and figure out how this might be done correctly, but for now I’ve found a way that works so I wanted to share it for anyone who is having issues.

Skinning a List is quite a bit different than skinning something simple like a Button or a TextInput box (which I talked about in my post here). It seems like the correct way to customize the look and feel of a list would be to create a CellRenderer which gives you access to the label for the list, and then swap out the graphics by creating a skin specifically for that CellRenderer. But my particular code didn’t rely on the label field so I skipped the first step and just created a CellRendererSkin that extends UISkin and implements ICellRenderer. By implementing ICellRenderer I get access to the data methods, but as you’ll see, that created some issues.

First off, here’s the list I wanted to create. It’s got an image and a few lines of text. You can see the regular state (white) and what it looks like when it’s selected (grey). I wanted a little bit more of a custom look so I put some space between each item and drew a rounded rectangle with a border around the content.

List Screenshot

To skin a list this way, the most important method is still initializeStates(). That method has to be overridden just like if we were skinning any other component, and it’s where we call setSkinState to associate a graphic with a state. A list has basically 8 different SkinStates: SkinStates.UP, SkinStates.UP_ODD, SkinStates.DOWN, SkinStates.FOCUS, SkinStates.DISABLED, SkinStates.SELECTED, SkinStates.DOWN_SELECTED, and SkinStates.DISABLED_SELECTED. I have no idea what FOCUS does, I’m not alternating my rows so I don’t care about ODD, and I decided I could live with my DOWN, SELECTED, and DOWN_SELECTED being the same. So here’s the code for my initializeStates() method

override protected function initializeStates():void
{
     _upSkin = new Sprite();    
     setSkinState(SkinStates.UP,_upSkin);
 
     _downSkin = new Sprite();    
     setSkinState(SkinStates.DOWN,_downSkin);
     setSkinState(SkinStates.DOWN_SELECTED,_downSkin);
     setSkinState(SkinStates.SELECTED,_downSkin);
 
     SkinStates              
     showSkin(_upSkin);
}

Here’s where things get a bit messier. With normal skinning, I could just start adding graphics to my sprites and then set the skin state accordingly. But what I found was that when the initializeStates() method got called, the width and height of the component hadn’t been set yet. So when I tried to draw a rectangle that used the height/width of the component, it would look scrunched. If you know the exact dimensions you want, you can just hard-code the values. But I wanted to be able to use this on different sized lists, so I wanted those dynamic values.

What I found was that if I overrode the setState(state:String) method, I could get the values for width/height there and then draw the correct graphics depending on whatever state was being set based on the height/width of each cell.

override protected function setState(state:String):void
{
     super.setState(state);
 
     var matrix:Matrix = new Matrix();
     matrix.createGradientBox(width,height,90/180*Math.PI);
 
     if(state == SkinStates.UP)
     {                   
          _background.graphics.clear();
          _background.graphics.beginGradientFill(GradientType.LINEAR,
                    [0xffffff,0xf2f2f2,0xffffff],[1,1,1],[0,127,255],matrix);
          _background.graphics.lineStyle(2,0x221206);
          _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
          _background.graphics.endFill();
     }
     if(state == SkinStates.DOWN ||
          state == SkinStates.DOWN_SELECTED ||
          state == SkinStates.SELECTED)
     {                        
          _background.graphics.clear();
          _background.graphics.beginGradientFill(GradientType.LINEAR,
                    [0xaaaaaa,0xcfcfcf,0xaaaaaa],[1,1,1],[0,127,255],matrix);
          _background.graphics.lineStyle(2,0x221206);
          _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
          _background.graphics.endFill();
     }              
}

Here I ran into another odd issue. Notice that I’m not adding the graphics to the state, but rather adding them to a _background Sprite. What I found is that when I would try to draw the graphics right on the skin sprite it would either overwrite my other content when the state changed and I couldn’t get it back, or the graphics wouldn’t display at all. I’m still not entirely sure why those two things happened and I went through so many iterations that I don’t remember the code that caused it. But what I found was that if I created a background Sprite and added that to the display list first, I could alter it depending on the state and it would draw correctly. That’s also why I have a _background.graphics.clear() call because the _background Sprite is in every state, it just needs to be redrawn when the state changes.

The next step was to add everything to the cell renderer and then to clean it up when the cell renderer goes away. All of the QNX components have an onAdded() and onRemoved() method that gets called when the object is added to or removed from the stage. So I just overrode those methods and added my content.

override protected function onAdded():void
{
     super.onAdded();
     addChild(_background);
     addChild(_image);
     addChild(_name);
     addChild(_brewery);
     addChild(_beerType);
     addChild(_ratingText);
     addChild(_avgRating);
}
 
override protected function onRemoved():void
{
     super.onRemoved();
     removeChild(_background);
     removeChild(_image);
     removeChild(_name);
     removeChild(_brewery);
     removeChild(_beerType);
     removeChild(_ratingText);
     removeChild(_avgRating);
}

There’s just one final step. As you’ll see in my code below, I set up most of the properties of the labels and the image in the constructor. But I don’t set any of the dynamic data. That’s because I had a really hard time finding out when I could access the data property of the cell renderer. There isn’t any data on init(), onAdded(), or initializeStates() so trying to set the dynamic data in those methods threw an error. I could access it in the setState() method, but I found that when I tried to set it there, the list wouldn’t display the values correctly. I finally figured out that it was because of the way the list is virtualized. The setState() method doesn’t get called when you initially scroll the list because the state hasn’t changed, just the data has. So I was seeing the values repeat when I’d scroll the list and didn’t see the correct value until I clicked on it and forced setState() to be called.

The solution was just to embrace the fact that I was implementing ICellRenderer and set all of the data in the data setter method. That set the data correctly for each cell and didn’t depend on the state at all. I also had to set the width of the Label objects there so that the text wouldn’t be cut off.

public function set data(data:Object):void
{
     _beer = data;
 
     _image.setImage(data.thumb);
 
     _name.text = data.beerName;
     _name.width = width-150;
 
     _brewery.text = data.brewerName;
     _brewery.width = width-150;
 
     _beerType.text = data.styleName;
     _beerType.width = width-150;
 
     if(data.avgRating > 3)
     {
          _avgRatingFormat.color = 0x4c9d17;
     } else if (data.avgRating < 1)
     {
          _avgRatingFormat.color = 0x9d1717;
     }
 
     _avgRating.text = data.avgRating;
     _avgRating.format = _avgRatingFormat;
}

Again, I want to stress that this probably isn’t the ideal way to do this. Especially if you have a pretty basic label you could just extend CellRenderer and override the draw() and drawLabel() method to do what you want. Or, like I said above, apply a special skin to that CellRenderer that handles all of the states for the list correctly. This was the rabbit hole that I went down though, and I found it to be kind of handy because I killed all of my birds with one class. Even if it’s an ugly class. Here’s the full code.

package com.pintley.components.listClasses
{
     import flash.display.GradientType;
     import flash.display.Sprite;
     import flash.filters.DropShadowFilter;
     import flash.geom.Matrix;
     import flash.text.TextFormat;
 
     import qnx.ui.display.Image;
     import qnx.ui.listClasses.ICellRenderer;
     import qnx.ui.skins.SkinStates;
     import qnx.ui.skins.UISkin;
     import qnx.ui.text.Label;
 
     public class BeerCellRendererSkin extends UISkin implements ICellRenderer
     {
          protected var _beer:Object;
          private var _row:int;
          private var _column:int;
          private var _section:int;
          private var _index:int;    
 
          private var _yOffset:int = 12;
          private var _xOffset:int = 35;
 
          /**
           * Skins
           **/
          protected var _upSkin:Sprite;
          protected var _selectedSkin:Sprite;
          protected var _downSkin:Sprite;
          protected var _disabledSkin:Sprite;
 
          /**
           * Cell Renderer content
           **/
          // I use a background sprite because I want to
          // make sure it's the lowest layer. Then I can just
          // modify the lowest layer without overwriting the text.
          protected var _background:Sprite;
          protected var _name:Label;
          protected var _brewery:Label;
          protected var _beerType:Label;
          protected var _ratingText:Label;
          protected var _avgRating:Label;
          protected var _image:Image;
 
          /**
           * TextFormats
           **/
          protected var _nameFormat:TextFormat;
          protected var _breweryFormat:TextFormat;
          protected var _beerTypeFormat:TextFormat;
          protected var _ratingTextFormat:TextFormat;
          protected var _avgRatingFormat:TextFormat;
 
          public function BeerCellRendererSkin()
          {
               super();
 
               /**
                * TextFormats
                **/
               _nameFormat = new TextFormat();
               _nameFormat.color = 0xbd5251;
               _nameFormat.size = 16;
               _nameFormat.bold = true;
 
               _breweryFormat = new TextFormat();
               _breweryFormat.color = 0x525252;
               _breweryFormat.size = 14;
               _breweryFormat.bold = true;
 
               _beerTypeFormat = new TextFormat();
               _beerTypeFormat.color = 0x525252;
               _beerTypeFormat.size = 14;
 
               _ratingTextFormat = new TextFormat();
               _ratingTextFormat.color = 0x79523e;
               _ratingTextFormat.size = 12;
               _ratingTextFormat.bold = true;
 
               _avgRatingFormat = new TextFormat();
               _avgRatingFormat.color = 0x000000;
               _avgRatingFormat.size = 14;
               _avgRatingFormat.bold = true;
 
               /**
                * CellRenderer Content
                **/
               _image = new Image();
               _image.x = _xOffset;
               _image.y = 20;
               _image.filters = [new DropShadowFilter(3,45,0x000000,.5,4,4,.5)];
 
               _name = new Label();
               _name.x = _xOffset+80;
               _name.y = _yOffset;
               _name.format = _nameFormat;
 
               _brewery = new Label();
               _brewery.x = _xOffset+80;
               _brewery.y = _yOffset+20;
               _brewery.format = _breweryFormat;
 
               _beerType = new Label();
               _beerType.x = _xOffset+80;
               _beerType.y = _yOffset+35;
               _beerType.format = _beerTypeFormat;
 
               _ratingText = new Label();
               _ratingText.x = _xOffset+80;
               _ratingText.y = _yOffset+55;
               _ratingText.format = _ratingTextFormat;
               _ratingText.text = "AVG Rating";
 
               _avgRating = new Label();
               _avgRating.x = _xOffset + 150;
               _avgRating.y = _yOffset+54;
 
               _background = new Sprite();
          }
 
 
 
          /**
           * Getters/Setters
           **/
 
          public function get data():Object
          {
               return _beer;
          }
 
          public function set data(data:Object):void
          {
               _beer = data;
 
               // Set the text and images for the
               // label after we get data from the list.
               _image.setImage(data.thumb);
 
               _name.text = data.beerName;
               _name.width = width-150;
 
               _brewery.text = data.brewerName;
               _brewery.width = width-150;
 
               _beerType.text = data.styleName;
               _beerType.width = width-150;
 
               if(data.avgRating > 3)
               {
                    _avgRatingFormat.color = 0x4c9d17;
               } else if (data.avgRating < 1)
               {
                    _avgRatingFormat.color = 0x9d1717;
               }
 
               _avgRating.text = data.avgRating;
               _avgRating.format = _avgRatingFormat;
          }
 
          public function get index():int
          {
               return _index;
          }
 
          public function set index(value:int):void
          {
               _index = value;
          }
 
          public function get row():int
          {
               return _row;
          }
 
          public function set row(value:int):void
          {
               _row = value;
          }
 
          public function get column():int
          {
               return _column;
          }
 
          public function set column(value:int):void
          {
               _column = value;
          }
 
          public function get section():int
          {
               return _section;
          }
 
          public function set section(section:int):void
          {
               _section = section;
          }
 
          public function get isHeader():Boolean
          {
               return false;
          }
 
          /**
           * Overriden Functions
           */
 
          override protected function initializeStates():void
          {
               // Set up the skin states
               _upSkin = new Sprite();    
               setSkinState(SkinStates.UP,_upSkin);
 
               _downSkin = new Sprite();    
               setSkinState(SkinStates.DOWN,_downSkin);
               setSkinState(SkinStates.DOWN_SELECTED,_downSkin);
               setSkinState(SkinStates.SELECTED,_downSkin);
 
               showSkin(_upSkin);
          }
 
          override protected function setState(state:String):void
          {
               super.setState(state);
 
               var matrix:Matrix = new Matrix();
               matrix.createGradientBox(width,height,90/180*Math.PI);
 
               // Check to see what state is being set and then draw
               // the graphics on the background Sprite accordingly.
               if(state == SkinStates.UP)
               {                   
                    _background.graphics.clear();
                    _background.graphics.beginGradientFill(GradientType.LINEAR,
                              [0xffffff,0xf2f2f2,0xffffff],[1,1,1],[0,127,255],matrix);
                    _background.graphics.lineStyle(2,0x221206);
                    _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
                    _background.graphics.endFill();
               }
               if(state == SkinStates.DOWN ||
                    state == SkinStates.DOWN_SELECTED ||
                    state == SkinStates.SELECTED)
               {                        
                    _background.graphics.clear();
                    _background.graphics.beginGradientFill(GradientType.LINEAR,
                              [0xaaaaaa,0xcfcfcf,0xaaaaaa],[1,1,1],[0,127,255],matrix);
                    _background.graphics.lineStyle(2,0x221206);
                    _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
                    _background.graphics.endFill();
               }              
          }
 
          override protected function onAdded():void
          {
               super.onAdded();
               addChild(_background);
               addChild(_image);
               addChild(_name);
               addChild(_brewery);
               addChild(_beerType);
               addChild(_ratingText);
               addChild(_avgRating);
          }
 
          override protected function onRemoved():void
          {
               super.onRemoved();
               removeChild(_background);
               removeChild(_image);
               removeChild(_name);
               removeChild(_brewery);
               removeChild(_beerType);
               removeChild(_ratingText);
               removeChild(_avgRating);
          }
     }
}

Setting Custom Labels on Lists with the PlayBook

By default, it seems like the only property that the QNX List class can display as a label is label. In Flex I was used to being able to set my own label property or my own custom label function but it doesn’t look like that’s possible in the QNX List. Luckily, it’s not TOO hard to set up a class to do just that.

First step is to create a class that extends CellRenderer. There’s a lot that seems to go on in CellRenderer but I’m just starting to dig into it so I’ll save that for a later post. The easiest method I found was to override the setter for data and then use the setLabel function there. You can pass in whatever string you want to that setLabel function and it will display as the default label with the default font.

One of the issues I’m having is trying to figure out exactly when the data gets set on the CellRenderer component. Some of the other things I tried (setting it in the constructor, setting it on the drawLabel method) all didn’t work because the data object was still null. So I’m trying to figure out exactly when data becomes available. Hopefully I’ll have that for a followup post that goes into more detail on creating custom renderers for the list class.

Here’s the code I used for my example:

package com.pintley.components.listClasses
{
import qnx.ui.core.SizeUnit;
import qnx.ui.listClasses.CellRenderer;
import qnx.ui.text.Label;
 
public class BeerItemRenderer extends CellRenderer
{
     private var _beer:Object;
 
     public function BeerItemRenderer()
     {
          super();
     }
 
     override public function set data(value:Object):void
     {
          _beer = value;
          setLabel(_beer.beerName);
     }
}
}

Getting Started with the BlackBerry PlayBook and Adobe AIR

As I’ve mentioned, I’m excited about the fact that RIM has wholeheartedly embraced AIR for their PlayBook. That means if you’re a Flash or Flex developer you’re going to be able to easily target this device along with all of the other phones and tablets that support AIR.

And there is no reason you can’t get started right away. RIM just released a new version of the AIR SDK that you can use along with their simulator to get stared building apps. I did a quick tutorial embedded below that shows the steps needed to get going. It’s pretty easy to get a jump and the emulator gives you a feel for how your application will behave on the device.

I’ll be talking a lot more about development on the PlayBook over the next couple of months and I’ve got a few projects that will be specifically targeting the 7″ form factor so I’ll be talking about those and showing some best practices.

RIM’s PlayBook and Adobe AIR

Yesterday RIM announced their tablet computer, the PlayBook, with an impressive set of specs and what looks like a great form factor. But what I thought was the coolest part of the announcement is that Adobe AIR is going to play a central role in application development on the tablet. While there will be support for Java eventually and developers can use C++ to tie into things like OpenGL, Adobe AIR is the primary way to develop applications for the tablet.

In fact, a lot of the work on the tablet is already being done in AIR. The browser, the application launcher, and a lot of the default applications have been built using Adobe AIR. One of the cool things is that this is just one step in a long evolution of Flash. The company behind the tablet OS, QNX, was acquired by RIM back in April. QNX has long been a partner of Adobe and is one of the experts on porting Flash to different pieces of hardware. So they’ve got a ton of experience working in the guts of Flash and it sounds like a lot of that knowledge went into the AIR integration on the PlayBook.

So if you’re building AIR applications then this is one more place you’ll be able to bring those skills. You can get a jump on development by heading over to the labs page we have set up for the BlackBerry and of course, there will be a lot more good stuff at MAX, so you’ll want keep an eye on news coming out of LA for the latest.

Adobe and RIM Collaborating on Tool Support for BlackBerry Devices

At the developer conference in San Francisco today, RIM and Adobe announced a collaboration around creating content for BlackBerry devices and Adobe’s Creative Suite tools. This builds off of the momentum we started with RIM when they announced they were joining the Open Screen Project and dedicated to bringing Flash Player to BlackBerry. There are some good links on Techmeme that cover the announcement pretty well.

rim_adobe_announcement

Creating Content with Adobe Tools

Adobe is known for first class design and development tools and today’s announcement means that you’ll be able to use those tools to target RIM’s devices. There are going to be multiple points of integration. One of the critical pieces of creating mobile content is to make sure it is optimized for the smaller screens and often less bandwidth. In Creative Suite 5 we’re going to support optimized graphic and video content from Adobe Photoshop, Adobe After Effects, and Adobe Illustrator. We’re also supporting a seamless workflow between those design tools and BlackBerry’s developer tools including the BlackBerry Web Plug-in for Eclipse and the BlackBerry Java Plug-in for Eclipse.

More interestingly for developers, we’re going to be working closely with RIM to enable full support for BlackBerry devices in Creative Suite Design Central, Dreamweaver, and Fireworks. You’ll be able to use those three tools to test and create content for BlackBerry’s mobile browser as well as to create widgets directly on the BlackBerry device. Device Central is a fantastic way to test both HTML and Flash content for specific mobile devices. It lets you tweak battery settings, screen sizes, and other phone-specific functionality. Now we’ll have support for most of the BlackBerry phones so you never have to leave Creative Suite to see exactly how something will look on the phone.

Lastly, on the application front, Adobe is to be working on applications for BlackBerry that will let users take rich media and image content from the phone and quickly and easily bring it into tools like Photoshop Elements and Photoshop.com so it can be edited and modified.

BlackBerry Momentum

My colleague Mark Doherty has some great stats on what the BlackBerry market looks like and what this collaboration will mean for people who want to use their existing skills with Adobe’s tools to create mobile content for BlackBerry. Seeing the level of cooperation between Adobe and RIM is an exciting thing for designers and developers. Unlike some companies, I think RIM sees the value in partnerships, and with the breadth of Adobe tools it means they’re able to leverage our community for all kinds of different content- not just Flash.

Next year is going to be incredibly exciting for Adobe developers and designers. We’ve already talked a lot about Flash Player being available for smart phones next year, you’ll undoubtedly be hearing more about AIR, and hopefully we’ll continue to see deeper mobile integration across all of our tools just like you’re seeing with RIM here today. For more information you can check out the BlackBerry portal on Adobe’s site to get the scoop on the details and see some of the workflows in action.