Extending ScrollControlBase: Displaying the Scrollbars
This post is the second of a 4-post series on “Extending ScrollControlBase.” It discusses how to override UIComponent’s validation methods in order to display the scrollbars provided with ScrollControlBase. Here’s some links to the other pages in this article:
1. Its Internals and Design Philosophy
2. Displaying the Scrollbars
3. Getting Scrolling to Work
4. The Workflow
Working with ScrollControlBase revolves around two things: Getting the mask to work right, and getting the scroll bars to function correctly with your image. We’ll cover each in turn in a simple example: Getting an image that’s viewed with a ScrollControlBase.
Our example component has a skeleton like this:
package {
import mx.core.ScrollControlBase;
public class ScrollingImage extends ScrollControlBase {
public function ScrollingImage() {
super();
this.horizontalScrollPolicy = "auto";
}
}
}
That code by itself gives you essentially a ScrollControlBase directly. It gives you the scrolling properties and methods of ScrollControlBase, but applies no mask to its children at all, so adding any components will result in them being unsized unless explicitly sized, in which case they will occlude the border.
One thing of note is the setting of horizontalScrollPolicy. Scroll policies are really easy and have three possible values: off, on, or auto. offnever shows scroll bars, on always does, and auto does only when there’s content needing to be scrolled. By default, verticalScrollPolicy is set to auto, and horizontalScrollPolicy is set to off. We set it to auto here since it’s as likely we’ll need to scroll horizontally as it is vertically.
For our component, the image being displayed is assumed to be an embedded asset, which are of type Class, so the getter and setters look like this:
public class ScrollingImage extends ScrollControlBase {
// Constructor and imports omitted for brevity
protected var _imageClass:Class;
public function get imageClass():Class {
return this._imageClass;
}
public function set imageClass(imageClass:Class):void {
this._imageClass = imageClass;
}
Of course, this code won’t get us the image added to our display list, since we haven’t instantiated it yet. We could instantiate it and add it there, but of course, we wouldn’t know what size it should be, or whether we’ll have a border eventually or not. The addition and positioning of the image should take place within the implementation rules of UIComponent. So we’ll follow this order to draw any image:
commitProperties: Remove the old image if any, add the new one.measure: Retrieve the size of the image, and use that, plus any border and padding of the parent, and setmeasuredWidthandmeasuredHeightto those values.updateDisplayList
: Position the image, offset by any padding and border, with the width governed by those values, plus the widths and heights of any scrollbars.
Clearly, changing the image will upset all of these, so we change our setter to the following to invalidate the properties, size, and rendering of our component:
private var imageClassChanged:Boolean = true;
protected var currentImage:DisplayObject;
protected var _imageClass:Class;
public function get imageClass():Class {
return this._imageClass;
}
public function set imageClass(imageClass:Class):void {
this._imageClass = imageClass;
this.imageClassChanged = true;
this.invalidateProperties();
this.invalidateSize();
this.invalidateDisplayList();
}
Having a flag variable (in this case, imageClassChanged) is a common practice amongst components. We use this flag in commitProperties so that we only update what’s been changed. We keep the image currently being rendered in a new variable named currentImage, with the imageClass being the cheap, inexpensive variable being changed in the setter.
Now for UIComponent’s three methods we’re overriding. With the exception of updateDisplayList, these methods would look the same if you were implementing any kind of component, but I’ll cover them here for completeness:
1. Implementing commitProperties
The code will look like this:
override protected function commitProperties():void {
super.commitProperties();
if(this.imageClassChanged) {
this.imageClassChanged = false;
if(this.currentImage) {
// Clear our old image if any.
this.removeChild(this.currentImage);
this.currentImage = null;
}
if(this.imageClass) {
// We have a new image, so load it up.
this.currentImage = new this.imageClass();
// Make it invisible for now until it's properly positioned.
this.currentImage.visible = false;
this.currentImage.mask = this.maskShape;
// Finally, add it to our display list.
this.addChild(this.currentImage);
}
}
}
This code doesn’t look very different from any other implementation of commitProperties so I won’t cover it in depth. Basically, you check your flag, and if its true, clear out the old image, and add your new one in. The critical pieces are here are the assignments to mask. Remember to clear the old one otherwise any reuse of the removed image will result in unpredictable behavior. Also, remember to set the new image’s mask to the maskShape property.
You should probably also set the new image’s visible property to false, otherwise you might end up with some unwanted flicker of the unpositioned image in its default size and position.
2. Implementing measure
override protected function measure():void {
super.measure();
var edgeMetrics:EdgeMetrics = this.viewMetrics;
var width:Number = edgeMetrics.left + edgeMetrics.right;
var height:Number = edgeMetrics.top + edgeMetrics.bottom;
if(this.currentImage) {
// We have a real image, so get its dimensions.
width += this.currentImage.width * this.currentImage.scaleX;
height += this.currentImage.height * this.currentImage.scaleY;
} else {
// No image, so just set it to our edgeMetrics, with an added default size of 40x40
var defaultSize:Number = 40;
width += defaultSize;
height += defaultSize;
}
this.measuredWidth = width;
this.measuredHeight = height;
}
Nothing amazing here either. The complicated piece is getting the size of the image, and getting some default size otherwise. An important note here is the use of viewMetrics. That will give you the size of the border in the ScrollControlBase you’re using, along with any visible scrollbar sizes. Forgetting to use this could result in clipping of the scrollbars if your content is unusually small, so add them to your measured values.
3. Implementing updateDisplayList
First, the code:
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
super.updateDisplayList(unscaledWidth, unscaledHeight);
if(!this.currentImage)
return;
var edgeMetrics:EdgeMetrics = this.viewMetrics;
this.currentImage.x = edgeMetrics.left + -this.horizontalScrollPosition;
this.currentImage.y = edgeMetrics.top + -this.verticalScrollPosition;
// Make the image visible.
this.currentImage.visible = true;
}
Again, using viewMetrics gets you the necessary offsets for your image. Since we’ve assigned mask of our currentImage already, there’s actually nothing ScrollControlBase-specific here at all.
Viewing the component in this state will show you a properly masked image if one’s assigned, surrounded by a inset border. Really, the implementation so far is quite simple, with the complexity coming from simply implementing a custom component; ScrollControlBase doesn’t get in the way at all, and provides its mask for easy use for your component.
Of course, a critical issue here is that while the image is properly masked, there’s no way to scroll with it. We’ll cover this issue in the next post on getting scrolling to work.
Leave a Comment