design: BlackBerry code for a rounded rectangle with gradient fill

This article includes sample Java code that creates a gradient-filled rounded rectangle on a BlackBerry device. Big Deal you say. Before I actually tried to do this, I wouldn't have rubbed two brain cells together worrying about it. But then I spent days trying to figure out how RIM's Graphics.drawShadedFilledPath() method works so that I could make a good-looking button.

what it looks like

Here's what the example code below ends up looking like:

BlackBerry rounded rectangle example

Here's the entire code example for your cuttage/pastage. I'll explain the important parts after you skim through.

import net.rim.device.api.ui.*;
import net.rim.device.api.ui.container.*;
import net.rim.device.api.ui.component.*;
import net.rim.device.api.ui.decor.*;

// Our application.
public class RoundedRectWithGradient extends UiApplication {

  // Entry point.
  public static void main(String[] args) {
    RoundedRectWithGradient theApp = new RoundedRectWithGradient();
    theApp.enterEventDispatcher();
  }
  
  // Display the rounded rect demo screen when the app starts.
  public RoundedRectWithGradient() {
    pushScreen(new DemoScreen());
  }
  
  // Simple demo screen with a title and a rounded rectangle with gradient.
  class DemoScreen extends MainScreen {

    public DemoScreen() {
      super(DEFAULT_CLOSE | DEFAULT_MENU);
      
      setTitle("Rounded Rectangle with Gradient");
      
      VerticalFieldManager vfm = (VerticalFieldManager) getMainManager();
      vfm.setBackground(BackgroundFactory.createSolidBackground(0xFFFFFF));
      
      LabelField spacer = new LabelField("", USE_ALL_WIDTH);
      vfm.add(spacer);
      
      RoundedRectField rrField = new RoundedRectField();
      vfm.add(rrField);
    }
  }
}

// Extending field to display a rounded rectangle with a gradient.
class RoundedRectField extends Field {

  // Layout values
  private static final int CURVE_X = 12; // X-axis inset of curve
  private static final int CURVE_Y = 12; // Y-axis inset of curve
  private static final int MARGIN = 2;   // Space within component boundary
  
  // Static colors
  private static final int TEXT_COLOR = 0xFFFFFF;   // White
  private static final int BORDER_COLOR = 0x4A4A4A; // dark gray
  private static final int BACKGROUND_COLOR = 0xFFFFFF; // White
  
  // Point types array for rounded rectangle. Each point type
  // corresponds to one of the colors in the colors array. The
  // space marks the division between points on the top half of
  // the rectangle and those on the bottom.
  private static final byte[] PATH_POINT_TYPES = {
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
    
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
  };
  
  // Colors array for rounded rectangle gradient. Each color corresponds
  // to one of the points in the point types array. Top light, bottom black.
  private static final int[] PATH_GRADIENT = {
    0xAAAAAA, 0xAAAAAA, 0xAAAAAA, 0xAAAAAA, 0xAAAAAA, 0xAAAAAA,
    
    0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000
  };
  
  // Center our readonly field in the space we're given.
  public RoundedRectField() {
    super(FIELD_HCENTER | FIELD_VCENTER | READONLY);
  }
  
  // This field in this demo has a fixed height.
  public int getPreferredHeight() { return 70; }
  
  // This field in this demo has a fixed width.
  public int getPreferredWidth() { return 240; }
  
  // When layout is requested, return our height and width.
  protected void layout (int width, int height) {
    setExtent(getPreferredWidth(), getPreferredHeight());
  }
  
  // When painting is requested, do it ourselves.
  protected void paint(Graphics g) {
    // Clear this area to white background, fully opaque.
    g.clear();
    g.setGlobalAlpha(255);
    g.setBackgroundColor(BACKGROUND_COLOR);
    
    // Drawing within our margin.
    int width = getPreferredWidth() - (MARGIN * 2);
    int height = getPreferredHeight() - (MARGIN * 2);
    
    // Compute paths for the rounded rectangle. The 1st point (0) is on the left
    // side, right where the curve in the top left corner starts. So the top left
    // corner is point 1. These points correspond to our static arrays.
    int[] xPts = {
      0, 0, CURVE_X, width - CURVE_X, width, width,
      width, width, width - CURVE_X, CURVE_X, 0, 0
    };
    int[] yPts = {
      CURVE_Y, 0, 0, 0, 0, CURVE_Y,
      height - CURVE_Y, height, height, height, height, height - CURVE_Y
    };
    
    // Draw the gradient fill.
    g.drawShadedFilledPath(xPts, yPts, PATH_POINT_TYPES, PATH_GRADIENT, null);
    
    // Draw a rounded rectangle for the outline.
    // I think that drawRoundRect looks better than drawPathOutline.
    g.setColor(BORDER_COLOR);
    g.drawRoundRect(0, 0, width, height, CURVE_X * 2, CURVE_Y * 2);
    
    // Place some text in the center.
    String someText = "Some Text";
    Font font = Font.getDefault().derive(Font.PLAIN, 9, Ui.UNITS_pt);
    int textWidth = font.getAdvance(someText);
    int textHeight = font.getHeight();
    g.setColor(TEXT_COLOR);
    g.setFont(font);
    g.drawText(someText, 
      (width / 2) - (textWidth / 2) - MARGIN,
      (height / 2) - (textHeight / 2) - MARGIN);
  }
}

once over lightly

Ok there's a lot of code here. BlackBerry programmers will recognize the first bit as a standard screen, without much going on. The RoundedRectField class is the bit we want to pay attention to. This extends the Field class from the BlackBerry API.

The RoundedRectField starts out with some strange-looking arrays. We'll come back to them in a minute; they're the really weird stuff here. The constructor sets a couple of simple styles to center the field in the available space. We give the field a fixed height and width for this example. Then comes a low-level paint function; this draws the rounded gradient-filled rectangle into the current Graphics context, then outlines it, and finally paints some text in the middle.

my rectangle has twelve points

I kept trying and failing to get what I wanted out of drawShadedFilledPath() because I thought that, since rectangles had four corners, I needed to feed the method an array of four points. Ah, but little did I know that in Ottawa, where the BlackBerry comes from, rounded gradient-filled rectangles have twelve points. I'll have to visit someday just to check the place out and see what it looks like; maybe their stop signs have 24 corners. This illustration shows where the points are found:

The twelve points on an Ottawa Rectangle

Since this is Java programming, we start counting at 0. You'll notice that point zero starts on the side, right where the curve begins. Why don't we start where point 1 is, in the upper left corner? Because if we do, the drawShadedFilledPath() call will fail. We need to use a point type for the corners (where we're not even going to draw anything) that can't be at the beginning or end of the point types array. So my convention has become to start just below the upper left corner, where the point type permits starting and ending.

Let's take another look at the point types array now. The four instances of Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT indicate the four corners (points 1, 4, 7, and 10). The rectangle is drawn beginning with the Graphics.CURVEDPATH_END_POINT at point 0. And no, I haven't the faintest clue what a CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT is. I just work here.

  private static final byte[] PATH_POINT_TYPES = {
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
    
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_END_POINT, 
    Graphics.CURVEDPATH_QUADRATIC_BEZIER_CONTROL_POINT,
    Graphics.CURVEDPATH_END_POINT, 
  };

Another little convention of mine is to leave a blank link between the top and bottom halves of the array, so I won't have to take my shoes off to count when I'm trying to fix something.

more twelve-pointed stuff

Our drawShadedFilledPath() method takes some other array based arguments too. These need to contain the exact same number of points as the Ottawa Rectangle of point types does. So here's the twelve-pointed array of colors in our gradient. The color at point 0 in this array is applied to the point type at point 0 in the array we just looked at.

  private static final int[] PATH_GRADIENT = {
    0xAAAAAA, 0xAAAAAA, 0xAAAAAA, 0xAAAAAA, 0xAAAAAA, 0xAAAAAA,
    
    0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000
  };

There are two more twelve-pointed arrays we need, one for points on the X-axis, the other for points on the Y-axis. Trying to keep X,Y points straight when broken off into separate arrays drove me right round the bend, maybe you're better at this sort of thing than I am. What I did to make things a little more flexible was to rely on a couple of layout constants to help me shape the curve:

  private static final int CURVE_X = 12; // X-axis inset of curve
  private static final int CURVE_Y = 12; // Y-axis inset of curve
  private static final int MARGIN = 2;   // Space within component boundary

The X-axis array also starts at point 0 in our diagram. The width variable is set at the top of the paint function to give us a bit of margin on the sides. The top line of the xPts array below defines the X-axis location of points 0 through 5, the bottom, points 6 through 11.

    int[] xPts = {
      0, 0, CURVE_X, width - CURVE_X, width, width,
      width, width, width - CURVE_X, CURVE_X, 0, 0
    };

The Y-axis array works the same way. The height variable is set at the top of the paint function to incorporate a little bit of margin. Again, start with point 0 in the diagram and work your way around:

    int[] yPts = {
      CURVE_Y, 0, 0, 0, 0, CURVE_Y,
      height - CURVE_Y, height, height, height, height, height - CURVE_Y
    };

big anticlimax

So we now have arrays containing X-axis locations, Y-axis locations, point types, and colors at each point. Each of these arrays has the exact same number of members; add an extra or leave one out and Bad Things happen, and you'll go crazy trying to figure out just what. So, at last, here's how to draw a gradient filled rounded rectangle on a BlackBerry screen:

    g.drawShadedFilledPath(xPts, yPts, PATH_POINT_TYPES, PATH_GRADIENT, null);

There's one more argument that also takes an array of something or other, but I wasn't able to figure that one out and didn't seem to need it. I'll leave it as an exercise for you.

final notes

I tried using a drawPathOutline() call to create a border around the rounded rectangle, but it didn't look very good to my eyes. I used drawRoundRect instead. Try them both and see what you think.

If you want a peek at what I did with this hard-won knowledge, check out the WAVE Mobile Communicator for BlackBerry. At the moment, it's featured on the Twisted Pair homepage. I used this same sort of drawing code for the big green (or red, or gray, depending) Talk button in the center of the screen when I prototyped the user interface.

Ok, hope this helps somebody. Have fun, and say hi to any twelve-pointed Ottawa Rectangles you run across.