Creating a Spark Resizable Title Window with Adobe Catalyst and Flash Builder

June 26th, 2009 by Ben

You may have noticed that with the new Spark components there isn't a new TitleWindow, much like the lack of icon property in a Spark button I wrote about in a previous post. In this tutorial, I'm going to demonstrate how to create a resizable TitleWindow using the new Spark SkinnableContainer component. You can view the results of this demo here or if you just want to grab the code and be on your way, you can download it here.

Create the UI elements in Catalyst.

The following screenshot demonstrates how I built and organized the components in my Catalyst project. For this component, I'm going to need a background, a header, a close button, and a handle to resize the button. I was able to name these components what I wanted by changing their names in the Library panel. Because Catalyst doesn't really let you know the type of components on the stage, it helps to rename these as you build each one.

Import and tweak the Catalyst components in Flash Builder.

After importing the Catalyst project into Flash Builder I made some changes to the following components:

TitleWindowBG.mxml

 
<?xml version="1.0" encoding="utf-8"?>
<s:Group xmlns:s="library://ns.adobe.com/flex/spark" xmlns:fx="http://ns.adobe.com/mxml/2009">
	<s:Rect height="100%" radiusY="10" width="100%" radiusX="10" alpha="0.75">
		<s:fill>
			<s:SolidColor color="#000000"/>
		</s:fill>
	</s:Rect>
</s:Group>

With this component I set the height and width of the rectangle to 100%. Currently Catalyst only lets you specify height and widths as fixed numbers.

TitleWindowCloseButton.mxml

 
<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:s="library://ns.adobe.com/flex/spark" xmlns:fx="http://ns.adobe.com/mxml/2009">
 
	<s:states>
		<s:State name="up"/>
		<s:State name="over"/>
		<s:State name="down"/>
		<s:State name="disabled"/>
	</s:states>
	<fx:Metadata>[HostComponent("spark.components.Button")]</fx:Metadata>
	<s:Rect height="21" radiusY="5" width="21" radiusX="5">
		<s:fill>
			<s:LinearGradient rotation="90">
				<s:GradientEntry color="#7f0000" alpha="1.0" ratio="0"/>
				<s:GradientEntry color="#ff0000" alpha="1.0" ratio="1"/>
			</s:LinearGradient>
		</s:fill>
	</s:Rect>
	<s:RichText
		id="labelElement"
		x="7" y="6"
		color="#ffffff"
		text="(Label)"
		fontSize="12" fontFamily="Arial" fontWeight="bold"
		width="8" height="12"/>
 
</s:Skin>
 

I cleaned out some transistion stuff that wasn't necassary.

TitleWindowHeader.mxml

 
<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:s="library://ns.adobe.com/flex/spark" xmlns:fx="http://ns.adobe.com/mxml/2009">
	<s:states>
		<s:State name="up"/>
		<s:State name="over"/>
		<s:State name="down"/>
		<s:State name="disabled"/>
	</s:states>
	<fx:Metadata>[HostComponent("spark.components.Button")]</fx:Metadata>
	<s:Rect height="28" radiusY="5" left="0" right="0" radiusX="5">
		<s:fill>
			<s:LinearGradient rotation="90">
				<s:GradientEntry color="#ff0000" alpha="1.0" ratio="0"/>
				<s:GradientEntry color="#7f0000" alpha="1.0" ratio="1"/>
			</s:LinearGradient>
		</s:fill>
	</s:Rect>
	<s:RichText
		id="labelElement"
		y="8"
		height="14"
		color="#ffffff"
		text="(Label)"
		fontSize="14" fontFamily="Arial" fontWeight="bold"
		left="5" right="20"  />
</s:Skin>

I set the left and right properties of the Rect and RichText so it would change size with our eventual component.

TitleWindowResizeHandle.mxml

 
<?xml version="1.0" encoding="utf-8"?>
<s:Skin
	xmlns:s="library://ns.adobe.com/flex/spark"
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	height="100%"
	width="100%">
	<s:states>
		<s:State name="up"/>
		<s:State name="over"/>
		<s:State name="down"/>
		<s:State name="disabled"/>
	</s:states>
	<fx:Metadata>[HostComponent("spark.components.Button")]</fx:Metadata>
	<s:Group y="22" rotation="-45" height="100%" width="100%">
 
		<s:Rect height="5" radiusY="3" width="31" radiusX="3" alpha="0.5">
			<s:fill>
				<s:SolidColor color="0xFFFFFF"/>
			</s:fill>
		</s:Rect>
		<s:Rect y="8" height="5" radiusX="3" width="18" x="7" radiusY="3" alpha="0.5">
			<s:fill>
				<s:SolidColor color="0xFFFFFF"/>
			</s:fill>
		</s:Rect>
		<s:Rect y="16" height="5" radiusX="3" width="5" x="14" radiusY="3" alpha="0.5">
			<s:fill>
				<s:SolidColor color="0xFFFFFF"/>
			</s:fill>
		</s:Rect>
	</s:Group>
	<s:Rect width="100%" height="100%">
		<s:fill>
			<s:SolidColor color="#ffffff" alpha="0"/>
		</s:fill>
	</s:Rect>
</s:Skin>

With this component I set the height and width of the artwork Group to 100% and added a transparent Rect to help catch mouse events better.

Create a SparkTitleWindow class that extends SkinnableContainer.

For our custom component we create two public bindable properties "title" and "isResizable" so a developer can easier implement the component. We also declare a "close" event that uses the standard CloseEvent object. For the dragging and resizing functionality, we create start and stop methods that start/stop a Timer that keeps track of the mouse position.

 
package com.tmg.components
{
	import flash.events.TimerEvent;
	import flash.geom.Point;
	import flash.utils.Timer;
 
	import mx.core.UIComponent;
	import mx.events.CloseEvent;
 
	import spark.components.SkinnableContainer;
	[Event(name="close", type="mx.events.CloseEvent")]
	public class SparkTitleWindow extends SkinnableContainer
	{
		private static const DRAG_INTERVAL:Number = 1;
		[Bindable]
		public var title:String = "";
		[Bindable]
		public var isResizable:Boolean = true;
		//drag props
		private var _dragTimer:Timer = new Timer(DRAG_INTERVAL);
		private var _dragInitPoint:Point;
		private var _dragHandle:UIComponent;
		//resize props
		private var _resizeTimer:Timer = new Timer(DRAG_INTERVAL);
		private var _resizeInitPoint:Point;
		private var _resizeHandle:UIComponent;
		public function SparkTitleWindow()
		{
			super();
		}
 
		public function closeWindow():void{
			dispatchEvent(new CloseEvent(CloseEvent.CLOSE));
		}
		//drag methods/handlers
		public function startWindowDrag(dragHandle:UIComponent):void{
			this._dragHandle = dragHandle;
			_dragInitPoint = new Point(_dragHandle.mouseX, _dragHandle.mouseY);
			_dragTimer.addEventListener(TimerEvent.TIMER, onDragTimerTick, false, 0, true);
			_dragTimer.start();
		}
		public function stopWindowDrag():void{
			_dragTimer.stop();
			_dragTimer.removeEventListener(TimerEvent.TIMER, onDragTimerTick);
		}
		private function onDragTimerTick(event:TimerEvent):void{
			var currentMousePoint:Point = new Point(_dragHandle.mouseX, _dragHandle.mouseY);
			var gPoint:Point = _dragHandle.localToGlobal(currentMousePoint);
			var lPoint:Point = this.parent.globalToLocal(gPoint);
			this.x = lPoint.x - _dragInitPoint.x;
			this.y = lPoint.y - _dragInitPoint.y;
 
		}
		//resize methods/handlers
		public function startWindowResize(resizeHandle:UIComponent):void{
			this._resizeHandle = resizeHandle;
			_resizeInitPoint = new Point(_resizeHandle.mouseX, _resizeHandle.mouseY);
			_resizeTimer.addEventListener(TimerEvent.TIMER, onResizeTimerTick, false, 0, true);
			_resizeTimer.start();
		}
		public function stopWindowResize():void{
			_resizeTimer.stop();
			_resizeTimer.removeEventListener(TimerEvent.TIMER, onResizeTimerTick);
		}
		private function onResizeTimerTick(event:TimerEvent):void{
			var currentMousePoint:Point = new Point(_resizeHandle.mouseX, _resizeHandle.mouseY);
			var gPoint:Point = _resizeHandle.localToGlobal(currentMousePoint);
			var lPoint:Point = this.parent.globalToLocal(gPoint);
			var newWidth:Number = this.width + lPoint.x - (this.width + this.x) + (_resizeHandle.width/2);
			var newHeight:Number = this.height + lPoint.y - (this.height + this.y) + (_resizeHandle.height/2);
			if(newWidth > this.minWidth){
				this.width = newWidth;
			}
			if(newHeight > this.minHeight){
				this.height = newHeight;
			}
		}
	}
}

Create SparkTitleWindow SparkSkin.

The last step before implementation is create our SparkSkin mxml component for our window. This where we incorporate the components we created in Catalyst. Note the Group component "contentGroup." Because our host component is a SkinnableContainer it is required to have this Group object. This is where the contents of our new container will go.

 
<?xml version="1.0" encoding="utf-8"?>
<s:Skin
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	xmlns:s="library://ns.adobe.com/flex/spark"
	xmlns:mx="library://ns.adobe.com/flex/halo"
	width="100%"
	height="100%"
	xmlns:components="components.*"
	>
	<fx:Script>
		<![CDATA[
			import components.TitleWindowResizeHandle;
		]]>
	</fx:Script>
	<fx:Metadata>
		[HostComponent("com.tmg.components.SparkTitleWindow")]
	</fx:Metadata>
 
	<s:states>
		<mx:State name="normal"/>
	</s:states>
	<s:layout>
		<s:BasicLayout/>
	</s:layout>
	<components:TitleWindowBG
		id="windowBG"
		width="100%" height="100%"/>
	<s:Button
		id="windowHeader"
		left="5" top="5" right="5"
		skinClass="components.TitleWindowHeader"
		label="{hostComponent.title}"
		mouseDown="{ hostComponent.startWindowDrag(windowHeader) }"
		mouseUp="{ hostComponent.stopWindowDrag() }"/>
	<s:Button
		id="closeButton"
		label="X"
		buttonMode="true"
		skinClass="components.TitleWindowCloseButton"
		right="7" top="8"
		click="{ hostComponent.closeWindow()}"/>
	<s:Group
		id="contentGroup"
		clipAndEnableScrolling="true"
		top="{windowHeader.y + windowHeader.height + 10}"
		left="5" right="5" bottom="5"
		color="#ffffff">
	</s:Group>
	<s:Button
		id="resizeButton"
		right="2" bottom="2"
		mouseDown="{ hostComponent.startWindowResize(resizeButton)}"
		mouseUp="{ hostComponent.stopWindowResize()}"
		visible="{ hostComponent.isResizable }"
		skinClass="components.TitleWindowResizeHandle"/>
 
</s:Skin>
 

Also note how we are able to access our newley written public properties and methods via the "hostComponent" property.

Implement new component.

With our new container written and skin created, we can now implement this new window component.

 
<components:SparkTitleWindow
		id="myWindow"
		skinClass="com.tmg.skins.SparkTitleWindowSkin"
		title="My Title Window"
		x="200" y="50"
		width="300" height="300"
		close="{myWindow.visible=false}">
		<components:layout>
			<s:VerticalLayout/>
		</components:layout>
		<mx:Label text="These are the contents of my window:"/>
		<s:TextInput
			width="100%"
			color="#000000"
			text="A text input..."/>
		<s:TextArea
			width="100%"
			text="A textarea..."
			color="#000000"/>
		<s:Button label="A Button" color="#000000"/>
	</components:SparkTitleWindow>

Summary

There you have it, a brand new container that can be dragged and/or resized. If you don't want to use Catalyst to edit the look and feel of you new component, you could declare style properties in the SparkTitleWindow class and in your skin component bind to these properties. An example of this can be found in my previous Spark post where we create an IconButton.

 

Share and Enjoy:
  • Print
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • DZone
  • Fark
  • LinkedIn
  • Live
  • Reddit
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
  • Yahoo! Bookmarks
  • RSS
  • email

4 Responses to “Creating a Spark Resizable Title Window with Adobe Catalyst and Flash Builder”

  1. Yarin says:

    Great post, very useful- but why all the acrobatics to get dragging working? Why not just startDrag(), stopDrag() on mouseDown/Up respectively?

    Here’s my entry for a simple draggable Spark Panel:
    http://flextip.blogspot.com/2009/09/draggable-spark-panel.html

    I ended up combining my previous drag code with yours for a really tight Spark TitleWindow.

    Nice work- thanks,
    Yarin

  2. Awesome tutorial!

    Thanks heaps! :)

  3. Julien says:

    Hi, can you give me the PSD because i want to make a new FXP with Catalyst beta 2. Your FXP don’t work with this new beta …

    Thanks

  4. Kenny says:

    I found a mistake

    Here, text = “(label)” no button in the text to obtain the values: Title Window
    Same TitleWindowCloseButton the text = “X”, in the skinClass through (label) to obtain the same can not!

    I ask how this is going on,You can email to help me?
    Email:csjialong@163.com

Leave a Reply