许多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指定的图片对象。
- 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的尺寸和位置,完成布局。
- 创建EngadgetItemRenderer类,继承IconItemRenderer
- 重新布局;
- 绘制一个半透明的黑色背景区域来衬托白色的message
- 加入图片下载进度条
- {
- // 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;
- }
- }
- 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();
- }
- if(rectShape && rectHeight>0){
- rectShape.width=myIconWidth;
- rectShape.height=rectHeight;
- rectShape.x=0;
- rectShape.y=unscaledHeight-rectShape.height;
- rectShape.visible=true
- }
- // 创建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;
- }
- }
定制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为例,看看这些方法完成的主要工作:
定制IconItemRenderer的步骤
我们自定义IconItemRenderer实现后的效果如下图。
图2: 自定义IconItemRenderer
为了实现该效果,定制EngadgetItemRenderer.as需要完成如下工作:
下面我们逐一讲解。
重新布局:覆盖layoutContents方法
IconItemRenderer中,子组件的布局是在layoutContents()方法中完成的。为了重新布局,我们在EngadgetItemRenderer类中覆盖该方法,实现自定义布局。由于在我们的例子中,新的item render只显示icon、decorator和message,因此我们简化了layoutContents()方法。
下面列出了完整的layoutContents()方法,具体解释见代码中注释。
绘制半透明黑色背景
接下来,我们希望能够在message文字后面填充半透明黑色背景,来更清晰地衬托显示文字。这部分工作将要在createChildren()方法中完成。
注:
尽管我们在layoutComponents之后覆盖createChildren方法,但其实该方法在layoutComponents方法(由updateDisplayList方法调用,想想我们刚刚讲过的组件生命周期)之前执行。
覆盖createChildren()方法的代码如下:
如上代码所示,我们绘制了一个半透明黑色矩形,但是并没有制定其具体尺寸和位置。尺寸和位置需要在updateDisplayList()方法中完成,因为其依赖与其他子组件(即message和icon)的尺寸。
我们在上面的layoutContents()方法的尾部加入如下代码,完成半透明黑色矩形最终的尺寸设定和的位置布局。
加入图片下载进度指示
图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。
完成代码如下:
小结
到此为止,我们已经创建为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/