Adding multiline text support to LabelItemRenderer

LabelItemRenderer is the default base class for mobile optimized item renderers. It has a single StyleableTextField for a labelDisplay that truncates its text with an ellipsis mark when the text is too large to fit on one line. This post demonstrates how to change that behavior to reflow text onto multiple lines rather than truncate.

The messageField of an IconItemRenderer has multiline text support by default. You can check out the code in the layoutContents() and measure() methods to see how this is accomplished, but IconItemRenderer has a lot of code and some people have found it difficult to look through just to learn about reflowing text. Below I have provided a much simpler class that should provide a clear, specific example of text reflow.

General Approach:

  • override createLabelDisplay() – to turn on multiline and wordWrap properties
  • override measure() – to handle text reflow with an estimated size
  • override layoutContents() – to handle text reflow

Full comments inline:

package
{
    import mx.core.DPIClassification;
    import mx.core.mx_internal;
 
    import spark.components.LabelItemRenderer;
    import spark.components.supportClasses.StyleableTextField;
 
    use namespace mx_internal;
 
    /**
    * A subclass of LabelItemRenderer that allows for multi-line text rather
    * than the default single line truncation behavior.
    */
    public class MultilineLabelItemRenderer extends LabelItemRenderer
    {
        /**
         *  The width of the component on the previous layout manager 
         *  pass.  This gets set in updateDisplayList() and used in measure() on 
         *  the next layout pass.  This is so our "guessed width" in measure() 
         *  will be as accurate as possible since labelDisplay is multiline and 
         *  the labelDisplay height is dependent on the width.
         */
        private var oldUnscaledWidth:Number;
 
        /**
        * Constructor
        */
        public function MultilineLabelItemRenderer()
        {
            // provide an initial guess at the estimated component width based on DPI class
            if (applicationDPI == DPIClassification.DPI_320)
                oldUnscaledWidth = 640;
            else if (applicationDPI == DPIClassification.DPI_240)
                oldUnscaledWidth = 480
            else // 160 dpi
                oldUnscaledWidth = 320
        }
 
        /**
        * Turn on multiline and wordWrap properties of the labelDisplay
        */
        override protected function createLabelDisplay():void
        {
            super.createLabelDisplay();
            labelDisplay.multiline = true;
            labelDisplay.wordWrap = true;
        }
 
        /**
        * Upgrade measure to handle text reflow.
        */
        override protected function measure():void
        {
            super.measure();
 
            var horizontalPadding:Number = getStyle("paddingLeft") + getStyle("paddingRight");
            var verticalPadding:Number = getStyle("paddingTop") + getStyle("paddingBottom");
 
            // now we need to measure labelDisplay's height.  Unfortunately, this is tricky and 
            // is dependent on labelDisplay's width.  We use the old unscaledWidth as an 
            // estimate for the new one.  If this estimate is wrong then there is code in 
            // updateDisplayList() that will trigger a new measure pass to correct it.
            var labelDisplayEstimatedWidth:Number = oldUnscaledWidth - horizontalPadding;
            setElementSize(labelDisplay, labelDisplayEstimatedWidth, NaN);
 
            measuredWidth = getElementPreferredWidth(labelDisplay) + horizontalPadding;
            measuredHeight = getElementPreferredHeight(labelDisplay) + verticalPadding; 
        }
 
        /**
        * Upgrade layoutContents to handle text reflow.
        */
        override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void
        {
            if (!labelDisplay)
                return;
 
            var paddingLeft:Number   = getStyle("paddingLeft"); 
            var paddingRight:Number  = getStyle("paddingRight");
            var paddingTop:Number    = getStyle("paddingTop");
            var paddingBottom:Number = getStyle("paddingBottom");
            var verticalAlign:String = getStyle("verticalAlign");
 
            var viewWidth:Number  = unscaledWidth  - paddingLeft - paddingRight;
            var viewHeight:Number = unscaledHeight - paddingTop  - paddingBottom;
 
            var vAlign:Number;
            if (verticalAlign == "top")
                vAlign = 0;
            else if (verticalAlign == "bottom")
                vAlign = 1;
            else // if (verticalAlign == "middle")
                vAlign = 0.5;
 
            if (label != "")
            {
                labelDisplay.commitStyles();
            }
 
            // Size the labelDisplay 
 
            // we want the labelWidth to be the viewWidth and then we'll calculate the height
            // of the text from that
            var labelWidth:Number = Math.max(viewWidth, 0);
 
            // keep track of the old label height
            var oldPreferredLabelHeight:Number = 0;
 
            // We get called with unscaledWidth = 0 a few times...
            // rather than deal with this case normally, 
            // we can just special-case it later to do something smarter
            if (labelWidth == 0)
            {
                // if unscaledWidth is 0, we want to make sure labelDisplay is invisible.
                // we could set labelDisplay's width to 0, but that would cause an extra 
                // layout pass because of the text reflow logic.  To avoid that we can 
                // just set its height to 0 instead of setting the width.
                setElementSize(labelDisplay, NaN, 0);
            }
            else
            {
                // grab old height before we resize the labelDisplay
                oldPreferredLabelHeight = getElementPreferredHeight(labelDisplay);
 
                // keep track of oldUnscaledWidth so we have a good guess as to the width 
                // of the labelDisplay on the next measure() pass
                oldUnscaledWidth = unscaledWidth;
 
                // set the width of labelDisplay to labelWidth.
                // set the height to old label height.  If the height's actually wrong, 
                // we'll invalidateSize() and go through this layout pass again anyways
                setElementSize(labelDisplay, labelWidth, oldPreferredLabelHeight);
 
                // grab new labelDisplay height after the labelDisplay has taken its final width
                var newPreferredLabelHeight:Number = getElementPreferredHeight(labelDisplay);
 
                // if the resize caused the labelDisplay's height to change (because of 
                // text reflow), then we need to re-measure ourselves with our new width
                if (oldPreferredLabelHeight != newPreferredLabelHeight)
                    invalidateSize();
 
            }
 
            // Position the labelDisplay
 
            var labelY:Number = Math.round(vAlign * (viewHeight - oldPreferredLabelHeight))  + paddingTop;
            setElementPosition(labelDisplay, paddingLeft, labelY);
        }
 
    }
}

Note: This sample requires the final release build of Flex 4.5 or later.

11 thoughts on “Adding multiline text support to LabelItemRenderer”

  1. Such a shame that labelDisplay is typed as TextBase instead of Adobe providing us with an interface for components that can display text. In the past we’ve needed the text to be selectable, and because there no interface this means cloning the whole class, as RichEditableText doesn’t extend TextBase.

  2. @Tink – LabelItemRenderer.labelDisplay is actually typed as StyleableTextField. I assume you mean ItemRenderer.labelDisplay?

  3. I’ve been trying to use your code to extend spark.skins.mobile.ButtonSkin and make it multiline. Unfortunately I’ve had no success! Please could you help me on this?

  4. Hi, thanks for this example.

    It seems that there’s no clear cut way of limiting a StylableTextField to a number of lines, as you can in Label. Is this due to performance, or just an omission on Flex’s part?

    I guess for now, I should use Label for 2 line displays in ItemRenderers, though this would mean a performance hit…?

  5. @Tim John – StyleableTextField is a thin wrapper around Flash’s TextField that adds support for simple styling. It is very barebones and doesn’t have some of the features of the spark Label. Moving from StyleableTextField to Label might not be a large performance hit depending on your use case. You could try it while using the tips in this post to get the best possible performance: http://flexponential.com/2011/10/05/performance-tuning-mobile-flex-applications/.

  6. This code worked perfectly for me. Just what i was looking for. Dissapointing that it takes something so extensive just to have a word wrap.

  7. This also works in IconItemRenderer as well, but doing so seems to eliminate the decorator icon. Is there anyway to preserve the decorator as well??

  8. @Jeremy Keczan – The messageField of IconItemRenderer already supports multiline text. If you are subclassing that you will need to make sure the size and position of the iconDisplay is still being set properly.

  9. This is so handy. But I have trouble combining the this MultilineLabelItemRenderer with the other “LabelItemRenderer looks like iTunes on the iPad”? Would you please post another renderer that does both?

  10. Hi! Thanks for the post!
    One question: why do I need to use this code first
    var labelDisplayEstimatedWidth:Number = oldUnscaledWidth – horizontalPadding;
    setElementSize(labelDisplay, labelDisplayEstimatedWidth, NaN);

    and not just use this code to apply measuredHeight
    labelDisplay.height + messageDisplay.height + verticalPadding; //used in IconItemRenderer

    I guess it has something to do with scaling, but not sure.
    Thanks!

Comments are closed.