A Couple of PlayBook-Related Videos on AdobeTV

The PlayBook is on sale today and it really is a beautiful device, especially for Flash developers. Having been able to play with one for a while now I am incredibly impressed with the QNX operating system and the general form factor of the tablet.

I did a couple of videos on the PlayBook for AdobeTV. The first one is on using OAuth with the PlayBook. It should be a good primer for PlayBook developers who want to leverage services like Twitter or Facebook Connect. The second one was a video of one of Renaun’s tips on how to use ImageCache in a QNX list to help improve performance. Both are embedded below.

Building a jQuery Mobile Application with the PlayBook WebWorks SDK

I remain really impressed with the PlayBook as a device. It’s a great size, has fantastic specs, and the user experience that the QNX team has built is fun to use. Primarily I’ve been using the Flash-based AIR SDK for my tests and applications. I think that’s the best way to build PlayBook apps and when I’ve talked to people I’ve gushed that it’s the best mobile Flash development experience we have. But there are also a lot of JS/HTML developers who will want to deploy PlayBook content and RIM has included a WebWorks SDK to make that possible. So I took it for a spin, porting my landmark finder to the device.

Getting Started

The first thing you have to do is grab all of the required files. You first need to make sure you have the AIR 2.5 SDK (which, since we’ve moved to 2.6, can be found here). Then you can grab the WebWorks SDK and the simulator files from the BlackBerry developer site. By default the WebWorks SDK installs to your user directory, so if you can’t find it, look for a folder called bbwp.

Building the Application

There really isn’t any special trick to getting jQuery Mobile working on the PlayBook. It works perfectly and it performs really well. My application just took the core jQuery Mobile framework and included two different screens; one for getting the location and another for a list of landmarks that are close to you. It uses the GPS coordinates from the device and then goes out to Geonames to find a list of landmarks in the area. Selecting one of the landmarks takes you to Wikipedia so you can find out more about it.

The code (above) doesn’t have anything particularly tricky, which is a testament to how easy it use to use the WebWorks SDK if you’re an HTML/JS developer. I was even able to use the standard HTML5 Geolocation AP. RIM hasn’t announced any plans for geolocation on the device so I’m not sure what the final status or experience will be. Today on the simulator you can get a geolocation object but you can’t get position, so I just default to using the lat/lng from my neighborhood here in Seattle.

Creating the Config.xml File

The PlayBook uses a manifest file, config.xml, to control the title, icon, and features on the device. The config.xml file is pretty straightforward and if you’re familiar with app manifest files you should have no issues. There is also some decent documentation here. Mine is below.

There were a couple of gotchas I ran into when I dove into the config.xml file. The first was the access attribute. Instead of asking for blanket access to the web, you have to specify which domains your application will access. In my case it was just the Geonames API. But if you’re pulling down remote source files (say from jQuery) you’ll also need to include that domain.

Compiling the Application

Compiling and deploying the application was a bit tougher. Right now the only tools I could find are command line tools for the compiling, packaging, and deployment of WebWorks applications. The first thing you have to do is zip up all of the files you want to include in the application. The packager tool converts a .zip file into a .bar file, the native file format of the PlayBook. So I selected my config.xml file, my index.html file, and then all of the css and js files from jQuery to create geonames.zip. The tool you use to do that is in the bbwp directory in the root of the WebWorks SDK directory. Here’s the command. You can also specify an output directory with a -o switch. By default it creates a bin directory in the directory you’re in and puts the .bar file there (my WebWorks SDK is in an SDKs folder in my Library).

/Library/SDKs/com.blackberry.dev/bbwp/bbwp geonames.zip

That will build a geonames.bar file in the bin directory of your project.

Deploying to the Simulator

I won’t get into how to set up and configure the simulator because that’s been covered in other places. But once you have the simulator set up, you can test and deploy your application. Within the bbwp directory you’ll find a blackberry-tablet-sdk/tools directory, which has a bunch of libs for deploying and signing your application. To test it on the simulator you’ll use blackberry-deploy. To deploy it you have to have the simulator in development mode and know the IP address of the simulator and the password on the device. With that info you can deploy your .bar file to the simulator with this command.

blackberry-deploy -installApp -device  -package "bin/geonames.bar" -password 

And now you should be all set!.

Word of Caution

This particular example isn’t fantastic for the simulator because it uses the geolocation APIs, which aren’t supported currently on the simulator. But if you’re building a straight jQuery Mobile app (or any HTML/JS app) then these steps will get you compiled and on the device. The next step is to make sure your application is signed and then you can submit it to RIM’s App World and it’ll be ready to use when the PlayBook launches.

Some Great BlackBerry PlayBook Blog Posts

You may have noticed that a lot of us at Adobe are spending time talking about the BlackBerry PlayBook and doing a bunch of blogging. We’ve all been heads down testing content, building apps, and creating samples so when the PlayBook launches (and even before) you guys will be able to find helpful articles to build apps. The team I’m on, Kevin Hoyt’s team, has been spending a lot of time doing blog posts and I thought I’d share some of the cool ones. No matter if you’re a Flex or an AS3 developer, there’s a lot of great content.

Using Swipe Events with the BlackBerry PlayBook

The BlackBerry PlayBook SDK and Simulator support a couple of different swipe events along the top of the device. I recorded a quick screencast that talks about a couple of different ways you can use the events in your own application.

You can grab the example code here in Snipplr.

Using OAuth for Twitter Authentication on the BlackBerry PlayBook

OAuth is becoming critical for any app developer who wants to access data from a 3rd party source on behalf of their users. If you aren’t familiar with OAuth there is an excellent intro doc over at Hueniverse. I had some difficulty wrapping my head around OAuth the first few times I tried to use it so I’ll provide a quick explanation below as I walk through the sample.

OAuth is a way of providing access from a 3rd party site (in this case Twitter) to your application without needing the user to type their 3rd party credentials into your application. Instead, OAuth lets you register with the 3rd party site and uses a series of tokens, which eventually give you access to the data. One of the main benefits with OAuth is that if an application does something nefarious with the data, the user can immediately revoke access. For this example I’ll be walking through the Twitter API and using the very excellent OAuth-AS3 library by Shannon Hicks, the founder of Pintley, which you should check out if you’re the kind of person who likes to drink beer.

The first step to using OAuth is to register with the site you want to get data from. Twitter has a very easy way to do this with their App portal. Fill in the necessary info (select the type as client since this won’t be a web-based application) and Twitter will provide a bunch of random strings that you’ll use to request access to the data and then request the data itself. The data that we’re most worried about is in the OAuth 1.0a Settings section and includes a consumer key, consumer secret, request token URL, access token URL, and authorize URL.

Getting information out of Twitter has the following flow:

  1. Use the consumer key and the consumer secret to create an OAuthConsumer object.
  2. Use the OAuthConsumer object to ask Twitter for permission to access data.
  3. Prompt the user to authorize your application to access their specific data.
  4. Request the user’s data and perform operations on their behalf.

This is all done with a series of tokens until the last stage when you get an access token that you will use to access data after the user gives you permission. I set those up as private field variables so they can be accessed from any method in this example and set the unchanging data as static variables.

private static var CONSUMER_SECRET:String = "<YOUR CONSUMER SECRET>";
private static var CONSUMER_KEY:String = "<YOUR CONSUMER KEY>";
private static var REQUEST_TOKEN_URL:String = "https://api.twitter.com/oauth/request_token";
private static var ACCESS_TOKEN_URL:String = "https://api.twitter.com/oauth/access_token";
private static var AUTHORIZE_URL:String = "https://api.twitter.com/oauth/authorize";
private static var API_URL:String = "https://api.twitter.com";
private static var SIGNATURE:OAuthSignatureMethod_HMAC_SHA1 = new OAuthSignatureMethod_HMAC_SHA1();
 
private var _consumer:OAuthConsumer;
private var _authRequest:OAuthRequest;
private var _accessRequest:OAuthRequest;
 
private var _requestToken:OAuthToken;
private var _accessToken:OAuthToken;

The first step is to have our application log into Twitter with the consumer secret and the consumer key so that it can make the authorization request. In the init() function I set up the UI as well as create the consumer and my first OAuthRequest object, the authorization request. This will just go out to Twitter and make sure that the consumer key and secret are registered to an application. If they are, then Twitter will return a request token that I can use to request access from the user for his or her data. In that init() method I have a button that will start the process and the event handler on the button uses the buildRequest method of my _authRequest object to format the URL string correctly and then I send it off to Twitter.

protected function init():void
{    
     _consumer = new OAuthConsumer(CONSUMER_KEY,CONSUMER_SECRET);
     _authRequest = new OAuthRequest(OAuthRequest.HTTP_MEHTOD_GET,REQUEST_TOKEN_URL,null,_consumer);
 
     _loginContainer = new Container();
     _loginContainer.align = ContainerAlign.MID;
 
 
 
     var button:LabelButton = new LabelButton();
          button.label = "Login to Twitter";
          button.addEventListener(MouseEvent.CLICK,onClick);
 
 
     _loginContainer.addChild(_spacer);
     _loginContainer.addChild(button);
     _loginContainer.setSize(1024,600);
     addChild(_loginContainer);
 
}
 
protected function onClick(event:MouseEvent):void
{
     var urlRequest:URLRequest = new URLRequest(_authRequest.buildRequest(SIGNATURE));
     var loader:URLLoader = new URLLoader(urlRequest);
     loader.addEventListener(Event.COMPLETE,onRequestComplete);
}

With that response I can now build the _requestToken, which I can use to let the user grant me access to his or her data. The next step is to prompt the user to authorize my application. Once I get the response back from Twitter’s servers I save that information as the request token and then build a UI to let the user start the authorization process.

protected function onRequestComplete(event:Event):void
{
     _requestToken = getTokenFromResponse(event.currentTarget.data);
 
     _authContainer = new Container();
     _authContainer.align = ContainerAlign.MID;
 
 
     var authBtn:LabelButton = new LabelButton();
          authBtn.label = "Authorize this application";
          authBtn.addEventListener(MouseEvent.CLICK,onAuthClick);
 
     _authContainer.addChild(_spacer);
     _authContainer.addChild(authBtn);
     _authContainer.setSize(1024,600);
 
     removeChild(_loginContainer);
     addChild(_authContainer);
}

When the user clicks the button my application will send them to Twitter where they will have to log in and authorize my application to use their data. They’re prompted either to deny my app or allow it.

Twitter Authorization Form

If they click Allow, then Twitter provides a PIN number that the user has to enter back in the application to complete the authorization process. For web-based applications there is a callback URL which is where the user is sent after they authorize the application. But for client-side applications, Twitter uses the PIN number and asks the user to enter it back in the application.

Getting the PIN Number

In the code, I first create the UI elements I need to track the PIN number and then make a URLRequest using the key that Twitter provided in the request token.

protected function onAuthClick(event:MouseEvent):void
{
     _verifyContainer = new Container();
     _verifyContainer.align = ContainerAlign.MID;
 
     var label:Label = new Label();
          label.size = 100;
          label.sizeUnit = SizeUnit.PERCENT;
          label.text = "Enter the PIN from Twitter.com";
 
     var font:TextFormat = new TextFormat();
          font.align = TextFormatAlign.CENTER;
          font.bold = true;
          font.size = 24;
 
     text = new TextField();
     text.type = TextFieldType.INPUT;
     text.border = true;
     text.width = 250;
     text.height = 30;
     text.defaultTextFormat = font;
 
 
     var getDataBtn:LabelButton = new LabelButton();
          getDataBtn.label = "Get Tweets";
          getDataBtn.addEventListener(MouseEvent.CLICK,onGetDataClick);
 
     _verifyContainer.addChild(_spacer);
     _verifyContainer.addChild(label);
     _verifyContainer.addChild(text);
     _verifyContainer.addChild(getDataBtn);
     _verifyContainer.setSize(1024,600);
 
 
     removeChild(_authContainer);
     addChild(_verifyContainer);
 
     var authRequest:URLRequest = new URLRequest('http://api.twitter.com/oauth/authorize?oauth_token='+_requestToken.key);
     navigateToURL(authRequest);
}

Once the user comes back, puts in the PIN, and clicks the button, the application uses that information to build the request for the access token. By passing the PIN as the oauth_verifier property we get the access token we need to start requesting data from Twitter.

protected function onGetDataClick(event:MouseEvent):void
{
     var params:Object = new Object();
          params.oauth_verifier = text.text;
 
     _accessRequest = new OAuthRequest(OAuthRequest.HTTP_MEHTOD_GET,ACCESS_TOKEN_URL,params,_consumer,_requestToken);
 
     var accessUrlRequest:URLRequest = new URLRequest(_accessRequest.buildRequest(SIGNATURE));
     var accessLoader:URLLoader = new URLLoader(accessUrlRequest);
          accessLoader.addEventListener(Event.COMPLETE,onAccessRequestComplete);
}
 
protected function onAccessRequestComplete(event:Event):void
{
     _accessToken = getTokenFromResponse(event.currentTarget.data);
 
     var mainRequest:OAuthRequest = new OAuthRequest(OAuthRequest.HTTP_MEHTOD_GET,API_URL+'/1/statuses/friends_timeline.xml',null,_consumer,_accessToken);
 
     var getStatusURLRequest:URLRequest = new URLRequest(mainRequest.buildRequest(SIGNATURE));
     var getStatusLoader:URLLoader = new URLLoader(getStatusURLRequest);
          getStatusLoader.addEventListener(Event.COMPLETE,onStatusLoadComplete);
}

Requesting data is pretty straightforward. Using the consumer and access tokens we just got we can build a normal request, send it off, and then parse the data that comes back. In this case I’m going to go through and display usernames and status for the tweets.

protected function onStatusLoadComplete(event:Event):void
{
     _mainContainer = new Container();
     _mainContainer.flow = ContainerFlow.HORIZONTAL;
 
     var sendTweetContainer:Container = new Container(25);
          sendTweetContainer.containment = Containment.DOCK_TOP;
 
     var font:TextFormat = new TextFormat();
          font.align = TextFormatAlign.CENTER;
          font.bold = true;
          font.size = 24;
 
     twitterTextField = new TextField();
     twitterTextField.type = TextFieldType.INPUT;
     twitterTextField.border = true;
     twitterTextField.width = 500;
     twitterTextField.height = 30;
     twitterTextField.defaultTextFormat = font;
 
     var tweetLabel:LabelButton = new LabelButton();
          tweetLabel.label = "Tweet This";
          tweetLabel.addEventListener(MouseEvent.CLICK,onTweetClick);
 
          sendTweetContainer.addChild(twitterTextField);
          sendTweetContainer.addChild(tweetLabel);
 
     // Code for parsing the XML from the response
     var xml:XML = new XML(event.currentTarget.data);
     var statusList:XMLList = xml.children();
     var arr:Array = new Array();
 
     for(var i:int=0;i<statusList.length();i++)
     {
          var obj:Object = new Object();
               obj.label = statusList[i].user.name.toString() +': ' + statusList[i].text.toString();
          arr.push(obj);
     }
 
     // Create the DataProvider out of the parsed data
     var dataProvider:DataProvider = new DataProvider(arr);
     var list:List = new List();
          list.dataProvider = dataProvider;
          list.size = 100;
          list.sizeUnit = SizeUnit.PERCENT;
          list.setSkin(AlternatingCellRenderer);
 
 
     _mainContainer.addChild(list);
     _mainContainer.addChild(sendTweetContainer);
     _mainContainer.setSize(1024,600);
     removeChild(_verifyContainer);
     addChild(_mainContainer);
}

The next step is to enable the ability to send a tweet. This one is a bit different. For all of the previous calls we just created a params object, and sent that with the OAuthRequest. But when we’re using POST instead of GET, things have to be done differently. Soenke Rohde has a Flash Twitter library that I used for help, but essentially we have to strip the URL params from the original request and then reset them as URLVariables to match the POST request.

protected function onTweetClick(event:MouseEvent):void
{
     var params:Object = new Object();
          params.status = twitterTextField.text;
 
     // Use the same consumer and accessToken to update the Status
     var tweetRequest:OAuthRequest = new OAuthRequest(OAuthRequest.HTTP_MEHTOD_POST,API_URL+'/1/statuses/update.json',params,_consumer,_accessToken);
 
     var setStatusURLRequest:URLRequest = new URLRequest(tweetRequest.buildRequest(SIGNATURE));
          setStatusURLRequest.method = URLRequestMethod.POST;
 
          // use the replace function to strip out the status
          setStatusURLRequest.url = setStatusURLRequest.url.replace("&status=" + URLEncoding.encode(params.status),"");
 
     // Add the status as a URLVariable since it's a POST operation
     setStatusURLRequest.data = new URLVariables( "status=" + twitterTextField.text  );
 
     var setStatusLoader:URLLoader = new URLLoader(setStatusURLRequest);
          setStatusLoader.addEventListener(Event.COMPLETE,onSetStatusComplete);
}

And that’s pretty much all there is to it. You can grab the whole bit of code over on Snipplr. Just swap in your own information from Twitter and you should be set to go.

One thing to keep in mind is that this example makes you authorize each time you use the application. In reality that would be very annoying so if you were using this in production you’d want to save the consumer and accessToken somewhere so you didn’t have to create those each time.

PlayBook Development: Flex Mobile Framework or the QNX Components?

Flex or QNX

One of the first decisions you’ll have to make when you start building a PlayBook application is whether you want to use the Flex Framework for mobile devices or the QNX component set that ships as part of the AIR SDK for PlayBook. Even if you’re a Flex developer it’s not a straightforward decision so it’s important to be able to weigh the pros and cons of each side. I’ve been talking to a number of developers about Flex versus QNX and wanted to put down my thoughts to help developers decide. If you think I missed something, let me know in the comments and I’ll add it.

Performance

I don’t yet have a PlayBook to test the differences on, but in talking to people that do, performance generally seems like a wash. There’s something to be said for the fact that the QNX team is working really closely with the hardware to optimize their component set, but I also know the Flex team has made leaps and bounds in performance for Flex Mobile. When I get an actual device I’ll update this section.

Component Set

This is an area where I thought Flex would have a decided advantage. And if you count the entire component set, it does. But if you limit the comparison to mobile-optimized components, Flex ends up a little behind. For both platforms the core components are there, lists, buttons, etc. Flex has a TextArea component while QNX doesn’t, but QNX has a slider component while Flex’s hasn’t been mobile-optimized yet. Where QNX ends up winning though is in the number of components that are being built to support UI features of the PlayBook. QNX has support for a number of different dialog components including the LoginDialog component as well as built in support for lists with sections. If you’re sticking to the normal base components, Flex is a perfect fit, but the QNX components are a bit higher level and provide some specific UI elements that your users will see across the PlayBook that you can then add to your own application.

The Picker component from the PlayBook AIR SDK



The Picker component selected

Layout

Flex wins this hands down in my opinion. The layout system that the QNX components employ works just fine, and it’s elegantly simple. It uses a container system and I’ve been told it’s similar to Java. I did a screencast on how to use it and it generally accomplishes most of what you’ll need to do. But if you want to do anything above and beyond, the Flex layout system still rules. Having to use things like a Spacer container to align a component in the middle of the screen just seems hacky compared to being able to set properties on a Group. Plus, with Flex you can very easily create your own layouts, which means you can create far more customized looking applications with Flex than with the QNX components.

Skinning/Designer-Developer Workflow

Currently I see this one as a draw. The Spark components of Flex offer a lot of customization and on the desktop provide some great integration with tools like Flash Catalyst, Illustrator, and Photoshop. The only problem is that you don’t really get those integrations if you’re building mobile Flex applications because for most components you won’t want to use MXML-based skins, you’ll want to use AS3 skins. Because all of the designer-developer tooling uses MXML, you won’t be able to pull out all of the benefits of Spark on mobile devices. On the other hand, the QNX components don’t really have any designer developer workflow either. I found the skinning model for QNX to be similar in a lot of ways to the skinning model of Spark. Either way, when you skin these applications you’ll be doing a lot of ActionScript and relying a lot on images.

Mobile UI Paradigms

The TabNavigator UI Paradigm

The Flex mobile team has done a fantastic job of building the framework from the ground up with mobile user interface paradigms in mind. A base Flex Mobile application gives you an ActionBar, which can contain global content, an easy way to add and remove pages from within the application (complete with default transitions between screens), and components for tabbed navigation. With QNX you’re going to be creating most of those from scratch. On one hand it means that you can build up your own mobile UI paradigms if for some reason you don’t want to use what Flex has to offer. On the other hand, the paradigms that Flex Mobile uses are ubiquitous at this point and I’ve found myself trying to copy them in my PlayBook applications.

Flex Mobile's ActionBar Component

Native

This is one of those intangible things but I think it’s important to look at critically. Alex Payne noted some of the issues with AIR applications and how they compare to native applications (also read Ed Finkler’s post on the subject). One of the things I find most exciting about the PlayBook is that it’s the only platform where Flash is native. The QNX components that you use in the SDK are the exact same ones that are being used across the device. It’s very exciting to be able to build native UIs with my Flash/Flex skills. I’m obviously a big fan of Flex and AIR for a lot of things, but given the chance to build native apps, I’ll do it. And it couldn’t be easier for Flash developers to use the QNX components to create native PlayBook apps. That being said, the native APIs and device functionality are all exposed via ActionScript APIs so even if you’re building a Flex app you can still take advantage of the same native and device-specific functionality that developers who use QNX can. So it just comes down to how you want your application to feel compared to other applications on the device.

Another important consideration is being able to reuse your code to deploy to other devices. If you’re looking for a consistent brand across multiple devices and the ability to reuse big chunks of code, Flex is the only answer. You won’t be able to use the QNX components outside of the PlayBook environment so you’ll have to rewrite the application for iOS or for Android or for any of the other devices AIR supports.

Conclusion

I don’t really see a winner for either one of these because I think it comes down to developer skills and what kind of experience you’re going for. I’ve really enjoyed diving into the native QNX components because I like when applications have a native look and feel. If you’re primarily a Flex developer you’re probably going to miss some of the things that you’ll lose by moving to the QNX components. And someone may come up with a Flex skin that mirrors the look and feel of the QNX components so you can get the best of both worlds. Either way building applications for the PlayBook will be right up the alley of any Flash developer. It’s a device that has basically been built from the ground up to support Flash and I’ve been enjoying it immensely. I’m hoping to have a Pintley application out for the PlayBook at launch so if you’re a beer lover, check back here.

One good place to start hashing out the differences is to take a look at the developer documentation for each: Flex, QNX. That will give you a feel for some of what’s supported in each one. And again, I’ll be updating this post with comments so feel free to comment below or to drop me an email.

Using The PlayBook AIR SDK with Flex “Hero” Mobile Projects

When I first started doing PlayBook development with Flash Builder “Burrito” I found it kind of confusing how the PlayBook SDK installed itself. During the install process, the PlayBook AIR SDK installer prompts you for two locations; one is the location of your Flash Builder installation and the second is an existing AIR 2.5 SDK installation. If you’re using Burrito, it drops a new SDK into the sdk folder of the Burrito install (blackberry-tablet-sdk-0.9.x) and then defaults to the 4.5.0 sdk in the next screen as the AIR 2.5 SDK.

If you’re used to doing Android development then the first thing you probably did was go to create a new Flex Mobile or ActionScript Mobile Project using the PlayBook SDK. Unfortunately you’ll get this screen:

Luckily, when you selected the existing AIR 2.5 SDK (assuming you put it in the default 4.5.0 folder or any version of the “Hero” SDK with mobile components) the PlayBook installer has already linked the PlayBook SDK to that sdk. So when you create a Flex Mobile project with the Flex Hero SDK set you’ll see the required QNX .swc files already linked:

But I ran into an issue when I tried to upgrade. My colleague Mihai has a great runthrough of how to migrate from the 0.9.1 PlayBook SDK to the 0.9.2 SDK. But after I tried to upgrade all of my Flex Hero projects kept referring to the old 0.9.1 SDK. And any new Flex Hero projects I created would reference the old 0.9.1 installation. What I discovered was that something during the install process wasn’t overwriting the link in the 4.5.0 folder to the old SDK.

It seems like there is a bug that won’t overwrite the reference to the 0.9.1 correctly but the SDK is referenced in the Flash Builder preferences under Target Platforms. I just had to change it from 0.9.1 to 0.9.2 and all of my old projects as well as all of the new projects started using the new version.

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);
     }
}
}

Using the Container Classes to Lay Out PlayBook Applications

I just put together a tutorial on using the Container classes in the PlayBook SDK. Coming from a Flex background I found the Container classes kind of confusing at first but ultimately a nice elegant solution to the problem of laying objects out on the screen. The tutorial provides an introduction to the Container classes, shows some of the properties that you use to customize them, then walks through a basic example of a header and two sub containers and how to place and align components inside them. Finally it talks about resizing containers.

Here’s a link to the video. The code for the demo is below.

package
{
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.MouseEvent;
 
	import qnx.ui.buttons.LabelButton;
	import qnx.ui.core.Container;
	import qnx.ui.core.ContainerFlow;
	import qnx.ui.core.Containment;
	import qnx.ui.core.SizeMode;
	import qnx.ui.core.SizeUnit;
	import qnx.ui.core.Spacer;
	import qnx.ui.data.DataProvider;
	import qnx.ui.listClasses.List;
	import qnx.ui.text.Label;
 
	[SWF(height="600",width="1024")]
	public class ContainerDemo extends Sprite
	{
 
		private var base:Container;
 
		private var left:Container;
 
		private var right:Container;
 
		private var header:Container;
 
		public function ContainerDemo()
		{
			super();
 
			// support autoOrients
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			// Set up the base container with a horizontal flow
			base = new Container();
			base.debugColor = 0xff0000;
			base.margins = Vector.<Number>([10,10,10,10]);
			base.flow = ContainerFlow.HORIZONTAL;
 
			// Set up the left container with a size of
			// 50 percent
			left = new Container();
			left.debugColor = 0x00ff00;
			left.margins = Vector.<Number>([10,10,10,10]);
			left.size = 50;
			left.sizeUnit = SizeUnit.PERCENT;
 
			// Set up the right container with a size of 
			// 50 percent. By default the Container class
			// lays things out with a vertical flow.
			right = new Container();
			right.debugColor = 0x0000ff;
			right.margins = Vector.<Number>([10,10,10,10]);
			right.size = 50;
			right.sizeUnit = SizeUnit.PERCENT;
 
			// Create the header and dock it to the top 
			// of the screen
			header = new Container();
			header.debugColor = 0x00ffff;
			header.margins = Vector.<Number>([10,10,10,10]);
			header.size = 15;
			header.sizeUnit = SizeUnit.PERCENT;
			header.containment = Containment.DOCK_TOP;
			header.flow = ContainerFlow.HORIZONTAL;
 
 
			// Set up the array, data provider, and 
			// list component 
			var arr:Array = new Array({label:"Deschuttes The Abyss"},
				{label:"New Belgium Sahti"},{label:"Russian River Pliney The Elder"},
				{label:"Dogfish Head Bitches Brew"},{label:"Stone Double Bastard Ale"},
				{label:"Elysian Bifröst Winter Ale"},{label:"Odell 90 Shilling"},
				{label:"Lagunitas Undercover Investigation Shut-Down Ale"},
				{label:"Delirium Tremens"},{label:"Alaskan Double Black IPA"},
				{label:"John John Juniper"},{label:"Full Sail Wreck the Halls"},
				{label:"Samuel Adams Winter Lager"});
			var data:DataProvider = new DataProvider(arr);			
			var list:List = new List();
				list.dataProvider = data;
				list.containment = Containment.BACKGROUND;
 
			// Create the nav buttons with a width of 33.3 percent
			// and set the sizeMode to both so they take up 100% 
			// of the height of the parent container.
			var nav1:LabelButton = new LabelButton();
				nav1.label = "Home";
				nav1.size = 33.3;
				nav1.sizeUnit = SizeUnit.PERCENT;
				nav1.sizeMode = SizeMode.BOTH;
				nav1.addEventListener(MouseEvent.CLICK,onClick);
 
			var nav2:LabelButton = new LabelButton();
				nav2.label = "Recommended Beers";
				nav2.size = 33.3;
				nav2.sizeUnit = SizeUnit.PERCENT;
				nav2.sizeMode = SizeMode.BOTH;
 
			var nav3:LabelButton = new LabelButton();
				nav3.label = "Rated Beers";
				nav3.size = 33.3;
				nav3.sizeUnit = SizeUnit.PERCENT;
				nav3.sizeMode = SizeMode.BOTH;
 
			var spacer:Spacer = new Spacer();
				spacer.size = 50;
				spacer.sizeUnit = SizeUnit.PERCENT;
 
			var label:Label = new Label();
				label.text = "Pintley For PlayBook!";
 
 
			// The addChild calls
			header.addChild(nav1);
			header.addChild(nav2);
			header.addChild(nav3);				
 
			right.addChild(spacer);
			right.addChild(label);
 
			left.addChild(list);
 
			base.addChild(header);
 
			base.addChild(left);
			base.addChild(right);
 
 
			addChild(base);
 
			base.setSize(stage.stageWidth,stage.stageHeight);
		}
 
		protected function onClick(event:MouseEvent):void
		{
			// changes the size of the left/right containers
			// and then calls base.layout() so that all of
			// the base container's children are re-configured
			// and redrawn
			left.size = 25;
			right.size = 75;
			base.layout();
		}
 
	}
}