许多Flex HERO的移动应用例子详细介绍了如何制作RSS Reader,使用的是Spark List和LabelItemRenderer或者IconItemRenderer。由于使用的都是默认的IconItemRenderer,这些应用看起来都一个样事实上,通过继承IconItemRenderer,定制自己的Item Renderer组件,我们能够设计出多种多样的应用。本文将主要讲解如何定制化自己的IconItemRenderer。
本文基于一个Engadget新闻阅读器移动应用EngadgetAIR,使用Engadget的RSS(http://cn.engadget.com/rss)作为新闻的数据源,通过HttpService获取,并显示在一个Spark列表中。当然,我们要使用定制的IconItemRenderer:EngadgetItemRenderer。
关于ICONITEMRENDERER
图1: 默认的IconItemRenderer
IconItemRenderer (注:在早前的Flex HE?RO 预览版中的MobileIconItemRenderer已经被重命名为IconItemRenderer)是HERO新加入的item render(所谓的"列表项呈示器",如果你能明白中文翻译说的是啥的话),并且为移动应用做了优化。
在IconItemRenderer中,开发者可以指定title, message, icon和decorator属性。上图显示了在Spark列表中应用的默认IconItemRenderer。 IconItemRenderer应用了Flex SDK HERO中提供的许多新特性。比如,在Flex HERO中,BitmapImage引入了新的缩放功能。 通过设置新引入的 'scaleMode' 属性,开发者可以方便地配置扩展图像、填充内容区的方式。在我们接下来看到的例子中,将使用iconScaleMode="letterbox",以等比例的方式缩放显示图像。
使用SPARK LIST生成显示ENGADGET新闻的列表
使用Spark List创建新闻列表News.mxml
- News.mxml是Flex移动应用EngadgetAIR(在整理完毕后,我会将该应用的源码共享在本博中)中的一个视图(如果你不了解HERO移动应用开发架构,请参考Adobe Devnet中相关教程)。在该视图中,我们通过应用的Singleton类AppPM获取engaget RSS数据(即AppPM.getInstance.rss)。rss.items是包含了新闻列表的ArrayCollection,将被绑定在Spark List类型的新闻列表newsList的dataprovider(数据提供者)。
- 我们创建了封装IconItemRenderer(或者我们自定义的itemRenderer)的NewsItemRenderer组件,该组件负责呈现列表中的每条新闻。
封装Item Renderer的MXML组件NewsItemRenderer.mxml
- NewsItemRenderer.mxml实际上是一个继承自我们自定义Item renderer(EngadgetItemRenderer)的MXML组件(当然你也可以修改为继承默认IconItemRenderer或者LabelItemRenderer)。
- 一些值得注意的属性:
- 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()方法,具体解释见代码中注释。
-
{
-
// no need to call super.layoutContents() since we're changing how it happens here
-
// start laying out our children now
-
-
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;
-
setElementPosition (decoratorDisplay, decoratorX, decoratorY );
-
}
-
// 计算message的位置和尺寸。message同图片同宽,居中靠下。
-
messageWidth=myIconWidth;
-
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
-
// 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的现有高度
-
// 记住现有的item宽度
-
oldUnscaledWidth = unscaledWidth;
-
// 设置message的尺寸,message比屏幕宽度小paddingLeft,作为边距(此处可以设置新的style来允许定制)
-
setElementSize (messageDisplay, messageWidth -paddingLeft -paddingRight, oldPreferredMessageHeight ); </p >
-
//在message已经确定最终宽度后,获取其最终高度。
-
// 测试宽度改变后,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. visible= false;
-
rectShape. graphics. beginFill (0x000000, 0.7 );
-
rectShape. graphics. drawRect ( 0, 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 (FlexEvent.READY,onIconDisplayReady );
-
}
-
// 创建icon,并删除事件侦听器
-
override protected function destroyIconDisplay ( ) : void {
-
super.destroyIconDisplay ( );
-
if (progressBar & & progressBar. parent ) {
-
removeChild (progressBar )
-
progressBar= null;
-
}
-
iconDisplay. removeEventListener (FlexEvent.READY,onIconDisplayReady );
-
}
-
// 如果没有下载指示,则创建下载进度指示。这里需要判断再次添加侦听器,因为iconDisplay可能被重用,所以对应的侦听器事件已被删除
-
if ( !progressBar ) {
-
progressBar = new ProgressBar (iconDisplay );
-
addChild (progressBar );
-
if (iconDisplay & & !iconDisplay. hasEventListener (FlexEvent.READY ) ) {
-
iconDisplay. addEventListener (FlexEvent.READY,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/