How to Skin QNX UI Components (Advanced)

by Retired ‎08-19-2011 02:22 PM - edited ‎06-01-2012 10:09 AM (8,762 Views)

In the last article, How to Skin QNX UI Components (Basic) we talked about using 9-Slice scaling to do simple, good looking skins. There are some limitations to that approach, such as if you want to do something that doesn't strech well like a gradient or a checkerboard pattern. In this case, you will need to take advantage of the Draw API.


Draw API

We got a taste of the Draw API with the Alpha skin when we had to draw the coloured backround behind our transparency mask. Now we are going to use it to programatically draw a rectangle with rounded corners, and fill it with a nice gradient to give a nice highlight to our targets when pressed. For example:



The first change is that we no longer need to embed any image files. Our skins can just start out as blank sprites:


		private var _upSkin:Sprite = new Sprite();
		private var _downSkin:Sprite = new Sprite();
		private var _disabledSkin:Sprite = new Sprite();


It didn't matter before because we were drawing a solid colour, but to do a gradient we'll want to make sure the sprite is being re-drawn every time the object is resized. So, we'll clean up the initializeStates method:

		override protected function initializeStates():void  
			setSkinState(SkinStates.UP, _upSkin);
			setSkinState(SkinStates.DOWN, _downSkin);
			setSkinState(SkinStates.DISABLED, _disabledSkin);

 and then override the draw function, which is called whenever the skin needs to be redrawn (because of a resize, for example):

		override protected function draw():void {
			drawBackground(_downSkin, HIGHLIGHT);
			drawBackground(_disabledSkin, LOWLIGHT);	


You'll note we've added an optional parameter to drawBackground. If it's specified, it will be with the colour used in the center of the gradient (either white or dark grey, to indicate a correct or incorrect press on the target). If it's not specified, it will tell us to draw a black center.


Now we'll need to change drawBackground so it's doing everything for us. First, we will set up the lineStyle so all of the rectagles we draw will have a solid border in the colour of the Target. Then, if we're drawing a gradient, we will paint a solid rectangle of the appropriate colour so we can put the gradient overtop:;
			//Set up the border for all subsequent draws, _colour);
			if (gradient >= 0) {
				//Draw the background colour, the gradient will let some through;,0,width,height, CORNER_CURVE);


I definited CORNER_CURVE as 18. This represents the width and height of the curve, and should give us one that is basically the same as the one we created for the 9-slice skins.


We will be drawing a either highlight for the DOWN state, a "lowlight" for the INVALID state, or a black rectangle for the UP state. The highlight will be a semi-transparent gradient going to white in the center, which will give us a nice glow effect, and allow the underlying colour through. The "lowlight" will be the same, except going to towards a dark grey, which would indicate an invalid press on the target.


You saw the highlight above, here is the "lowlight":



And of course the UP state is visible on the other three targets.


We'll need a matrix for the actual gradient, but since we aren't doing any fancy rotations or translations we can have a default gradient box created for us:


				var matrix:Matrix = new Matrix;
				matrix.createGradientBox(width, height, 0, 0, 0);


Then we need to set up the graphics object on our sprite to be ready do do a gradient fill. We do this with the sprite.grapgics.beginGradientFill method. This takes several different parameters:, [gradient, gradient], [CENTER_ALPHA, EDGE_ALPHA], [0, 250], matrix, SpreadMethod.PAD, InterpolationMethod.RGB, 0);


For a full explination of each parameter in the beginGradientFill method, check out the ASDocs. Basically what it's doing though is creating a radial gradient (so, it expands out in a circle), with two points (those three arrays of length 2). The colour will go from gradient (either white or grey in this case) to... gradient. It will actually be a single colour. What is changing on the gradient is the alpha value (I defined those constants in the second array as .65 and .30). This lets us show a lot of the background colour near the edge, and have a mostly white or grey highlight in the center.


If we don't want a gradient, we'll just set up a simple black fill with no borders.


			} else {
				// If we aren't sending a gradient value, set it up to draw a black background;

 Then we draw the gradient or the black fill, whatever we had set up.,0,width,height, CORNER_CURVE);;



Advanced Draw API

All of our previous examples have been creating simple rectangles with rounded corners. Those are very nice, but sometimes you want to draw something a little different. You could apply what we've already discussed to do a few more shapes, like circles and eplipses, but what if you want to draw a skin with a completely arbitrary shape? For that you need the drawPath method, as seen in the ShapedGradientSkin class.


ShapedGradientSkin is based off of our GradientSkin. initializeStates and draw are the same,  the only difference is in drawBackground. Rather than a rounded rectangle, we are going to draw a triangle with a little indent in the corner facing the middle. For example:




Lets look at the first part of the code:


private function drawBackground(sprite:Sprite, gradient:int = -1):void {;
			//Set up the border for all subsequent draws, _colour);
			var xIndent:int = int(width * .8);
			var yIndent:int = int(height * .8)
			var lt:int = GraphicsPathCommand.LINE_TO;
			var mt:int = GraphicsPathCommand.MOVE_TO
			var commands:Vector.<int> =Vector.<int>([mt, lt, lt, lt, lt, lt]);
			var data:Vector.<Number> = Vector.<Number>([
				width, 0,
				width, yIndent,
				xIndent, yIndent,
				xIndent, height,
				0, height,
				width, 0]);
			//Draw the gradient or black center as in GradientSkin
			if (gradient >= 0) {
				//Draw the background colour, the gradient will let some through;, data);
				// Set up the matrix to handle the alpha gradient
				// We will translate it slightly so it goes more to the center of the triangle
				var matrix:Matrix = new Matrix;
				matrix.createGradientBox(width, height, 0, width/4, height/4);
				//Set up the gradient for drawing, [gradient, gradient], [CENTER_ALPHA, EDGE_ALPHA], [0, 250], matrix, SpreadMethod.PAD, InterpolationMethod.RGB, 0);
			} else {;
			}, data);;


The code is basically the same as in GradientSkin. Our call to drawRoundRect has been replaced with a call to drawPath, with some extra setup to make that call work. The core of drawPath is the commands vector that will tell the drawPath method what to do. The initial point is defined with the MOVE_TO command, so we start at the x origin and the bottom of the sprite. Then, we step through the rest of the commands vector, and for each one we'll do the command (in this case, a LINE_TO every time, but you could also do a MOVE_TO to not draw a line, or CURVE_TO to draw a curve) to the point specified by the next two values in the data vector.



This ends up giving us a nice triangle with our little indent. The shape is closed, and so can be filled with black or our gradient and it won't bleed over the edges. This also has the neat effect of defining the touch sensitve region of our object. If you touch the black area, the object doesn't get any event! It's as if it isn't there. Which, really, it isn't.


You may have noticed that this only draws the top left target, and the others must be flipped in order to correctly have the indent facing the center. There are two ways to do this. The first is explored in the drawBackground2 method in the sample app, and focuses on defining the points correctly the first time. The second, and arguably simpler if you understand matrix operations is to apply a transformation matrix.


We have four shapes to draw. They have an _index value that goes from 0-3, corrisponding to the following quadrants:



If we examine that picture, we can see that if _index is odd, we'll need to mirror the target on the y-axis, and if it's greater than or equal to 2 we'll have to mirror it on the x-axis. As these operations will not happen in-place, we'll also have to translate the target back by the width (index is odd) or height (_index is >=2) to get back where we started. Obviously for _index=3 both cases apply.


Explaning transformation matrixes in general is beyond the scope of this article, but basically we'll have a 3x2 matrix with the following values: a, b, c, d, tx, and ty. b and c are only used for rotations and can be set to 0 and ignored in our sample. For index 0 we want the identity matrix so no flips/translations occur (a==d==1, tx==ty==0). For the others we'll need to replace the first 1 (a) with -1 to flip on the y-axis, and the second 1 (d) with -1 to flip on the x-axis. This can be summarized by the following chart:


_index a d tx ty
 0 1 1 0 0
1 -1 1 width 0
2 1 -1 0 height
3 -1 -1 width height


We could use a series of conditionals to assign the values for our variables, but it can be a bit hard to read (see the sample app if you are curious). Instead, we can take advantage of the fact that (-1)^0 is 1, and (-1)^1 is -1. We can turn our _index value into 0 or 1 with modulus (even or odd) or integer division (less than 2 or greater than 2). So we'll use that to create our matrix and then assign it to our sprite's transform.matrix property to have it automatically applied for us:


			var a:int = Math.pow(-1, _index % 2);
			var d:int = Math.pow(-1,int(_index / 2));
			var tx:int = width * (_index % 2);
			var ty:int = height * int(_index / 2);
			var transformMatrix:Matrix = new Matrix(a, 0, 0, d, tx, ty);
			sprite.transform.matrix = transformMatrix;


That's it, we now have four fancy Targets that will switch between various states, showing a gradient or not, with our touch sensitive regions automatically defined to what is visible.


The Draw API is very powerful. Using these techniques you can skin your UI components to look any way you like. To see all the techniques discussed in action, take a look at the attached sample app, Sequence.