ActionScript移动项目组件开发(2):可滚动的容器

内容简介

这篇文章是上一篇《 ActionScript移动项目组件开发(1):可滚动容器 》的延续。在上一篇文章中,我们继承Sprite,封装了一个可滚动的容器,并且这个容器的交互方式是适用于移动设备的,因此我们可以使用这个容器,在移动项目中处理大尺寸的显示对象(比如一张风景图)。还有一种很普遍的需求,就是列表组件。比如我们要展现一个RSS数据源,那么列表是非常适合的一种方式。而列表组件则是一个典型的MVC模式的实施,Model就是列表组件赖以生成显示对象的数据源(数组,XML,或其它类型),View则是展现给用户的列表视图(显示对象),而Controller呢,则是响应用户的交互,比如选中某一项,或对列表进行滚动等行为控制。如果您之前使用过Flex框架,应该不会对这个组件感到陌生。Flex的组件体系中就包括了列表(List),而且在Flex 4.5 SDK中,列表组件还专门为移动类型的项目单独实现了一个皮肤,以便适应移动设备的交互方式,如下图所示:

1

图1:Flex 4.5中适用于移动设备的列表外观

和上一篇文章我们讨论的情况类似,如果是纯ActionScript的移动项目,就没有一个这样官方提供的组件可用。在这篇文章里,我们将探讨如何去实现这个列表组件。这个组件将具备什么样的特性呢?

首先,它和Flex中的列表组件行为非常类似,可用设置一个数组作为数据源(dataProvider),指定一个项渲染器(ItemRenderer),显示出可视化和可交互的列表(交互方式和ScrollableContainer类似,也是基于拖动),并且允许每一行不同的高度,见下面的示例:

然后,它也可以适应大数据量的情况(比如1万条数据),优化的关键就在于它对“虚拟滚动”的支持,参见这个例子(1万条数据):

这些源码已经放在瑞研社区(RIADEV)的开源项目RIASamples中,大家可以用SVN检出所有的源码。

SVN地址:

http://svn.riadev.com/riasamples/trunk/source/riadev-mobile-lib/

前置知识

您需要具备基础的ActionScript 3编程经验,并对于移动项目的特点(屏幕尺寸,交互方式,触碰事件)有一定的了解。

所需软件

您需要下载和安装下面的软件,以便运行这篇文章的相关源码:

实现过程

创建组件类

列表组件我们命名为ScrollableList,继承自Sprite:

  1. public class ScrollableList extends Sprite
复制代码

列表中如果显示对象的尺寸超出了约束尺寸,也要实现滚动,这里可以看到,上一篇文章中我们创建的ScrollableContainer就可以派上用场了,我们在列表组件中放置这个容器的实例,以承载根据数据源生成的显示对象:

  1. /**用可滚动的容器来承载数据项*/
  2. protected var container:ScrollableContainer;
复制代码

在继续之前,我们先来思考一个问题,在展现方式上,ScrollableList和ScrollableContainer有何差异?基于Flash Player默认的坐标体系,添加到ScrollableContainer的显示对象实例,都将根据自己的x和y坐标来定位。但对于ScrollableList,我们希望它包含的显示对象的定位,由组件来统一管理,实现一个垂直排列的效果。这个实现过程其实不难,代码也不会很长,但问题是,这些代码我们要耦合到组件中吗?一旦之后我们要做水平排列的列表组件,或表格式的列表组件,因为布局的代码是耦合在组件中的,我们就无法轻松改变组件的一些属性来实现不同的布局,这是可扩展性的一个隐忧。这里我们仿照Flex Spark组件体系的一些设计思想,将对组件内部显示对象位置的管理,抽象为布局类和布局接口。这样将增强组件的可扩展性:

  • ScrollableList + HorizontalLayout = HorizontalScrollableList
  • ScrollableList + TileLayout = TileScrollableList

布局类的接口:ILayout,只包含一个方法layoutChildren,接受包含若干显示对象的Sprite容器,按照预定的规则排列这些显示对象:

  1. public interface ILayout
  2. {
  3. /**布局子元件*/
  4. function layoutChildren(container:Sprite,parentWidth:Number,parentHeight:Number):void;
  5. }
复制代码

而列表式的垂直布局类VerticalLayout,则实现了这个接口:

  1. public class VerticalLayout implements ILayout
  2. {
  3. /**如果设置为true,将自动调整子元件的宽度为容器宽度*/
  4. public var matchContainerWidth:Boolean = false;
  5. /**项之间的间距*/
  6. public var gap:Number = 0;
  7. /**
  8. * 构造方法
  9. * @param matchContainerWidth
  10. * @param gap
  11. *
  12. */
  13. public function VerticalLayout(matchContainerWidth:Boolean=false,gap:Number=0)
  14. {
  15. this.matchContainerWidth = matchContainerWidth;
  16. this.gap = gap;
  17. }
  18. /**
  19. * 布局子元件
  20. * @param container
  21. * @param parentWidth
  22. * @param parentHeight
  23. *
  24. */
  25. public function layoutChildren(container:Sprite, parentWidth:Number, parentHeight:Number):void
  26. {
  27. var num:Number = container.numChildren;
  28. var currentY:Number = 0;
  29. for (var i:int = 0; i < num; i++)
  30. {
  31. var child:DisplayObject = container.getChildAt(i);
  32. child.x = 0;
  33. child.y = currentY;
  34. if(matchContainerWidth)
  35. child.width = parentWidth;
  36. currentY = child.y + child.height + gap;
  37. }
  38. }
  39. }
复制代码

然后我们要修改一下ScrollableContainer,使得它可以接受一个布局类的实例,并在更新显示列表的时候,让布局类实例去管理子显示对象的布局。

普通浏览 复制代码
  1. /**@private*/
  2. private var _layout:ILayout ;
  3. /**布局类实例,控制子元件的排列方式*/
  4. public function get layout ( ):ILayout
  5. {
  6.      return _layout ;
  7. }
  8. public function set layout (value:ILayout ): void
  9. {
  10.      if (_layout == value )
  11.          return ;
  12.     _layout = value ;
  13.     needUpdate =  true ;
  14. }

然后要修改一下ScrollableContainer的measure这个方法,在度量之前,先让布局类实例去完成对子元件的定位:

  1. if(_layout != null)
  2. _layout.layoutChildren(contentGroup,containerWidth,containerHeight);
复制代码

而这个布局类的实例,则是由ScrollableList创建的,所以回到ScrollableList类,定义一个变量,并在

  1. /**布局类*/
  2. protected var layout:ILayout;
  3. protected function createChildren():void
  4. {
  5. //...
  6. layout = new VerticalLayout(true);
  7. container.layout = layout;
  8. //...
  9. }
复制代码

然后是项目渲染器的定义。这也是Flex体系中的列表中的一个概念,所谓项目渲染器,就是一个显示类,当列表得到对它的引用,就可以在运行时根据数据动态创建这个类的实例,所以这里定义一个对项目渲染器的引用,类型则是Class。

  1. /**项目渲染器类*/
  2. public var itemRenderer:Class;
复制代码

那么一个项目渲染器应该具备什么样的属性和方法呢?我们可以定义一个接口来约束:它可以接受一个数据对象,具备两个状态(选中/非选中),并且可以派发事件:

  1. public interface IItemRenderer extends IEventDispatcher
  2. {
  3. function get data():Object;
  4. function set data(value:Object):void;
  5. function get selected():Boolean;
  6. function set selected(value:Boolean):void;
  7. function clear():void;
  8. }
复制代码

用户在使用列表的时候,应该根据自己的实际需要,去创建一个项目渲染器并实现这个接口。

补充一点:基于性能考虑,建议用户在自己的项目渲染器中实现两个静态方法:createInstance 和 reclaim,类似于工厂模式,createInstance用来创建这个ItemRenderer类的实例,而reclaim方法则负责回收用不到的实例,当创建实例的时候,优先从缓存里去取,当缓存没有的时候再创建新的对象,这样做的好处是当数据频繁更新的时候,对象可以得到最有效的利用,防止出现内存溢出的问题。

然后为列表创建一个dataProvider属性,类型是数组,这就是列表所依赖的数据源。当数据源被设置,列表就可以根据数据源数组的长度,决定创建多少个ItemRenderer的实例。

普通浏览 复制代码
  1. /**@private*/
  2. private var _dataProvider:Array ;
  3. /**数据源*/
  4. public function get dataProvider ( ):Array
  5. {
  6.      return _dataProvider ;
  7. }
  8. public function set dataProvider (value:Array ): void
  9. {
  10.     _dataProvider = value ;
  11.     createItemRenderer ( ) ;
  12. }
  13. /**
  14.  * 根据数据源,创建列表项
  15.  */        
  16. protected function createItemRenderer ( ): void
  17. {
  18.      //clear first
  19.     var currentItemNum:Number = container.numChildren ;
  20.      for  (var i: int =  0 ; i < currentItemNum ; i++ ) 
  21.     {
  22.          if ( (itemRenderer  as Object ).reclaim !=  null )
  23.              (itemRenderer  as Object ).reclaim (container.removeChildAt ( 0 ) ) ;
  24.     }
  25.      //create ItemRenderer
  26.     var child:IItemRenderer ;
  27.      for  (var j: int =  0 ; j < dataProvider.length ; j++ ) 
  28.     {
  29.          if ( (itemRenderer  as Object ).createInstance !=  null )
  30.             child =  (itemRenderer  as Object ).createInstance ( ) ;
  31.          else
  32.             child =  new itemRenderer ( ) ;
  33.         child.data = dataProvider [j ] ;
  34.          if (!child.hasEventListener (MouseEvent.MOUSE_DOWN ) )
  35.             child.addEventListener (MouseEvent.MOUSE_DOWN,itemClickHanlder ) ;
  36.         container.addChild (child  as DisplayObject ) ;
  37.     }
  38. }

注意上面的代码中,我们为每一个ItemRenderer实例都添加了事件侦听,这是为了根据点击的对象,确实列表当前那个数据被选中了,这是列表的一个基本特性。注意点击后的处理方法,我们做了一个特殊处理,原因是:移动设备的屏幕交互,MouseDown和MouseMove经常同时触发,造成判断错误,所以需要延迟对于点击事件的判断。

  1. /**
  2. * 当数据列表项被点击,设置selectedItem
  3. * @param event
  4. */
  5. protected function itemClickHanlder(event:Event):void
  6. {
  7. setTimeout(validateItemClick,200,event.currentTarget);
  8. }
  9. /**
  10. * 因为移动设备的屏幕,MouseDown和MouseMove经常同时触发,造成判断错误,所以需要延迟对于点击事件的判断
  11. * inScrollState是个布尔值,标记当前列表是否处于滚动状态,如何用户点击了某一项,同时触发了滚动,则“选择”这个操作是无效的,这样来避免事件的冲突
  12. * @param child
  13. */
  14. protected function validateItemClick(child:Object):void
  15. {
  16. if(inScrollState || selectedItem == child.data)
  17. return;
  18. selectedItem = child.data;
  19. var itemChangeEvent:DataChangeEvent = new DataChangeEvent(DataChangeEvent.ITEM_CHANGE);
  20. itemChangeEvent.selectedItem = _selectedItem;
  21. dispatchEvent(itemChangeEvent);
  22. }
复制代码

当selectedItem被设置,我们派发一个事件出去,这样列表的使用者就可以通过侦听这个事件来判断列表的哪一项被选中了,然后执行下一步操作。同样我们也要更改被选中的ItemRenderer实例的状态,由ItemRenderer类自己去控制状态变化引起的外观差异(比如不同的背景色)。

普通浏览 复制代码
  1. /**@private*/
  2. private var _selectedItem:Object ;
  3. /**当前选中项*/
  4. public function get selectedItem ( ):Object
  5. {
  6.      return _selectedItem ;
  7. }
  8. public function set selectedItem (value:Object ): void
  9. {
  10.      if (_selectedItem == value )
  11.          return ;
  12.     _selectedItem = value ;
  13.     var currentItemNum:Number = container.numChildren ;
  14.      for  (var i: int =  0 ; i < currentItemNum ; i++ ) 
  15.     {
  16.         var child:IItemRenderer = container.getChildAt (i )  as IItemRenderer ;
  17.          if (child.data == _selectedItem )
  18.             child.selected =  true ;
  19.          else
  20.             child.selected =  false ;
  21.     }
  22. }

对列表的初始化,自然是从列表的构造方法开始:

  1. /**
  2. * 构造方法
  3. * 这个方法只是做一些初始化的工作
  4. */
  5. public function ScrollableList(itemRenderer:Class,itemGap:Number=0)
  6. {
  7. this.itemRenderer = itemRenderer;
  8. this.itemGap = itemGap;
  9. createChildren();
  10. }
  11. /**
  12. * 创建内部所需的对象
  13. */
  14. protected function createChildren():void
  15. {
  16. container = new ScrollableContainer(useVirtualScroll);
  17. container.horizontalScrollEnabled = false;
  18. layout = new VerticalLayout(true,itemGap);
  19. container.layout = layout;
  20. super.addChild(container);
  21. }
复制代码

同时要注意,对于List,不能由用户代码添加显示对象,它不是容器,只能由我们的类内部方法决定如何来控制列表的显示列表结构。所以禁止操作下面的方法:

普通浏览 复制代码
  1. /**@private*/
  2. override  public function addChild (child:DisplayObject ):DisplayObject
  3. {
  4.      return throwError ( ) ;
  5. }
  6. /**@private*/
  7. override  public function addChildAt (child:DisplayObject, index: int ):DisplayObject
  8. {
  9.      return throwError ( ) ;
  10. }
  11. /**@private*/
  12. override  public function removeChild (child:DisplayObject ):DisplayObject
  13. {
  14.      return throwError ( ) ;
  15. }
  16. /**@private*/
  17. override  public function removeChildAt (index: int ):DisplayObject
  18. {
  19.      return throwError ( ) ;
  20. }
  21. /**抛出错误*/
  22. private function throwError ( ):*
  23. {
  24.      throw  new Error ( "List不是容器,禁止操作此方法" ) ;
  25.      return  null ;
  26. }

和ScrollableContainer类似,对于宽度和高度的定义我们也要重写(override),以保证产生正确的行为。

普通浏览 复制代码
  1. /**@private*/
  2. override  public function get width ( ):Number
  3. {
  4.      return container.width ;
  5. }
  6. override  public function set width (value:Number ): void
  7. {
  8.      if  (container.width == value )
  9.          return ;
  10.     container.width = value ;
  11. }
  12. /**@private*/
  13. override  public function get height ( ):Number
  14. {
  15.      return container.height ;
  16. }
  17. override  public function set height (value:Number ): void
  18. {
  19.      if  (container.height == value )
  20.          return ;
  21.     container.height=value ;
  22. }

OK,至此,列表的雏形已经完成。我们可以创建一个自定义的ItemRenderer,并生成一个数组存储数据,来测试列表的表现。下面的代码里,我们创建一个名为RSSItem的类来实现IItemRenderer接口,并加载一个RSS数据(XML格式),并加工成数组,传递给列表。

RSSItem.as

普通浏览 复制代码
  1. package renderer
  2. {
  3.     import com.riadev.mobile.ui.renderer.IItemRenderer ;
  4.     
  5.     import flash.display.GradientType ;
  6.     import flash.display.Graphics ;
  7.     import flash.display.Sprite ;
  8.     import flash.geom.Matrix ;
  9.     import flash.text.TextField ;
  10.     import flash.text.TextFormat ;
  11.      /**
  12.      * 这是一个显示RSS某一项数据的显示对象类,实现IItemRenderer接口
  13.      * @author NeoGuo
  14.      * 
  15.      */    
  16.      public  class RSSItem extends Sprite implements IItemRenderer
  17.     {
  18.          private var labelTitle:TextField ;
  19.          private var labelContent:TextField ;
  20.         
  21.          private var itemWidth:Number ;
  22.         
  23.          public function RSSItem ( )
  24.         {
  25.              //创建标题
  26.             labelTitle =  new TextField ( ) ;
  27.             labelTitle.width =  100 ;
  28.             labelTitle.height =  30 ;
  29.             labelTitle.x =  10 ;
  30.             labelTitle.y =  10 ;
  31.             var labelFormat:TextFormat =  new TextFormat ( ) ;
  32.             labelFormat.size =  20 ;
  33.             labelFormat.bold =  true ;
  34.             labelTitle.defaultTextFormat = labelFormat ;
  35.             labelTitle.selectable =  false ;
  36.             addChild (labelTitle ) ;
  37.              //创建摘要
  38.             labelContent =  new TextField ( ) ;
  39.             labelContent.width =  100 ;
  40.             labelContent.height =  20 ;
  41.             labelContent.x =  10 ;
  42.             labelContent.y =  30 ;
  43.             labelContent.wordWrap =  true ;
  44.             labelContent.multiline =  true ;
  45.             labelContent.selectable =  false ;
  46.             addChild (labelContent ) ;
  47.              //
  48.              this.mouseChildren =  false ;
  49.         }
  50.         
  51.          private var _data:Object ;
  52.         
  53.          public function get data ( ):Object
  54.         {
  55.              return _data ;
  56.         }
  57.         
  58.          public function set data (value:Object ): void
  59.         {
  60.             _data = value ;
  61.             labelTitle.text = data.t ;
  62.             labelContent.text = data.c ;
  63.              if (parent !=  null )
  64.                 drawBackGround ( ) ;
  65.         }
  66.         
  67.          private var _selected:Boolean ;
  68.         
  69.          public function get selected ( ):Boolean
  70.         {
  71.              return _selected ;
  72.         }
  73.         
  74.          public function set selected (value:Boolean ): void
  75.         {
  76.              if (_selected == value )
  77.                  return ;
  78.             _selected = value ;
  79.              if (parent !=  null )
  80.                 drawBackGround ( ) ;
  81.         }
  82.         
  83.          override  public function get width ( ):Number
  84.         {
  85.              return itemWidth ;
  86.         }
  87.          override  public function set width (value:Number ): void
  88.         {
  89.             itemWidth = value ;
  90.              if (parent !=  null )
  91.                 drawBackGround ( ) ;
  92.         }
  93.          /**
  94.          * 绘制背景
  95.          */        
  96.          public function drawBackGround ( ): void
  97.         {
  98.             labelTitle.width = itemWidth-20 ;
  99.             labelContent.width = itemWidth-20 ;
  100.             labelContent.height = labelContent.textHeight +  20 ;
  101.              //bg
  102.             var g:Graphics =  this.graphics ;
  103.             g.clear ( ) ;
  104.             var matrix:Matrix =  new Matrix ( ) ;
  105.             matrix.createGradientBox (itemWidth,height, 90, 0, 0 ) ;
  106.              if (_selected )
  107.                 g.beginGradientFill (GradientType.LINEAR, [ 0xFF0000, 0x00FF00 ], [ 1, 1 ], [ 1, 255 ],matrix ) ;
  108.              else
  109.                 g.beginGradientFill (GradientType.LINEAR, [ 0xFFFFFF, 0xCCCCCC ], [ 1, 1 ], [ 1, 255 ],matrix ) ;
  110.             g.drawRect ( 0, 0,itemWidth,height+ 10 ) ;
  111.             g.endFill ( ) ;
  112.         }
  113.         
  114.          public function clear ( ): void
  115.         {
  116.              //do nothing
  117.         }
  118.         
  119.     }
  120. }

然后我们创建一个主类,读取RSS数据,用列表来展现:

普通浏览 复制代码
  1. package
  2. {
  3.     import com.riadev.mobile.events.DataChangeEvent ;
  4.     import com.riadev.mobile.ui.container.ScrollableContainer ;
  5.     import com.riadev.mobile.ui.control.ScrollableList ;
  6.     import com.riadev.mobile.ui.renderer.IconItemRenderer ;
  7.     
  8.     import flash.display.Bitmap ;
  9.     import flash.display.Loader ;
  10.     import flash.display.Sprite ;
  11.     import flash.display.StageAlign ;
  12.     import flash.display.StageScaleMode ;
  13.     import flash.events.Event ;
  14.     import flash.events.MouseEvent ;
  15.     import flash.net.URLLoader ;
  16.     import flash.net.URLRequest ;
  17.     
  18.     import renderer.RSSItem ;
  19.     import net.hires.debug.Stats ;
  20.     
  21.      /**
  22.      * 测试列表
  23.      * @author NeoGuo
  24.      * 
  25.      */    
  26.      public  class ListDemo extends Sprite
  27.     {
  28.          private var marginValue: int =  20 ;
  29.          private var list:ScrollableList ;
  30.         
  31.          private var feedURL:String =  "assets/rss.xml" ;
  32.          private var data: XML ;
  33.          private var testData:Array =  [ ] ;
  34.         
  35.          public function ListDemo ( )
  36.         {
  37.             super ( ) ;
  38.             stage.align = StageAlign.TOP_LEFT ;
  39.             stage.scaleMode = StageScaleMode.NO_SCALE ;
  40.             var loader:URLLoader =  new URLLoader ( ) ;
  41.             loader.load ( new URLRequest (feedURL ) ) ;
  42.             loader.addEventListener (Event.COMPLETE,dataComplete ) ;
  43.         }
  44.         
  45.          /**数据加载完成,生成数组*/
  46.          protected function dataComplete ( event:Event ): void
  47.         {
  48.             var data: XML =  new  XML ( event.currentTarget.data ) ;
  49.             testData =  [ ] ;
  50.             var count: int = data.channel.item.length ( ) ;
  51.              for (var i: int =  0 ;i < count ; i++ )
  52.             {
  53.                 var itemNode: XML = data.channel.item [i ] ;
  54.                 var obj:Object = {t:itemNode.title,c:itemNode.description} ;
  55.                 testData.push (obj ) ;
  56.             }
  57.             initApp ( ) ;
  58.         }
  59.         
  60.          /**初始化应用,创建列表*/
  61.          private function initApp ( ): void
  62.         {
  63.             list =  new ScrollableList (RSSItem, 1 ) ;
  64.             list.x = marginValue ;
  65.             list.y = marginValue ;
  66.             list.setStyle ( "borderColor", 0xCCCCCC ) ;
  67.             list.setStyle ( "bgColor", 0x000000 ) ;
  68.             list.setStyle ( "bgAlpha", 0 .5 ) ;
  69.             list.dataProvider = testData ;
  70.             list.addEventListener (DataChangeEvent.ITEM_CHANGE,itemChangeHandler ) ;
  71.             addChild (list ) ;
  72.             layoutChildren ( ) ;
  73.             stage.addEventListener (Event.RESIZE,layoutChildren ) ;
  74.             addChild ( new Stats ( ) ) ;
  75.         }
  76.         
  77.          protected function itemChangeHandler ( event:DataChangeEvent ): void
  78.         {
  79.             trace ( event.selectedItem ) ;
  80.         }
  81.         
  82.          private function layoutChildren (...args ): void
  83.         {
  84.             list.width = stage.stageWidth - 2*marginValue ;
  85.             list.height = stage.stageHeight - 2*marginValue ;
  86.         }
  87.         
  88.     }
  89. }

注意代码中有这样一句:addChild(new Stats());Stats是一个开源类,用于实施检测SWF的性能(FPS,内存占用等),您可以在这个地址找到它的相关说明和下载:

https://github.com/mrdoob/Hi-ReS-Stats

运行这个类,就可以看到文章开头的第一个列表的实例效果:

2

图2:List运行效果

虽然做到这里已经小有成果,但您是否注意到其中隐藏的性能问题?在上面的实现中,有多少条数据,就对应产生多少个ItemRenderer的实例,这样的行为简单容易理解,但却也有着很大的性能隐患。当数据量比较小的时候(比如几十条,几百条数据),对于性能还没有很明显的影响,但数据量更大一些呢,几千条,甚至上万条数据,可以想象,单单处理上万个显示对象,就足以让Flash Player的CPU占用率“居高不下”,一旦要进行交互,更是惨不忍睹。那么有没有办法对这种情况进行优化呢?就是下面我们要探讨的内容。

为列表实现"虚拟滚动"

先让我们花一些篇幅,来了解“虚拟滚动”这个实现思路。在上面的实现中,产生性能问题的原因是:

  • 显示对象过多,Flash Player渲染显示对象的负担过重(实际上1万个ItemRenderer实例,光创建就需要很长时间)
  • 如此众多的显示对象,排列在contentGroup中,contentGroup的尺寸会非常大(如果单项的高度是100像素,那么contentGroup的高度将是100万像素),因为滚动时实际上是移动contentGroup的位置,而且是根据帧频频繁的移动,性能消耗也是非常大的。

那么是否真的有必要需要这么多的显示对象呢?让我们从现实生活中找一些灵感:

在中国戏剧表演中,同一场能上场的演员是非常有限的,有时要表现千军万马出征的场景,演员是如何做的呢?比如有10个演员,舞台大小可以容纳6个人走过场,那么这10个人就排好队,一个接一个出场,第一个人走完过场后,进入幕后,马上排到队尾,以此来保证队伍的连续性,这样走上几个循环,观众可能感觉至少有几十个人走过去了,但实际上呢,只是这10个人而已。

观察我们的屏幕,当您对列表进行交互,无论它如何滚动,在同一时间,显示出来的数量是非常有限的(比如列表高度是500像素,单项的高度是100像素,那么同一时间能显示的最大数量,不超过6个),那么我们完全可以只创建6个显示对象,让它们在屏幕上进行走马灯式的运动,比如向上滚动时,超出屏幕区域的实例,则自动补上列表底部“即将露馅”的位置,以此类推,当向上滚动时,6个对象轮流补位,对我们的眼睛就会产生错觉,感觉是有很多的显示对象,进行不间断的滚动(实际上当然只有6个);当向下滚动时也是如此处理。而我们要继续做的是,虚拟计算出当前的滚动位置,替换屏幕上显示对象的值。这样对于用户来说,仍然是顺畅的列表交互,但因为我们优化了实现细节,可以在很大程度上改善列表的运行效率(计算都是在数据层面,显示对象的渲染被确定在很小的范围内)。

下面这张图表现了这个过程:

3

图3:List虚拟滚动设想

下面回到我们的代码,来实现这个设想:

首先回到ScrollableList类,定义一个变量,确定是否开启虚拟滚动(虚拟滚动同时对ItemRenderer有一些限制,不能保持不同高度,所以这个选择权留给用户,由用户根据实际情况,决定是否开启虚拟滚动)。

  1. /**是否开启虚拟滚动。在数据量大的时候,虚拟滚动可以节省一些性能,但要求每个itemRenderer必须高度相同*/
  2. protected var useVirtualScroll:Boolean = false;
复制代码

然后创建一个VirtualItemRenderer,这是一个虚拟的ItemRenderer,不是显示对象,只是为了在模拟计算滚动位置时,存储相应的虚拟坐标值。

  1. class VirtualItemRenderer
  2. {
  3. public var data:Object;
  4. public var x:Number;
  5. public var y:Number;
  6. public function VirtualItemRenderer(data:Object)
  7. {
  8. this.data = data;
  9. }
  10. }
复制代码

然后声明一个virtualItemArr变量,类型是数组,当设置dataProvider时,会相应的生成相同数量的虚拟项目渲染器实例,存储在这个数组中,并将初始化计算好的虚拟坐标存储在每个对象中:

  1. public function set dataProvider(value:Array):void
  2. {
  3. _dataProvider = value;
  4. if(useVirtualScroll)
  5. {
  6. virtualItemArr = [];
  7. var itemHeight:Number = (itemRenderer as Object).itemHeight;
  8. for (var i:int = 0; i < dataProvider.length; i++)
  9. {
  10. var virtualItem:VirtualItemRenderer = new VirtualItemRenderer(dataProvider[i]);
  11. virtualItem.y = i*(itemHeight+itemGap);
  12. virtualItemArr.push(virtualItem);
  13. }
  14. }
  15. createItemRenderer();
  16. }
复制代码

创建一个measureFunction方法,这个方法用于计算虚拟的项目渲染器应该占据多大的尺寸:

普通浏览 复制代码
  1. /**
  2.  * 在开启虚拟滚动时,由这个方法计算虚拟的内容高度
  3.  * @ return 
  4.  */        
  5. protected  function measureFunction(): Object
  6. {
  7.     var cotentHeight:Number = _dataProvider.length*((itemRenderer  as  Object).itemHeight+itemGap);
  8.      return { width: width, height:cotentHeight};
  9. }

同样要在ScrollableContainer增加一个measureFunction变量,类型是Function,一旦开启虚拟滚动,ScrollableList将把自身的measureFunction传递给ScrollableContainer,然后ScrollableContainer会用这个方法代替measure方法(因为实际显示对象数量与虚拟计算所要求的内容高度不一致,所以在开启虚拟滚动之后,ScrollableContainer的measure方法已经失去原有作用)。修改ScrollableContainer的measure方法,增加一段定义:

普通浏览 复制代码
  1. if(measureFunction !=  null)
  2. {
  3.     var returnObj: Object = measureFunction();
  4.     _contentWidth = returnObj. width;
  5.     _contentHeight = returnObj. height;
  6.     drawContentBackground();
  7.      return;
  8. }

回到ScrollableList类,修改createChildren方法,增加下面的定义:

普通浏览 复制代码
  1. if(useVirtualScroll)
  2. {
  3.     container.measureFunction = measureFunction;
  4.     container.addEventListener( "startScroll",contentScrollHandler);
  5.     container.addEventListener( "endScroll",contentScrollHandler);
  6. }

因为开启虚拟滚动后,交互的控制器:ScrollControler将不再控制真实的contentGroup的实际坐标值(实际坐标值将一直是0),而是计算一个虚拟的坐标值。所以如果您此时对列表进行拖动,将看不到任何效果,但实际虚拟移动值已经被计算出来了,同时会触发两个事件:startScroll和endScroll。我们需要侦听这两个事件,然后控制显示列表中仅有的几个ItemRenderer实例,去模拟出整个列表滚动的效果。

  1. /**
  2. * 当"虚拟的滚动"被触发时,处理显示列表中的内容
  3. * @param event
  4. */
  5. protected function contentScrollHandler(event:Event):void
  6. {
  7. if(event.type == "startScroll")
  8. {
  9. addEventListener(Event.ENTER_FRAME,changeChildrenLocation);
  10. inScrollState = true;
  11. }
  12. else
  13. {
  14. inScrollState = false;
  15. if(container.virtualContentGroupY==0 || container.virtualContentGroupY==(container.height-container.contentHeight))
  16. {
  17. changeChildrenLocation();
  18. }
  19. removeEventListener(Event.ENTER_FRAME,changeChildrenLocation);
  20. }
  21. }
  22. /**
  23. * 计算子元件的位置,仅在虚拟滚动时使用
  24. * @param event
  25. */
  26. protected function changeChildrenLocation(...args):void
  27. {
  28. if(container.virtualContentGroupY > 0 || container.virtualContentGroupY < -(container.contentHeight-container.height))
  29. {
  30. return;
  31. }
  32. var itemHeight:Number = (itemRenderer as Object).itemHeight;
  33. //表记
  34. var dataLength:int = virtualItemArr.length;
  35. needUpdateItemArr.length = 0;
  36. var virtualItem:VirtualItemRenderer;
  37. var currentItemY:Number;
  38. for (var i:int = 0; i < dataLength; i++)
  39. {
  40. virtualItem = virtualItemArr[i];
  41. currentItemY = virtualItem.y+container.virtualContentGroupY;
  42. if(currentItemY > -itemHeight && currentItemY < container.height)
  43. {
  44. needUpdateItemArr.push(virtualItem);
  45. }
  46. }
  47. //定位
  48. dataLength = needUpdateItemArr.length;
  49. if(dataLength < container.numChildren)
  50. {
  51. container.getChildAt(container.numChildren-1).y = -itemHeight;
  52. }
  53. for (i = 0; i < dataLength; i++)
  54. {
  55. virtualItem = needUpdateItemArr[i];
  56. currentItemY = virtualItem.y+container.virtualContentGroupY;
  57. var displayItem:IItemRenderer = container.getChildAt(i) as IItemRenderer;
  58. if(displayItem.data != virtualItem.data)
  59. displayItem.data = virtualItem.data;
  60. displayItem.y = currentItemY;
  61. }
  62. }
复制代码

然后注意ScrollControler类,增加两个属性:virtualContentGroupX和virtualContentGroupY,这两个值将存储开启虚拟滚动后,系统模拟计算出的contentGroup的坐标值(注意contentGroup的实际坐标值不会变化,永远是0)。

  1. /**开启虚拟滚动*/
  2. public var useVirtualScroll:Boolean = false;
  3. /**一旦开启虚拟滚动,则以此值为计算依据,实际值将永远是0*/
  4. public var virtualContentGroupX:Number = 0;
  5. /**@copy #virtualContentGroupX*/
  6. public var virtualContentGroupY:Number = 0;
复制代码

然后要修改其中所有涉及坐标值计算的部分,判断是计算实际坐标值,还是虚拟坐标值,比如:

  1. if(horizontalScrollEnabled)
  2. {
  3. if(useVirtualScroll)
  4. virtualContentGroupX=parent.mouseX - currentMouseOffsetX;
  5. else
  6. contentGroup.x=parent.mouseX - currentMouseOffsetX;
  7. }
复制代码

OK,优化工作完成。现在我们可以再建立一个主类,写测试代码,运行起来看实际效果:

普通浏览 复制代码
  1. package
  2. {
  3.     import com.riadev.mobile.events.DataChangeEvent ;
  4.     import com.riadev.mobile.ui.control.ScrollableList ;
  5.     import com.riadev.mobile.ui.renderer.IconItemRenderer ;
  6.     
  7.     import flash.display.Bitmap ;
  8.     import flash.display.Loader ;
  9.     import flash.display.Sprite ;
  10.     import flash.display.StageAlign ;
  11.     import flash.display.StageScaleMode ;
  12.     import flash.events.Event ;
  13.     import flash.events.MouseEvent ;
  14.     import flash.net.URLLoader ;
  15.     import flash.net.URLRequest ;
  16.     
  17.     import net.hires.debug.Stats ;
  18.     
  19.      /**
  20.      * 测试开启虚拟滚动的列表
  21.      * @author NeoGuo
  22.      * 
  23.      */    
  24.      public  class ListDemo2 extends Sprite
  25.     {
  26.          private var marginValue: int =  20 ;
  27.          private var list:ScrollableList ;
  28.         
  29.          private var testData:Array =  [ ] ;
  30.         
  31.          public function ListDemo2 ( )
  32.         {
  33.             super ( ) ;
  34.             stage.align = StageAlign.TOP_LEFT ;
  35.             stage.scaleMode = StageScaleMode.NO_SCALE ;
  36.             fillData ( ) ;
  37.             initApp ( ) ;
  38.             addChild ( new Stats ( ) ) ;
  39.         }
  40.         
  41.          protected function fillData ( ): void
  42.         {
  43.              for  (var i: int =  0 ; i <  10000 ; i++ ) 
  44.             {
  45.                 testData.push ({label: "P "+ (i+ 1 ),icon: "assets/air.jpg"} ) ;
  46.             }
  47.         }
  48.         
  49.          private function initApp ( ): void
  50.         {
  51.             list =  new ScrollableList (IconItemRenderer, 1, true ) ;
  52.             list.x = marginValue ;
  53.             list.y = marginValue ;
  54.             list.setStyle ( "borderColor", 0xCCCCCC ) ;
  55.             list.setStyle ( "bgColor", 0x000000 ) ;
  56.             list.setStyle ( "bgAlpha", 0 .5 ) ;
  57.             list.dataProvider = testData ;
  58.             list.addEventListener (DataChangeEvent.ITEM_CHANGE,itemChangeHandler ) ;
  59.             addChild (list ) ;
  60.             layoutChildren ( ) ;
  61.             stage.addEventListener (Event.RESIZE,layoutChildren ) ;
  62.         }
  63.         
  64.          protected function itemChangeHandler ( event:DataChangeEvent ): void
  65.         {
  66.             trace ( event.selectedItem ) ;
  67.         }
  68.         
  69.          private function layoutChildren (...args ): void
  70.         {
  71.             list.width = stage.stageWidth - 2*marginValue ;
  72.             list.height = stage.stageHeight - 2*marginValue ;
  73.         }
  74.         
  75.     }
  76. }

注意代码中给List提供的数据源是一个包含1万条数据的数组。您可以开启或关闭虚拟滚动,观察性能对比。下面是笔者电脑上的实测结果:

4

图4:未开启虚拟滚动测试结果

5

图5:开启虚拟滚动测试结果

后续工作

欢迎您体验和使用这个组件,并继续完善它。您可以继续浏览Adobe开发者中心,寻找移动开发相关的文章和资源,加深这一领域的知识技能。


原文转载:http://www.riadev.com/flex-thread-1218-1-1.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值