In Flex, what is a better way to build UI component whose height is based on width and content? - actionscript-3

I need to build a new UI control based on UIComponent. The control's height should have a certain ratio to the width and also depends on what content is shown (amount of text inside). Could anybody recommend how I should do this?
I have tried googling around, but it seems all articles just try to avoid talking about the case. My personal feeling is that the way Flex renders, i.e. invalidate and update display list, is natively inefficient to deal with the situation.
Below are my codes. It works all fine, but it seems this is not the most efficient way because the height is calculated inside updateDisplayList(), which triggers another updateDisplayList().
The control is used inside a mobile project, so when the device is rotated, the parent's size will be changed and the control needs to be resized as well.
So, a few questions:
What should I do in measure()? Since the height is based on actual width, I cannot get the final width until updateDisplayList(), because the parent of the control has width defined as percentage.
Is it correct to calculate the height at the very beginning of updateDisplayList()? If not, how should I do it?
Below is an example what I mean.
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import mx.core.UIComponent;
public class MyControl extends UIComponent
{
public function MyControl()
{
super();
}
public var textPadding:Number = 15;
public var heightRatio:Number = 1.61803398875;
private var _label:String;
private var labelChanged:Boolean = false;
public function get label():String
{
return _label;
}
public function set label(value:String):void //can be long text with about 20-30 lines
{
if (_label != value)
{
_label = value;
labelChanged = true;
invalidateProperties();
invalidateSize();
invalidateDisplayList();
}
}
private var text:TextField;
override protected function createChildren():void
{
super.createChildren();
if (!text)
{
text = new TextField();
text.autoSize = TextFieldAutoSize.LEFT;
text.multiline = true;
text.wordWrap = true;
addChild(text);
}
}
override protected function commitProperties():void
{
super.commitProperties();
if (labelChanged)
{
text.text = label;
labelChanged = false;
}
}
override protected function measure():void
{
super.measure();
//What should I do here?
}
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
text.width = unscaledWidth;
if (text.height < unscaledWidth / heightRatio)
{
height = unscaledWidth / heightRatio + textPadding * 2;
}
else
{
height = text.height + textPadding * 2;
}
super.updateDisplayList(unscaledWidth, unscaledHeight);
text.x = textPadding;
text.y = textPadding;
}
}

I need to build a new UI control based on UIComponent. The control's
height should have a certain ratio to the width and also depends on
what content is shown (amount of text inside). Could anybody recommend
how I should do this?
Technically you can't.
Your whole approach / understanding of how Flex works is fundamentally flawed.
A Flex component will never size itself. It is always sized by its parent. The measure() method will set ideal values: measuredWidth, measuredHeight, measuredMinWidth, and measuredMinHeight. But, they are just suggestions to be provided to the parent container; which may or may not use them.
MXML masks this concept and your misunderstanding is very common.
In most cases, the default Flex layout containers will do their best to honor these values, but they have logic to handle situations where their is not enough space. For example; a VGroup with two children that are both set to 100% height won't size both children to 100% of the VGroup.
Without seeing your code that uses and sizes your component it is tough to say what is going on. Most likely the 'height' that you are setting is not getting overwritten by the parent container; and that is the only reason I can believe this approach works.
I'm making some assumptions here; but I think your measure method should look something like this:
override protected function measure():void
{
super.measure();
this.measuredwidth = text.getMeasuredOrExplicitWidth();
this.measuredHeight = text.getMeasuredOrExplicitHeight()/heightRatio + textPadding * 2;
}
updateDisplayList(), should be something like this:
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
text.width = unscaledWidth;
text.height = unscaledWidth / heightRatio;
super.updateDisplayList(unscaledWidth, unscaledHeight);
text.x = textPadding;
text.y = textPadding;
}
The updateDisplayList() in the component's parent container should do something like this:
myControlInstance.height = myControlInstance.measuredHeight;
myControlInstance.width = myControlInstance.measuredWidth;
The Spark framework uses all the same methods; but sometimes shuffles them around a bit. So, the updateDisplayList() and measure() are usually in a skin class; or a layout class.

According to Adobe Documentation http://livedocs.adobe.com/flex/3/html/help.html?content=ascomponents_advanced_2.html, measure() is used to measure default size and updateDisplayList() is used to size and place child components.
To answer your questions:
1) I'd say to do nothing, since you're not interested in default size.
2) I think, it doesn't matter since the value change doesn't have immediate effect (as you write updateDisplayList will be called again).
My suggestion is to have an listener for widthChanged event and use it calculate proper height. And to do height calculation when label is changed.

There is no way around the extra update cycle, that is just the nature of layout with text reflow. Usually how it's done (or at least how it's done in the flex components) is to measure based on it's preferred width first, which may be quite wide if measuring text with no explicit bounds. This measurement is important however as it notifies parent containers if and when it should try and expand out the width as much as possible, before the uper limit is set in layoutBoundsWidth or layoutBoundsHeight. Once your bounds are set in the first call to updateDisplayList, you should resize your text and invalidateSize() so you can measure the height on the second pass. Sorry I have no code examples, im on the mobile. If you need them, I can post some when I'm in the office.

Related

Having trouble getting calls to addChild() on nested children in vanilla ActionScript 3 to do anything

Assume the following code:
package
{
import somenamespace.Button;
import flash.display.*;
import flash.events.Event;
import flash.text.TextField;
public final class Main extends Sprite
{
public function Main()
{
stage ? init() : addEventListener(Event.ADDED_TO_STAGE, init);
function init(pEvent:Event = null):void {
// support autoOrients
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
removeEventListener(Event.ADDED_TO_STAGE, init);
/*var btn:Button = new Button(0, 0, 400, 400)
addChild(btn);*/
var sprite:Sprite = new Sprite();
sprite.width = 400;
sprite.height = 400;
addChild(sprite);
var label2:TextField = createCustomTextField(sprite, 0, 50, 200, 20);
label2.text = "Drag to select some of this text.";
}
}
private function createCustomTextField(sprite:Sprite, x:Number, y:Number, width:Number, height:Number):TextField {
var result:TextField = new TextField();
result.x = x; result.y = y;
result.width = width; result.height = height;
sprite.addChild(result);
return result;
}
}
}
I'm having trouble getting nested children to show their own children on the screen. addChild() presumably adds the child, but nothing happens on the screen. I don't understand why. I'm used to using Flex and Starling and stuff like that, but the idiom seems to have changed a bit on even this basic point when going to straight vanilla.
You can see where I've commented out a bit of code where I was designing a custom button class, and I was having a lot of the same trouble with it; however I was able to get it to finally draw a blue rectangle on the screen, once I stopped trying to add a sprite to that effect to the button, and once I started to use the button's own graphics property to draw the rectangle instead (Button extends Sprite), along with using some awkward timing and such when manipulating its width and height.
Earlier I was trying to add the TextField directly to the Button, originally within its own constructor, but even adding it to a regular Sprite in the Main class isn't working. Replacing sprite.addChild(result); with addChild(result); makes it show up fine though.
In general, what's wrong with this type of design? I'm not just asking about this particular example, but in general, how do you get children to add children that add children and so on? Thanks!
UPDATE
Well, as it turns out, setting the width and height on a Sprite is what's causing the problem. Take that out, and you're fine. You can even make that nested Sprite's graphic property draw a rectangle of a certain width and height, and then add children to that Sprite, as long as you are not actually setting those particular properties. Obey this one rule, and everything works great. Disobey, and it's the end of the world!!!
...Or is it? I mean, those properties are probably there for a valid reason; it's just hard to tell why they would be linked to causing issues like preventing children from showing up on the screen, for no other reason than the simple fact that you touched them. As I experimented, I usually made sure the values provided for a size that was plenty big enough for any of the Sprite's children, and that their x and y properties were also set appropriately, so they shouldn't have just been moved offscreen or even off the Sprite itself. Anybody got an idea?
EDIT
One other thing to note is that, once I got the Button class to draw a blue rectangle, I was somehow able to adjust the size of the rectangle itself by setting the width and height properties of the Button; the Button's graphics property was having its size scaled to align with the Button's, and this was part of a built-in algorithm. However whether that actually worked or not depended on things being set up a certain way, and that setup itself seemed kind of random. At this point, I'm moving forward just fine without messing with Sprites' width and height properties, but there was something there that was kind of obscure.
Let's first find out what the documentation says about the width property:
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/DisplayObject.html#width
The width is calculated based on the bounds of the content of the display object.
There's no content in a newly created Sprite object. It is just an empty container.
Reading this value without content makes sense, the answer is 0. But writing a value to it that's different from 0 doesn't make too much sense.
How do you want to change the width of nothing?
Basically speaking, an empty DisplayObject should not have a writeable property width.
Maybe it should throw an error when you try to do it, but it doesn't really have to, because the operation isn't very logical.
There's a couple of things I'd suggest here - take a look at this:
package com.test
{
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.text.TextField;
public class Main extends Sprite
{
public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
//Change the stage parameters
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
//Adding a test Sprite
var sprite:Sprite = new Sprite();
this.addChild(sprite);
//Adding some color to the sprite so you can see ti
sprite.graphics.beginFill(0xff0000,1);
sprite.graphics.drawRect(0, 0, 100, 200);
sprite.graphics.endFill();
//Adding a text field
var textField:TextField = new TextField();
textField.text = "HELLO!";
this.addChild(textField);
}
}
}
Here's a similar example that'll hopefully show what's going on. First, your init function is being called in the constructor so I'm not sure if that'll work like it should, try pulling it out into its own function. Also - display objects when instantiated don't actually have anything in them so there's really no need to specify height and width. Let the contents determine the height and width unless you some need to define them explicitly.
You can modify the example so that the TextField is added to the sprite instead of the main Sprite by using:
sprite.addChild(textField);
Hopefully this helps!

Optimize ItemRenderer For Flex Mobile Application

I made this class, which is an ItemRenderer class, used in a DataGroup ( mobile application ),
and I am not entirely sure if I did the right thing or not, my issues are :
Is there a better way to show the image, which is 80x80 and directly loaded from the server;
How to make the height of the row dynamic, I mean, depending on the height of the 3 StyleableTextFeild
Is this the right way to add the listener on the image, that will trigger a simple HTTPService,
Here is the functions from the class, Any help would be much appreciated !!
Image
Declared it as a simple image :
var logo:Image;
On override createChildren
logo = new Image();
addChild(logo);
And I added on set Data
logo.source = "http://192.168.0.15:3000/"+value.logo_thumb_url;
Size
override protected function measure():void {
measuredWidth = measuredMinWidth = stage.fullScreenWidth;
measuredHeight = measuredMinHeight = 100;
}
Listener
override public function set data(value:Object):void {
tel.text = String(value.Tel);
description.text = String(value.Descricao);
nome.text = String(value.Nome);
logo.addEventListener(MouseEvent.CLICK, function():void{
var service:HTTPService = new HTTPService();
service.url = value.targer;
service.method = "GET";
// setting headers and other variables ...
service.send();
});
}
You can use URLLoader or Loader for loading the image if you are planning to cache the image on the client side, if you cache the image, it wil help you not load the image again when the users scrolls through the list. (What you have done is Ok, but you will hit performance issues)
For variable row height, if Datagroup does not work, use List. find it here Flex 4: Setting Spark List height to its content height
There should be a buttonMode property for some items, make it buttonMode for the logo, for variable row height, find something related to wordWrap and variableRowHeight properties on the datagroup.
There are a few suggestions, what you have coded is good, but, instead of adding the listeners on set data, add it in creation complete, as it is more appropriate. Also, the event listeners has to be weak referenced, http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/events/EventDispatcher.html#addEventListener()

Invisible height in Flash's CheckBox when used in a ScrollPane for auto-calculation of height

When using fl.controls.CheckBox inside a fl.controls.ScrollPane, it seems that whenever the last item in ScrollPane's children container is a CheckBox, ScrollPane's update() method always leaves significant blank space below the CheckBox, although I specifically set the size of the CheckBox.
Question: How do I remove the invisible height from the CheckBox?
I've researched in many places and finally found a useful post by this awesome person in the link below. All credits goes to him, Infineon. I can't trace to the flash support forum he is referring to though.
http://www.actionscript.org/forums/showpost.php3?p=842806&postcount=6
The solution to the above invisible height problem is really hard to find as a dig of the Flash AS3 documentation doesn't turn up override protected function configUI():void method easily. At least I haven't found it till now, and I will never have known about it either without some kind of prior knowledge into the inner workings.
*Note:* This solution doesn't only work on CheckBox. Other form UI widgets like fl.controls.RadioButton also works the same way.
As noted below, this is because of the package hard-coding width and height of the rectangle in the super class of the widget. We will need to override it so that the invisible rectangle becomes smaller or editable.
I will reproduce his code verbatim here to prevent link rot:
package com.your.package.here
{
import fl.controls.CheckBox;
import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.display.Shape;
import flash.text.TextFieldAutoSize;
public class WidgetCheckBox extends CheckBox
{
public function WidgetCheckBox()
{
}
override protected function configUI():void
{
super.configUI();
// remove the background movie clip added in the superclass
removeChild(background);
// redraw the hit area of the checkbox component
var bg:Shape = new Shape();
var g:Graphics = bg.graphics;
g.beginFill(0, 0);
// draw the background area using the width and the height of the
// component, instead of hardcoding these properties ( in the
// superclass the width and height of the rectangle were 100 and 100
g.drawRect(0, 0, _width, _height);
g.endFill();
background = bg as DisplayObject;
// add the new background
addChildAt(background, 0);
}
override public function set label(value:String):void
{
super.label = value;
// in the superclass the size of the label textfield was set to
// 100 by 100 px; instead of using these values, autosize the
// textfield every time a new label is set
textField.multiline = false;
textField.autoSize = TextFieldAutoSize.LEFT;
}
}
}
Then all you need to do is to import the new class you have extended:
var checkBox:WidgetCheckBox = new WidgetCheckBox();
Hope this helps some people to relieve their head-scratching.

AS3 - Right alignment in DataGrid not working as I expected

Right, so I have a DataGrid with two columns. In the left column, I want the text to be aligned to the right: more specifically, if the column is too small to show the entire text, I want it to contain the right-most contents.
I managed to align it by making a custom class extending CellRenderer and setting the DataGrid's cellRenderer property to the class, as specified in this article. However, this doesn't work exactly how I want it to. It does indeed align the text to the right, but if the column is too small for all the text to fit, it still only shows the left-most section of it.
Does anybody know how I can make it show the right-most part of the text instead?
Just so you know, this is in a pure AS3 AIR application in FlashDevelop, and my CellRenderer class is the same as the one in the article I linked to above (i.e. RightAlignCell).
Here was my original code of my CellRenderer:
package
{
import fl.controls.listClasses.CellRenderer;
import fl.controls.listClasses.ICellRenderer;
import flash.text.TextFormat;
import flash.text.TextFormatAlign;
public class RightAlignCell extends CellRenderer implements ICellRenderer
{
private var tf:TextFormat;
public function RightAlignCell()
{
tf = new TextFormat();
tf.align = TextFormatAlign.RIGHT;
}
override protected function drawLayout():void
{
textField.setTextFormat(tf);
super.drawLayout();
}
}
}
However, changing the value of textField.x had no effect. I figured this was because I was calling super.drawLayout() at the end of my overridden version, which apparently changed the value back to the default. If I called it at the start, changing the x worked (I'm using -50 here as a noticeable example):
override protected function drawLayout():void
{
super.drawLayout();
textField.setTextFormat(tf);
textField.x = -50;
}
However, if I tried to set the value to this.width - textField.width, as Gio suggested should give the desired result, it only increased the x by 5 pixels.
I discovered that the problem was, again, super.drawLayout(). It appears that it was resizing the textField's width to fit inside the column, which is why my calculation didn't work. I tried to remove it the function, but that was a big mess since it handled a lot of code necessary for the column to display properly. So instead, I figured to make the textField resize itself automatically to its proper width, even when super.drawLayout() resized it, using the autoSize property:
override protected function drawLayout():void
{
super.drawLayout();
textField.setTextFormat(tf);
textField.autoSize = TextFieldAutoSize.LEFT;
textField.x = this.width - textField.width;
}
Now it worked, although with a few hitches. These were minimal though, and were fixed by moving the lines defining text format and autosize to the constructor of the CellRenderer (which I realised I should have done in the first place anyway). In the end, my final, functional class was this:
package
{
import fl.controls.listClasses.CellRenderer;
import fl.controls.listClasses.ICellRenderer;
import flash.text.TextFormat;
import flash.text.TextFormatAlign;
import flash.text.TextFieldAutoSize;
public class RightAlignCell extends CellRenderer implements ICellRenderer
{
private var tf:TextFormat;
public function RightAlignCell()
{
tf = new TextFormat();
tf.align = TextFormatAlign.RIGHT;
textField.setTextFormat(tf);
textField.autoSize = TextFieldAutoSize.LEFT;
}
override protected function drawLayout():void
{
super.drawLayout();
textField.x = this.width - textField.width - 5;
}
}
}
The subtraction of 5 pixels to textField.x is just a bit of positioning I did to make the text look better, instead of touching the border of the column.
Sorry for such a long post while the solution is simple, but I decided to explain it in-depth. Basically I just came up with the class above, which worked.

Override get width or height for just one component- Flex Mobile

Just a little background on what I am trying to do. I have a custom skin where I have a stylable text field to display the date. When clicking on the stylable text field, which is binded to the date, a date spinner comes up. Behind the datespinner I draw a sprite which needs to cover the whole screen so I can detect a click and make the datespinner go away.
Now the problem-
Without overriding the get width or get height I haven't been able to fill the whole screen. However when I do this the datespinner breaks because its getting the height from the override. Just wondering if anyone knew a way to override just one component and set all others to their default values. I know this might seem like a noob question and maybe I am missing something obvious, I am mostly new to as3.
Here is some code-
override protected function createChildren():void {
super.createChildren();
if(!img) {
img = new dateButtonBG();
addChild(img);
}
if (!maskSprite) {
maskSprite = new Sprite();
maskSprite.graphics.beginFill(0xFFFFCC, .5);
maskSprite.graphics.drawRect(-((stage.height)/2),-((stage.width)/2), stage.width, stage.height);
maskSprite.graphics.endFill();
}
if(!dateButton) {
dateButton = new StyleableTextField();
todayDate = new Date();
BindingUtils.bindProperty(dateButton, "text", date, "selectedDate");
dateButton.addEventListener(MouseEvent.CLICK, onDateButtonClick);
addChild(dateButton);
}
invalidateDisplayList();
}
protected function removeSpinner( event:Event):void{
maskSprite.removeEventListener(MouseEvent.CLICK, removeSpinner);
removeChild(date);
removeChild(maskSprite);
}
protected function onDateButtonClick (event:Event):void {
addChild(maskSprite);
addChild(date);
maskSprite.addEventListener(MouseEvent.CLICK, removeSpinner);
}
override public function get width () : Number {
return _width;
}
override public function get height () : Number {
return _height;
}
The code is not complete but is just for getting my point across. Thanks for reading and all your help.
EDIT-
I figured out how to do it.Adding some information in case some one has the same problem-
Flex limits the size of your sprite( or any UI component) to the size of the container. If you try to go over the size of the container it just returns the size of the container. In my code-
override public function get width () : Number {
return _width;
}
override public function get height () : Number {
return _height;
}
This is commonly touted as a fix to go over the size of the container. This approach is flawed however because it overrides everything that asks for width and height. In this case it tries to make everything the size of _height and _width. For any skin that has more than 1 component this is a huge problem, either you can try to set sizes for items indivigually, which for me didn't work or find an alternate approach.
Here is what works-
public override function validateDisplayList():void {
super.validateDisplayList();
yourComponent.width = someConstantW;
yourComponent.height = someConstantH;
}
This is not going to be enough however. You might need to move your component outside of the container for this try-
override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void {
super.layoutContents(unscaledWidth, unscaledHeight);
setElementPosition(yourComponent, x, y);
}
Hopefully I saved someone a few hours of work :)
It's not quite clear from the code you have posted, but perhaps you should be overriding the updateDisplayList() method instead of the width and height.
updateDisplayList() is the Flex life cycle method that components implement to size and position their child objects.
updateDisplayList() is called by the Flex framework, and it passes in two values: unscaledWidth and unscaledHeight. Your implmentation of updateDisplayList() should honor those values and size/position the child objects accordingly. You should use other life cycle methods (shown below) to do the actual sizing/positioning.
Here's an example implementation to get you started. It may be off a little, as I don't fully understand the code snippet you provided.
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
// size/position the background (or whatever it is that should occupy the whole screen)
background.setLayoutBoundsPosition(0,0); // unnecessary because 0,0 is default
background.setLayoutBoundsSize(unscaledWidth, unscaledHeight);
// size/position the text in the center
var textWidth:Number = text.getPreferredBoundsWidth;
var textheight:Number = text.getPreferredBoundsHeight
text.setLayoutBoundsPosition( (unscaledWidth - textWidth)/2, (unscaledHeight - textHeight)/2 );
text.setLayoutBoundsSize(textWidth, textHeight); // probably not necessary
}