[ Team LiB ] |
22.2 Building the ComponentsThe four elements described in the previous section are all good candidates for components. In each case, the elements are discrete constituents that can be abstracted. For example, the toolbar is composed of multiple buttons that are responsible for different tasks. However, each of the buttons shares basic, core functionality. Likewise, each of the shape units that are drawn by the user may look different—different outline shapes, sizes, and fill colors—but the basic functionality of all shapes is the same. And it is the same with the text units as well. While there is only one color selector instance in the application, it is nonetheless a good candidate for a component because it is a distinct unit that can likely be reused in another application. 22.2.1 Creating the Superclass for the ComponentsThe toolbar buttons, shape units, and text units all share some common functionality. Each of these three types of components needs to have the capability to define callback methods, including callback methods for when the component is selected, deselected, pressed, and released. While it is certainly possible to define the same methods individually in each of the component classes, you can also define a single superclass from which the component classes can inherit. This is advantageous because you can define the methods in one location. Complete the following steps to define the custom PaintBase class from which three of the components inherit their methods:
Congratulations! You have just created a superclass that defines some common functionality for any class that extends it. Now, perhaps a little explanation is due. A class such as PaintBase is sometimes referred to as an abstract class because it contains abstract functionality that can be used by any class that extends it. By itself, PaintBase doesn't do very much. But it does save you from having to define the same methods in multiple classes later on. Instead, each of the component classes inherits from PaintBase in the following way: ComponentClassName.prototype = new PaintBase( ); When you create a class that inherits from PaintBase, all the methods of PaintBase are available from instances of that class. For example, if myObj is an instance of MyComponentClass, and MyComponentClass inherits from PaintBase, then you can call any of the PaintBase methods from myObj: // Define an onRelease( ) callback function for myObj. Now, // whenever myObj is clicked and released the function // myOnReleaseCallback( ) defined on _root is invoked automatically. myObj.setOnRelease("myOnReleaseCallback", _root); Now, let's look a little more closely at each of the methods of the PaintBase class. The first line of code is an #initclip directive. In this case, you should follow the directive with the value 0, which instructs the Flash movie to execute all the code contained between the #initclip and #endinitclip directives before anything else, even before other code within #initclip directives. Flash processes all #initclip code before any code on the frame in which the component first exists. If the component is an exported symbol, the #initclip code is processed before the first frame of the main timeline. However, in this case you need to ensure that PaintBase is defined before any of the component classes are defined. You can specify the precedence for processing the #initclip code using the optional order parameter. Code with lower order parameters is processed first, and code with order parameters is processed before code without order parameters. Therefore, we use: #initclip 0 The PaintBase class is an abstract class for component classes. All components must extend MovieClip directly or indirectly. Therefore, PaintBase must extend MovieClip (which is not to say that all abstract classes must extend MovieClip): PaintBase.prototype = new MovieClip( ); The setOnPress( ), setOnRelease( ), setOnSelect( ), and setOnDeselect( ) methods all work in the same manner. The concept is to closely mimic the functionality of the callback-setting methods of many of the predefined ActionScript classes and the UI components. Each of these four methods, therefore, accepts a string specifying the name of the callback function. Additionally, you can specify a path to the function. If no path is specified (that is, if the path parameter is undefined), the value of the timeline on which the component instance resides (given by this._parent) is used. In each case, the function name and path to the function are stored in unique properties, such as onPressCB and onPressPath: PaintBase.prototype.setOnPress = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onPressCB = functionName; this.onPressPath = path; }; There is a very good reason to use callback functions instead of allowing each component to define onPress( ) and onRelease( ) methods directly. If onPress( ) or onRelease( ) methods were to be defined for the component instances, the prototype methods would be wiped out. Working with callback functions allows you to define actions to occur on these events for the instances while not overwriting the functionality defined for the prototype. The PaintBase class's onPress( ) and onRelease( ) methods demonstrate how the callback function is invoked without wiping out any functionality that is already defined within the prototype. ActionScript treats functions as properties (Function datatypes) of the timeline in which they are defined. You can use this fact to invoke the callback functions using array-access notation. In each case, the callback function is passed a reference to the component doing the callback. This is both convenient and in keeping with the conventions of callback functions as they are used throughout ActionScript. PaintBase.prototype.onPress = function ( ) { this.onPressPath[this.onPressCB](this); }; PaintBase.prototype.onRelease = function ( ) { this.onReleasePath[this.onReleaseCB](this); this.selected = true; this.onSelectPath[this.onSelectCB](this); }; 22.2.2 Creating the Toolbar Button ComponentThe toolbar comprises eight tools/buttons:
Each of the toolbar buttons shares common functionality with every other toolbar button, namely:
Additionally, there are two kinds of toolbar buttons. The first six buttons are called "stick" buttons because when they are pressed, they remain selected until otherwise deselected. The remaining two buttons (the back and forward buttons) are called "spring" buttons because they spring back after they have been clicked and released. Perform the following steps to construct the ToolbarButton component:
The ToolbarButtonClass class is not an overly complex class, yet there are still some parts of it that warrant further examination. Let's take a closer look at parts of the class. The create( ) method is a long method, but it doesn't need to be intimidating. Most of the code in this method is comprised of drawing methods. First, you create a movie clip for the basic button portion of the component. This movie clip contains three nested movie clips for the button's highlight, center, and shadow. Generally, it is a good idea to create different movie clips for any parts that you draw using the drawing API. Additionally, you should group together any related movie clips into a parent clip, as is done here: this.createEmptyMovieClip("btn", this.getNewDepth( )); this.btn.createEmptyMovieClip("btnHighlight", this.btn.getNewDepth( )); this.btn.createEmptyMovieClip("btnShadow", this.btn.getNewDepth( )); this.btn.createEmptyMovieClip("btnCenter", this.btn.getNewDepth( )); Next, the highlight, shadow, and center of the button are drawn using the drawRectangle( ) method from Recipe 4.4. The center of the button is a gray rectangle. This movie clip has a greater depth than the other two, and so it appears above them. The other two movie clips are also rectangles. Each is drawn slightly larger than the button's center, and the shadow is offset by one pixel in the x and y directions. The size, positioning, and depths of the three movie clips give the illusion of three dimensions to the button. with (this.btn.btnHighlight) { lineStyle(0, 0x000000, 0); beginFill(0xECECEC, 100); drawRectangle(w + 2, h + 2); endFill( ); } with (this.btn.btnShadow) { lineStyle(0, 0x000000, 0); beginFill(0, 100); drawRectangle(w + 2, h + 2); endFill( ); _x += 1; _y += 1; } with (this.btn.btnCenter) { lineStyle(0, 0x000000, 0); beginFill(0xDFDFDF, 100); drawRectangle(w, h); endFill( ); } You create the symbol movie clip above the basic button clip and add the appropriate symbol or label to it. You use a switch statement to determine which symbol or label to add, depending on the value of the name parameter that is passed to the create( ) method. If the value is "select", you should draw an arrow (for the selection tool). You can draw an arrow by placing a rectangle and a triangle end to end. Then, in this example, the entire symbol movie clip is rotated 30 degrees so that the arrow is angled. case "select": with (this.symbol) { lineStyle(0, 0x000000, 100); beginFill(0, 100); drawRectangle(w/4, h/9); drawTriangle(2 * h/3, 2 * h/3, 60, 30, -w/4); endFill( ); _rotation += 30; _x += w/8; _y += h/8; } break; You can draw a line, rectangle, or ellipse using the basic drawing techniques covered in Chapter 4. When the value of name is "text", "fill", "back", or "forward", you should add a text field to the symbol movie clip instead of drawing anything in it. If you create the text field with the width and height of the button, you can format the text so that it is aligned to the center of the button. case "text": this.symbol.createTextField("label", this.symbol.getNewDepth( ), -w/2, -h/2, w, h); this.symbol.label.text = "abc"; var tf = new TextFormat( ); tf.align = "center"; this.symbol.label.setTextFormat(tf); break; Finally, because the custom drawing methods such as drawRectangle( ) and drawEllipse( ) draw shapes with the registration point at the center, you should shift everything in the toolbar button down and to the right by half the height and half the width such that the component's registration point is in the upper-left corner: var shiftx = this._height / 2; var shifty = this._width / 2; this.btn._y += shiftx; this.btn._x += shifty; this.symbol._y += shiftx; this.symbol._x += shifty; The select( ) and deselect( ) methods do the reverse of one another. The select( ) method gives the appearance of the button being in a pressed state. You accomplish this by increasing the dimensions of the basic button movie clip while hiding the highlight and shadow. Additionally, the width and height of the symbol are decreased (since we want to give the appearance of it being slightly further away). Then, when deselect( ) is called, these actions are all reversed. ToolbarButtonClass.prototype.select = function ( ) { this.btn._width += 1; this.btn._height += 1; this.symbol._width -= 1; this.symbol._height -= 1; this._x += 1; this._y += 1; this.btn.btnHighlight._visible = false; this.btn.btnShadow._visible = false; }; ToolbarButtonClass.prototype.deselect = function ( ) { this.btn._width -= 1; this.btn._height -= 1; this.symbol._width += 1; this.symbol._height += 1; this._x -= 1; this._y -= 1; this.btn.btnHighlight._visible = true; this.btn.btnShadow._visible = true; }; The onPress( ) method needs to check to make sure the button is not selected. If it is selected, then nothing needs to be done. But if the button is not already selected, the select( ) method needs to be called (to make the button look like it is being pressed), and the onSelect( ) callback method should be called as well: ToolbarButtonClass.prototype.onPress = function ( ) { if (!this.selected) { this.select( ); this.onSelectPath[this.onSelectCB](this); } }; When the button is released, the type of actions depends on the type of button—"stick" or "spring". A spring button should always appear deselected once it is released. Therefore, if the button is a spring button, call the deselect( ) method. On the other hand, if the button is a stick button, there are more decisions that need to be made. If the button had been previously selected, then the deselect( ) method should be called. Also, spring buttons cannot maintain a selected/deselected state—they are selected only when pressed—but stick buttons maintain a selected/deselected state. Therefore, each time a release event occurs on a stick button, the selected state should be toggled: ToolbarButtonClass.prototype.onRelease = function ( ) { if (this.selected || this.type == "spring") { this.deselect( ); } if (this.type == "stick") { this.selected = !this.selected; } }; 22.2.3 Creating the Shape ComponentThe Flash Paint application uses shapes as one of the two kinds of units that can be drawn by the user (the other being text units). Here is a list of some of the basic characteristics of a shape:
Complete the following steps to create the shape component:
Now that you've had a chance to see the ShapeClass component class, let's delve into it a little more closely. The create( ) method directs the drawing of a new shape. To create the shape, the method requires the width and height of the shape, the color to use for the outline, and the type of shape to draw. The possible values for the shape parameter are "line", "rectangle", and "ellipse". The method then proceeds to call the draw( ) method to draw the outline of the shape. If the shape is not a line, the draw( ) method is also called to draw a fill. At this point, no color has been defined for the fill, so the fill is transparent. This might seem to be a bit pointless, but a shape filled with a transparent fill is much easier to select than a shape that is only an outline. ShapeClass.prototype.create = function (w, h, outlineRGB, shape) { this.shape = shape; this.w = w; this.h = h; this.outline_mc.rgb = outlineRGB; this.draw(this.outline_mc, false); if (this.shape != "line") { this.draw(this.fill_mc, true); } }; The draw( ) method contains the core drawing functionality for the shape component. Before the draw( ) method is invoked, the component should already have at least three properties defined by the call to create( ): shape ("line", "rectangle", or "ellipse"), w (the width), and h (the height). Therefore, the draw( ) method does not require any of these values to be passed to it as parameters. However, the draw( ) method does need to know into which movie clip it should draw (outline_mc or fill_mc) and whether to apply a fill: ShapeClass.prototype.draw = function (mc, doFill) { . . . }; The draw( ) method can be invoked multiple times for each shape, so it is important that any existing contents in the movie clip are cleared: mc.clear( ); Next, the method needs to determine what line styles and what kind of fill (if any) to apply. If the doFill parameter is true, then the outline should be transparent. If the fill color has been defined, it should be used for the fill. Otherwise, the fill should be transparent. On the other hand, if doFill is not true, the outline should be the color that has been set for the rgb property of the outline movie clip. if (doFill) { mc.lineStyle(0, 0x000000, 0); if (this.fill_mc.rgb == undefined) { mc.beginFill(0, 0); } else { mc.beginFill(this.fill_mc.rgb, 100); } } else { mc.lineStyle(0, this.outline_mc.rgb, 100); } The next part of the draw( ) method uses a little programming trick with a return statement. If the shape is a line, then all you need to do is call lineTo( ) once. Therefore, you can use a return statement immediately following that to end the processing of the method. if (this.shape == "line") { mc.lineTo(this.w, this.h); return; } Finally, if the shape is an ellipse or rectangle, the appropriate drawing method is called. These methods don't accept negative values, so it is important that you convert the width and height of the shape to their absolute values. drawW = Math.abs(this.w); drawH = Math.abs(this.h); switch (this.shape) { case "ellipse": mc.drawEllipse(drawW/2, drawH/2); break; case "rectangle": mc.drawRectangle(drawW, drawH); } The doFill( ) method accomplishes a fill by defining the rgb property of the fill_mc movie clip and then calling the draw( ) method. Once the rgb property is defined, the draw( ) method draws the shape with that fill color. ShapeClass.prototype.doFill = function (rgb) { this.fill_mc.rgb = rgb; this.draw(this.fill_mc, true); }; 22.2.4 Creating the Text ComponentText units are the other kind of unit (in addition to shapes) that users can create using Flash Paint. Text units behave similarly to shape units in many ways. Here is a list of the functionality of the text component:
Complete the following steps to create the text component:
The TextClass component class is not overly complex, but it is also not without its own intricacies that are worth exploring in a little more depth. First of all, notice that both the ShapeClass and the TextClass classes have create( ) methods. Furthermore, the create( ) method of each class accepts similar parameters in a similar order (although ShapeClass.create( ) accepts one additional parameter). This similarity is intentional and allows for text and shape components to be treated more or less identically by the Flash Paint application. The TextClass.create( ) method includes several points of interest. First of all, notice that the text field is always created with a depth of 1. The create( ) method can be called multiple times for the same text unit, and creating the text field with the same depth each time ensures that any previous text fields are overwritten: this.createTextField("input_txt", 1, 0, 0, w, h); Additionally, the create( ) method employs an interesting technique using the onChanged( ) event handler method for the text field. When the text field is first created, it is designed to have a specified width and height. The width and height are maintained until the user first enters text into the text field. At that point the onChanged( ) method is automatically invoked, and the text field is set to autosize. The onChanged( ) method has done its job at that point, so it can delete itself: this.input_txt.onChanged = function ( ) { this.autoSize = true; delete this.onChanged; }; The onKillFocus( ) event handler method of the text field is also used. This method calls the deselect( ) method of the component instance. The purpose of this is to deselect the unit when the user clicks outside of the text field. this.input_txt.onKillFocus = function ( ) { if (this.text != "") { this._parent.deselect( ); } }; When a text unit is deselected, set the self-describing editing and selected properties to false. Additionally, the border and background of the text field should be hidden. The TextClass.descelect( ) method calls the deselect( ) method of the superclass, which ultimately invokes the onDeselect( ) callback function to perform necessary housekeeping, such as recording which tool is selected: TextClass.prototype.deselect = function ( ) { super.deselect( ); this.editing = false; this.input_txt.background = false; this.input_txt.border = false; this.selected = false; }; The TextClass.doFill( ) method shares its name with the ShapeClass.doFill( ) method for the same reasons that the create( ) methods are named the same. The doFill( ) method of the TextClass class assigns the new color to the text field: TextClass.prototype.doFill = function (rgb) { this.input_txt.textColor = rgb; }; The onPress( ) and checkEditing( ) methods are designed to overcome a specific problem that can occur when you place a text field within a movie clip. Normally, when you use the mouse to click on an input text field, Flash brings focus to that text field, and you can type in it. However, if the text field is nested within a movie clip, and the movie clip handles button events (press, release, etc.), the movie clip's event handling takes precedence over the event handling of the nested text field. Therefore, there is not a convenient way to select a text field when it is nested within a movie clip that handles button events (such as with the TextClass component class). To work around this issue, the onPress( ) method checks for double-clicks versus single-clicks. When the component is clicked, the value returned by getTimer( ) is recorded. Then that time is compared with the value of the previous click. If the difference is less than half a second, it constitutes a double-click. Otherwise, it is a single-click. When a double-click occurs, the Selection.setFocus( ) and Selection.setSelection( ) methods are used to bring focus to the text field and to move the cursor to the end of the existing text. When a single-click occurs, the onPress( ) method of the superclass is invoked to allow the default handling for single-clicks, namely dragging the item on the paint canvas. TextClass.prototype.onPress = function ( ) { this.input_txt.border = true; this.currentTime = getTimer( ); if (this.currentTime - this.previousTime < 500) { this.editing = true; this.input_txt.background = true; super.deselect( ); Selection.setFocus(this.input_txt); Selection.setSelection(this.input_txt.length, this.input_txt.length); } else { super.onPress( ); } this.previousTime = this.currentTime; }; You might think that the code in the onPress( ) method should be enough to successfully keep the focus on the text field when it is double-clicked. However, the button events keep causing the text field to lose focus. As a workaround for this, the checkEditing( ) method continually brings focus back to the text field if editing is on. You'll need to refer back to the create( ) method to see where we set the interval on which this method is called. The only drawback to this technique is that it does not allow you to do any kind of editing to the text field other than append text or delete text from the end. Text.prototype.checkEditing = function ( ) { if (this.editing && !(Selection.getFocus( ) != String(this.input_txt))) { Selection.setFocus(this.input_txt); Selection.setSelection(this.input_txt.text.length, this.input_txt.text.length); } }; 22.2.5 Creating the Color Selector ComponentYou should use the color selector component that you created in Recipe 12.13. If you followed the complete instructions for that program, then the color selector component should be available from the Components panel. If not, download the code from http://www.person13.com/ascb and install it according to the instructions in Chapter 12. When you have created or installed the color selector, create a copy of the component symbol in the flashPaint.fla Library. You can do this by dragging an instance of the component from the Components panel onto the Stage. Then delete the instance from the Stage. The symbol remains in the Library. |
[ Team LiB ] |