- labelField、messageField、iconField和decorator分别定义了将要在item中显示的标签、描述、图标和装饰图标。
- iconScaleMode:Spark IconItemRenderer使用了BitmapImage处理icon,iconScaleMode定义了一个image的缩放行为。iconScaleMode有两个属性值BitmapScaleMode.STRETCH("stretch")和BitmapScaleLETTERBOX(letterbox)。设置为"stretch"时,图片将被拉伸填充,而如果设置为"letterbox",图表将按照原始尺寸等比例调整填充。具体内容可以参见http://opensource.adobe.com/wiki/display/flexsdk/Spark+Image*
- iconPlaceholder:在加载外部图片资源时,有时候会希望显示一个默认图片。一旦设置iconPlaceholder,加载外部图片时,IconItemRenderer就会在icon位置显示iconPlaceholder指定的图片对象。
定制IconItemRenderer: EngadgetItemRenderer.as
创建继承IconItemRenderer类的EngadgetItemRenderer.as。
组件的生命周期
在开始创建自定义Item Renderer类EngadgetItemRenderer之前,我们需要解释一下Flex组件的生命周期。所有UIComponent的子类,都遵从同样的生命周期,Flex框架通过自动调用createChildren()、commitProperties()、measure()和updateDisplayList()方法来管理组件。
Flex调用createChildren()在组件自身内部创建子组件,但是对于一些数据驱动的组件或者动态组件(这些组件随着生命周期的变化其属性,比如尺寸属性,会发生变化),通常会延迟(defer)在之后的commitProperties()方法中创建。比如在EnadgetItemRenderer中,createChildren()方法绘制了黑色半透明矩形rectShape,LabelItemRenderer中,createChildren()方法创建了label子组件。然而,在IconItemRenderer中,icon、message和decorator这些子组件都被延迟在commitProperties()方法中创建。开发者根据具体情况来判断在哪个方法中创建子组件。在计算组件的尺寸(measure方法)和进行组件布局(updateDisplayList)之前,Flex框架会调用commitProperties()方法,这个方法用来计算并把变化的值设定给属性以及相关数据。在commitProperties方法中,我们也会根据属性值的变化销毁需要移除的子组件。在属性发生变化时。我们通常会调用invalidateSize()(对应measure方法)和invalidateDispalyList()(对应updateDisplayList)方法,通知Flex框架来调用measure方法或者updateDisplayList()方法。Flex框架会根据具体情况来决定何时调用。measure方法用来计算组件的"自然"尺寸和最小尺寸,当组件的子组件尺寸发生变化时,Flex框架也会隐式的调用该方法。而updateDisplayList则根据组件的布局规则计算各组件位置并布局。
我们以IconItemRenderer为例,看看这些方法完成的主要工作:
- createChildren():IconItemRenderer的createChildren方法实际上没有做任何事情,只是调用了父组件LabelItemRenderer的createChildren方法。其子组件icon、message和decorator都被延迟到commitProperties方法中完成创建。至于label子组件,由于IconItemRenderer继承自LabelItemRenderer,因此其由LabelItemRenderer的createChildren方法创建。
- commitProperties():IconItemRenderer在commitProperties方法中,分别调用了createMessageDispaly()、createIconDispaly()和createDecoratorDisplay()创建了需要显示的message、icon和decorator子组件。
- measure():计算这些需要显示子组件的尺寸,组件的尺寸会根据显示内容(比如图片的大小、message内容)的不同发生变化。
- updateDisplayList():调用父类LabelItemRenderer的drawBackground()绘制背景,之后调用layoutContents()方法设定label、icon、decorator和message的尺寸和位置,完成布局。
定制IconItemRenderer的步骤
我们自定义IconItemRenderer实现后的效果如下图。

图2: 自定义IconItemRenderer
为了实现该效果,定制EngadgetItemRenderer.as需要完成如下工作:
- 创建EngadgetItemRenderer类,继承IconItemRenderer
- 重新布局;
- 绘制一个半透明的黑色背景区域来衬托白色的message
- 加入图片下载进度条
下面我们逐一讲解。
重新布局:覆盖layoutContents方法
IconItemRenderer中,子组件的布局是在layoutContents()方法中完成的。为了重新布局,我们在EngadgetItemRenderer类中覆盖该方法,实现自定义布局。由于在我们的例子中,新的item render只显示icon、decorator和message,因此我们简化了layoutContents()方法。
下面列出了完整的layoutContents()方法,具体解释见代码中注释。
-
override
protected
function layoutContents
(unscaledWidth
:
Number,unscaledHeight
:
Number
)
:
void
-
{
-
// no need to call super.layoutContents() since we're changing how it happens here
-
// start laying out our children now
-
var myIconWidth
:
Number =
0;
-
var myIconHeight
:
Number =
0;
-
var decoratorWidth
:
Number =
0;
-
var decoratorHeight
:
Number =
0;
-
var decoratorY
:
Number=
0;
-
var decoratorX
:
Number=
0;
-
var messageWidth
:
Number =
0;
-
var messageHeight
:
Number =
0;
-
var messageY
:
Number=
0;
-
var hasMessage
:
Boolean = messageDisplay
&
& messageDisplay.
text
!=
"";
-
-
var paddingLeft
:
Number =
getStyle
(
"paddingLeft"
);
-
var paddingRight
:
Number =
getStyle
(
"paddingRight"
);
-
var paddingTop
:
Number =
getStyle
(
"paddingTop"
);
-
var paddingBottom
:
Number =
getStyle
(
"paddingBottom"
);
-
var horizontalGap
:
Number =
getStyle
(
"horizontalGap"
);
-
var verticalGap
:
Number =
(hasMessage
)
?
getStyle
(
"verticalGap"
)
:
5;
-
if
(iconDisplay
)
-
{
-
this.iconWidth=unscaledWidth;
-
// 设置图标的尺寸:宽度与手机屏幕同宽。由于我们设置iconDisplay为letterbox,因此图片会自动等比例缩放
-
setElementSize
(iconDisplay,this.iconWidth,this.iconHeight
);
-
myIconWidth = iconDisplay.getLayoutBoundsWidth
(
);
//实际上,myIconWidth就是item的宽度
-
myIconHeight = iconDisplay.getLayoutBoundsHeight
(
);
//myIconHeight就是item的高度
-
//设置图片的位置,x=0,y=0>
-
setElementPosition
(iconDisplay,0,0
);
-
}
-
// decorator的位置居中,靠右
-
if
(decoratorDisplay
)
-
{
-
decoratorWidth = getElementPreferredWidth
(decoratorDisplay
);
-
decoratorHeight = getElementPreferredHeight
(decoratorDisplay
);
-
//设定decorator的尺寸
-
setElementSize
(decoratorDisplay,decoratorWidth,decoratorHeight
);
-
// decorator居中,靠右
-
decoratorX=unscaledWidth
- decoratorWidth;
-
decoratorY =
Math.
round
(
(unscaledHeight
- decoratorHeight
)
/
2
) ;
-
setElementPosition
(decoratorDisplay,decoratorX,decoratorY
);
-
}
-
// 计算message的位置和尺寸。message同图片同宽,居中靠下。
-
messageWidth=myIconWidth;
-
var messageTextHeight
:
Number =
0;
-
if
(hasMessage
)
-
{
-
// commit styles to make sure it uses updated look
-
messageDisplay.commitStyles
(
);
-
}
-
if
(hasMessage
)
-
{
-
// handle message...because the text is multi-line,measuring and layout
-
// can be somewhat tricky
-
messageWidth =
Math.
max
(messageWidth,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
(messageWidth ==
0
)
-
{
-
// if unscaledWidth is 0,we want to make sure messageDisplay is invisible.
-
// we could set messageDisplay's width to 0,but that would cause an extra
-
// layout pass because of the text reflow logic. Because of that,we
-
// can just set its height to 0.
-
setElementSize
(messageDisplay,NaN,0
);
-
}
-
else
-
{
-
// 在resize之前,获取messageDisplay的现有高度
-
var oldPreferredMessageHeight
:
Number = getElementPreferredHeight
(messageDisplay
);
-
// 记住现有的item宽度
-
oldUnscaledWidth = unscaledWidth;
-
// 设置message的尺寸,message比屏幕宽度小paddingLeft,作为边距(此处可以设置新的style来允许定制)
-
setElementSize
(messageDisplay,messageWidth
-paddingLeft
-paddingRight,oldPreferredMessageHeight
);
</p
>
-
//在message已经确定最终宽度后,获取其最终高度。
-
var newPreferredMessageHeight
:
Number = getElementPreferredHeight
(messageDisplay
);
-
// 测试宽度改变后,message文本重新布局得到的最终高度与原来高度是否相同,如果不同,则需要调度measure()重新计算item的尺寸
-
if
(oldPreferredMessageHeight
!= newPreferredMessageHeight
)
-
invalidateSize
(
);
-
//记录获取到的message
-
messageHeight = newPreferredMessageHeight;
-
}
-
//设置message的位置:居中,靠下但留下verticalGap大小的下边距
-
messageY=unscaledHeight
-messageHeight
-verticalGap;
-
setElementPosition
(messageDisplay,paddingLeft,messageY
);
-
//设置message黑色背景尺寸
-
rectHeight=messageHeight
+verticalGap
*
2;
-
}
-
}
绘制半透明黑色背景
接下来,我们希望能够在message文字后面填充半透明黑色背景,来更清晰地衬托显示文字。这部分工作将要在createChildren()方法中完成。
注:
尽管我们在layoutComponents之后覆盖createChildren方法,但其实该方法在layoutComponents方法(由updateDisplayList方法调用,想想我们刚刚讲过的组件生命周期)之前执行。
覆盖createChildren()方法的代码如下:
-
override
protected
function createChildren
(
)
:
void
-
{
-
if
(
!rectShape
)
{
-
rectShape=
new
Shape
(
);
-
rectShape.
visible=
false;
-
rectShape.
graphics.
beginFill
(0x000000,0.7
);
-
rectShape.
graphics.
drawRect
(
0,1,1
);
//此处暂不指定真实尺寸和位置
-
this.
addChild
(rectShape
);
-
}
-
super.createChildren
(
);
-
}
如上代码所示,我们绘制了一个半透明黑色矩形,但是并没有制定其具体尺寸和位置。尺寸和位置需要在updateDisplayList()方法中完成,因为其依赖与其他子组件(即message和icon)的尺寸。
我们在上面的layoutContents()方法的尾部加入如下代码,完成半透明黑色矩形最终的尺寸设定和的位置布局。
-
if
(rectShape
&
& rectHeight
>
0
)
{
-
rectShape.
width=myIconWidth;
-
rectShape.
height=rectHeight;
-
rectShape.
x=
0;
-
rectShape.
y=unscaledHeight
-rectShape.
height;
-
rectShape.
visible=
true
-
}
加入图片下载进度指示

图3:EngadgetItemRenderer 中的图片下载进度指示
在图片下载过程中,IconItemRenderer会显示iconPlaceholder属性指定的嵌入图片对象(如果指定了iconPlaceholder的话)。但是IconItemRenderer并没有显示图片的下载状态,对于移动应用来说,由于网络连接速度的限制,有的时候应用会因此显得似乎没有响应。接下来,我们将为自定义的EngadgetItemRenderer加入下载进度指示。
我们的进度指示组件是一个继承了UIComponent的as3类,我们不在这里介绍如何制作进度指示,下面代码中ProgressBar即为该进度指示组件,该组件将接收要显示下载进度的BitmapImage对象(本例中就是iconDisplay,负责显示icon),通过bytesLoaded和bytesTotal属性,以及BitmapImage对象的PROGRESS类型的ProgressEvent事件与READY类型的FlexEvent事件管理下载进度显示。
这里需要解决的是,如何在EngadgetItemRenderer中添加ProgressBar,并及时销毁。
在自定义的EngadgetItemRenderer中,我们并没有在createChildren()方法中创建progressBar。这是由于Spark List组件并不是为List中的每一个项目生成一个IconItemRenderer实例,当滚动屏幕时,List会重用已经创建的
IconItemRenderer绘制对应的Item。因此,如果我们在createChildren方法中创建progressBar,就会漏掉那些被重用的Item Render(因为Flex框架不会调用createChildren方法来创建已经被销毁的progressBar)。这些Item就不会显示下载进度指示。
IcomRenderer使用了createIconDisplay()方法来创建icon,使用destroyIconDisplay()方法来销毁icon。我们就借助这两个方法在其中为icon加入或者删除PROGRESS类型的ProgressEvent事件侦听器onIconDisplayProgress方法及READY类型的FlexEvent事件侦听器onIconDisplayReady方法。在onIconDisplayProgress方法中,每当图片开始加载,我们就实例化progressBar,并将其加入显示列表displayList。在onIconDisplayReady方法中,在下载完成后,我们就从displayList中删除该progressBar。
完成代码如下:
-
// 创建icon,并添加事件侦听器以创建下载显示进度指示
-
override
protected
function createIconDisplay
(
)
:
void
{
-
super.createIconDisplay
(
);
-
iconDisplay.
addEventListener
(
ProgressEvent.
PROGRESS,onIconDisplayProgress
);
-
iconDisplay.
addEventListener
(FlexEvent.READY,onIconDisplayReady
);
-
}
-
// 创建icon,并删除事件侦听器
-
override
protected
function destroyIconDisplay
(
)
:
void
{
-
super.destroyIconDisplay
(
);
-
if
(progressBar
&
& progressBar.
parent
)
{
-
removeChild
(progressBar
)
-
progressBar=
null;
-
}
-
iconDisplay.
removeEventListener
(
ProgressEvent.
PROGRESS,onIconDisplayProgress
);
-
iconDisplay.
removeEventListener
(FlexEvent.READY,onIconDisplayReady
);
-
}
-
// 如果没有下载指示,则创建下载进度指示。这里需要判断再次添加侦听器,因为iconDisplay可能被重用,所以对应的侦听器事件已被删除
-
private
function onIconDisplayProgress
(event
:
ProgressEvent
)
:
void
{
-
if
(
!progressBar
)
{
-
progressBar =
new ProgressBar
(iconDisplay
);
-
addChild
(progressBar
);
-
if
(iconDisplay
&
&
!iconDisplay.
hasEventListener
(FlexEvent.READY
)
)
{
-
iconDisplay.
addEventListener
(
ProgressEvent.
PROGRESS,onIconDisplayReady
);
-
}
-
}
-
}
-
// 删除下载进度指示
-
private
function onIconDisplayReady
(event
:FlexEvent
)
:
void
{
-
if
(progressBar
&
& progressBar.
parent
)
{
-
removeChild
(progressBar
);
-
progressBar=
null;
-
}
-
}
小结
到此为止,我们已经创建为EngadgetAIR应用基于IconItemRenderer创建了新的EngadgetItemRenderer。希望能你通过这个例子能够更好的理解IconItemRenderer以及Flex组件的生命周期。
本例来自于正在开发的一个Flex移动应用示例EngadgetAIR,在完成第一阶段的全部开发工作之后,我会把该应用和源码分享在这个博客以及我的新浪微博中。如果您希望了解更多,可以关注我的微博和博客。
ownload: EngadgetMobileAIR.fxp
NOTES: 这个应用还没有完成,代码仅供学习参考。我会持续更新这个应用。
关于作者

董龙飞
http://t.sina.com.cn/donglongfei
?
zhuanzi:http://wolfgangkiefer.blog.163.com/blog/static/86265503201151873812258/