分享

【AS3 Coder】任务二:了解Flex组件生命周期

 ilovepesx 2013-10-18
使用框架:Flex
任务描述:了解Flex组件生命周期,掌握标准的Flex组件开发方法
难度系数:2


      做为一个Flash开发人员,一定或多或少地有听说过Flex这个框架。有人用Flex开发游戏,也有人用Flex做企业级应用(这是Adobe官方所推荐的用途)。不过据说使用Flex来开发游戏的公司要多于企业级应用的公司。但是经过贫道对身边人的观察后发现,很多人用Flex都只是停留在一个很“浅”的层次上面,只会简单地使用Flex所提供的组件以及创建一些继承于Flex组件的简单自定义组件。这对于企业级应用来说可能还不会造成什么影响,因为企业级应用的界面往往不会发生什么频繁的改变,而且Flex提供的组件已差不多能满足企业级应用对界面的需要。但是,对于我们这些使用Flex来开发游戏的人来说,如果你只是一味地依靠Flex自提供的这套组件的话就很难做出游戏中所需要的那些华丽多变的界面,且在遇到某些问题时无法解决。为此,贫道觉得有必要写一篇帖子来为Flexer们开拓开拓视野了。如果道友你是Flex的开发人员但对Flex组件生命周期不太了解或者你即将尝试使用Flex来做一些应用,那么你可以花十到十五分钟时间来阅读一下本帖,这有可能在以后为你节省10-20个小时的查错时间哦~
      在以往的Flash项目中,当我们需要更改一个或多个对象的属性时,在决定最终属性的过程中有可能会频繁地发生更改。考虑这样一种情况,在一个Sprite对象中存在两个形状Shape对象,我们每更改其中一个Shape对象的尺寸都会造成此Sprite对象的尺寸发生更改,这样会产生许多冗余的工作不是吗,如果我这个Sprite里只有一两个对象也还好,若是有成百上千个对象的话岂不是要浪费很多时间?!
    作为一个成熟的框架,Flex已考虑到了此问题,并采用了一套组件失效机制(invalidation mechanism)来提高应用性能。当组件的属性发生变化时,Flex用一系列方法的调用来标记组件的某些东西已经发生变化,然后将其延迟到下一次屏幕更新时通过布局管理器统一进行处理。现在许多介绍Flex的书籍上只介绍了再MXML中使用组件的方式,这种方式虽然简单易用但是却隐藏了许多细节。我找到一本书叫做《Flex企业应用开发实战》,这本书上比较系统地讲解了一下Flex组件的生命周期原理,现在,请列位阅读此书的2.5.2节,先大致了解一下Flex是如何使用布局管理器对组件进行统一布局的:http://book.51cto.com/art/201006/208254.htm   
       很多道友在阅读上面给出的文章后头上一定会出现或大或小的一个问号,那么我在此归总一下先:
Flex中存在全局唯一的一个 布局管理器实例,Flex用它来实现组件失效机制,即在每次更改某些组件的属性后Flex会将这些组件通过一个方法标记为失效组件,并在Event.RENDER事件到来时通过布局管理器统一处理这些已失效组件,计算它们将出现在的屏幕位置、尺寸等布局信息。而这个过程可以具体分为三部分:  提交阶段(提交组件已变化的属性)、  度量阶段(计算组件的默认尺寸)、  布局阶段(根据组件最新的属性、样式以及默认尺寸来更新组件显示列表中子显示对象的尺寸、位置并画出组件所使用的所有皮肤及图形化元素)。
在提交阶段布局管理器的处理流程是:LayoutManager.validateProperties()  --> component.validateProperties() ---> component.commitProperties()你可以在你的Flex项目中新建一个继承于UIComponent的类,之后重写它的commitProperties方法并打上断点进行监测:
1.jpg
从左上角的小窗口中我们可以清楚地看到布局管理器处理一个UIComponent组件的提交阶段的函数调用次序正如刚才所讲的那样。
同样地,在度量阶段和布局阶段中,布局管理器也是通过类似的过程进行函数调用的,只不过在度量阶段最后调用的函数名是measure(), 而布局阶段最后调用的函数名是updateDisplayList()罢了。
LayoutManager.validateSize()  --> component.validateSize() ---> ...  -->  component.measure();
LayoutManager.validateDisplayList()  --> component.validateDisplayList() ---> component.updateDisplayList();

如果你不信,你大可试着运用我刚才的办法通过调试来验证这个函数调用的过程。
       一个组件将会在应用程序运行过程中发生三种“失效”:属性失效、尺寸失效、显示失效。Flex组件将通过
invalidateProperties方法标记自己的属性失效;
invalidateSize方法标记自己的尺寸失效;
invalidateDisplayList方法标记自己的显示失效。

       每一种失效都会在下一帧到来时由布局管理器来进行相应的函数调用,就如贫道之前所说,如果一个组件属性失效,那么在下一帧布局管理器在提交阶段执行其commitProperties方法;同样地,尺寸失效和显示失效则会执行其measure和updateDisplayList方法。

     那么知道这个过程对我们日后的开发有什么帮助呢?知道了这个流程后你就会明白,一个组件在什么时候会干什么事,在自定义一个组件的时候把代码放在什么位置最好以及如何在读别人写的Flex组件代码时更快速地找到你想找的代码块。始终记得,commitProperties方法用以处理组件因属性更改而会发生变化的逻辑,measure方法用以确定组件的实际尺寸,updateDisplayList方法用以刷新显示列表中将发生改变的图像或组件逻辑。  下面,我将和列位一起边写代码边熟悉构建组件的流程。

     首先,我们接到BOSS的一个任务说要自定义一个Flex组件,此组件可以理解成是一个网格类,其中将创建根据需要来创建N行N列的格子对象。那么按照我们以往在ActionScript项目中的做法,应该先自定义一个格子类Cell,它允许外部设置它的外观皮肤。这个类就取名叫做MyFuckingCell好了,个人觉得这个名字比较cool:
  1. package
  2. {
  3.         import flash.display.Bitmap;
  4.         import flash.display.Sprite;
  5.        
  6.         public class MyFuckingCell extends Sprite
  7.         {
  8.                 private var _skin:Bitmap;
  9.                
  10.                 public function MyFuckingCell()
  11.                 {
  12.                         super();
  13.                 }
  14.                
  15.                
  16.                 public function get skin():Bitmap
  17.                 {
  18.                         return _skin;
  19.                 }

  20.                 public function set skin(value:Bitmap):void
  21.                 {
  22.                         if( _skin != value )
  23.                         {
  24.                                 _skin = value;
  25.                                
  26.                                 if( _skin && this.contains(_skin) )
  27.                                 {
  28.                                         removeChild(_skin);
  29.                                 }
  30.                                 addChild( _skin );
  31.                         }
  32.                 }
  33.                
  34.         }
  35. }
复制代码
之后轮到做我们的网格类了,由于在Flex中的容器类里面只能addChild那些UIComponent或其子类(如果你往Application或者Canvas或者BOX或Group中AddChild一个Sprite或者Bitmap等对象,在程序运行将报错),我们就必须让我们的格子类继承UIComponent了,先撇开Flex组件生命周期神马概念不管,按照我们以前在AS项目中的写法写这个类的话应该是像下面这段代码一样:
  1. package
  2. {
  3.         import flash.display.Bitmap;
  4.        
  5.         import mx.core.UIComponent;
  6.        
  7.         public class MyFuckingGrid extends UIComponent
  8.         {
  9.                 [Embed(source="assets/blueCircle.jpg")]
  10.                 private var cellSkin:Class;
  11.                
  12.                 private var _numCols:int;
  13.                 private var _numRows:int;
  14.                 private var _cellList:Vector.<MyFuckingCell>;
  15.                
  16.                 public function MyFuckingGrid()
  17.                 {
  18.                         super();
  19.                 }


  20.                 private function createCells():void
  21.                 {
  22.                         removeAllCells();
  23.                         var cell:MyFuckingCell;
  24.                         var skin:Bitmap;
  25.                         for(var i:int=0; i<_numCols; i++)
  26.                         {
  27.                                 for(var j:int=0; j<_numRows; j++)
  28.                                 {
  29.                                         cell = new MyFuckingCell();
  30.                                         skin = new cellSkin();
  31.                                         cell.skin = skin;
  32.                                         cell.x = i * skin.width;
  33.                                         cell.y = j * skin.height;
  34.                                         addChild(cell);
  35.                                         _cellList.push( cell );
  36.                                 }
  37.                         }
  38.                 }
  39.                
  40.                 private function removeAllCells():void
  41.                 {
  42.                         if( _cellList && _cellList.length > 0 )
  43.                         {
  44.                                 for each(var cell:MyFuckingCell in _cellList)
  45.                                 {
  46.                                         removeChild(cell);
  47.                                 }
  48.                         }
  49.                        
  50.                         _cellList = new Vector.<MyFuckingCell>();
  51.                 }
  52.                
  53.                 public function get numCols():int
  54.                 {
  55.                         return _numCols;
  56.                 }

  57.                 public function set numCols(value:int):void
  58.                 {
  59.                         _numCols = value;
  60.                         createCells();
  61.                 }

  62.                 public function get numRows():int
  63.                 {
  64.                         return _numRows;
  65.                 }

  66.                 public function set numRows(value:int):void
  67.                 {
  68.                         _numRows = value;
  69.                         createCells();
  70.                 }


  71.         }
  72. }
复制代码
很简单的一段代码,这个简单的网格类会根据设置的行、列数创建相应数目的格子,全部格子用的都是同一种皮肤(有的同学可能对Vector这个类有些陌生,这个类在我的教程里是第一次出现。那么什么是Vector呢?我们称其为‘向量’,是在FlashPlayer 10.0版本出现的新AS3类,它具备大多数数组Array所具有的API,我们可以把它作为数组来使用,但是与数组不同的一点是,Vector中保存的全部对象都是相同类型或者具有父类的,使用Vector对象能得到比Array对象更快的遍历速度)。看上去这个类写得应该没什么问题了,把它放到Application里面去运行看看,并为Application侦听键盘事件,让我们能够实时改变网格的规格:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" minWidth="955" minHeight="600" xmlns:local="*"
  3.                                 addedToStage="application1_addedToStageHandler(event)">
  4.        
  5.         <mx:Script>
  6.                 <![CDATA[
  7.                        
  8.                        
  9.                         protected function application1_addedToStageHandler(event:Event):void
  10.                         {
  11.                                 stage.addEventListener(KeyboardEvent.KEY_DOWN, application1_keyDownHandler);
  12.                         }
  13.                        
  14.                         protected function application1_keyDownHandler(event:KeyboardEvent):void
  15.                         {
  16.                                 switch( event.keyCode )
  17.                                 {
  18.                                         case Keyboard.UP:
  19.                                                 theGrid.numRows++;
  20.                                                 break
  21.                                         case Keyboard.DOWN:
  22.                                                 theGrid.numRows--;
  23.                                                 break
  24.                                         case Keyboard.LEFT:
  25.                                                 theGrid.numCols--;
  26.                                                 break
  27.                                         case Keyboard.RIGHT:
  28.                                                 theGrid.numCols++;
  29.                                                 break
  30.                                 }
  31.                                                
  32.                         }

  33.                        
  34.                 ]]>
  35.         </mx:Script>
  36.        
  37.         <local:MyFuckingGrid id="theGrid" x="50" y="50" numCols="10" numRows="4"/>
  38. </mx:Application>
复制代码
最终运行结果应该像下图这样:

2.jpg
看上去很完美,很perfect啊,你看吧,知道Flex组件生命周期有屌用啊,不一样正常运作么,哈哈~得意个毛得意,如果你把这个网格对象放到一个Panel里面去看看会发生什么事情,额,好吧,试试就试试,于是我写了如下代码:
  1. <mx:Panel x="20" y="20" width="200" height="200" title="网格面板" fontSize="12">
  2.         <local:MyFuckingGrid id="theGrid" x="50" y="50" numCols="4" numRows="3"/>
  3. </mx:Panel>
复制代码
最终运行结果如下图所示:
3.jpg
看出问题来没?作为Panel子类的MyFuckingGrid类居然脱离了它老爸的管辖范围,且Panel没有显示滚动条,这自然不是我们想要的效果,我们想要的效果是Panel能将网格类约束于其尺寸范围内并可以通过拖动滚动条来浏览隐藏的部分。
如果你不信邪,你可以把Panel类换做HBOX类(Flex4中可以用HGroup),并在其中放置两个MyFuckingGrid类:
  1. <mx:HBox x="20" y="20">
  2.         <local:MyFuckingGrid id="theGrid" x="50" y="50" numCols="2" numRows="2"/>
  3.         <local:MyFuckingGrid  x="50" y="50" numCols="2" numRows="2"/>
  4. </mx:HBox>
复制代码
我们期待的结果是这两个网格类能够按照队列状一个排在左边一个排在右边,而实际结果是怎样的呢?见下图:
4.jpg
这……这TMD两个网格居然重合在了一起!是VBOX的问题么?不是,肯定不是,那么问题出在哪里呢?回想一下之前说的内容,我们可以猜测是否是我自定义的这个网格组件的尺寸没有计算正确导致的问题呢?因为VBOX在为其子组件布局的时候会根据组件的宽度计算组件位置。那么带着这个疑问,我们可以打两个断点测试一下全部的MyFuckingGrid 组件的width和height,实验结果发现,它们的width和height永远是0,果然没错!
    那么如何解决这个棘手的问题呢?不了解Flex组件生命周期的人可能会犯难,甚至采用一种暴力法:直接给每个MyFuckingGrid 组件实例固定死width和height,的确,这样可以暂时解决问题,但不能保证之后不会出现新的问题。之前说过,Flex中组件的布局工作都是通过全局唯一的布局管理器在每一帧统一来做,而其中就包含了组件尺寸的计算工作,这个工作的流程还记得吗?对,是LayoutManager.validateSize()  --> component.validateSize() ---> ...  -->  component.measure();那么既然最终会调用组件自身的measure方法的话我们就可以通过重载组件的此方法来定义组件尺寸的计算规则。明白了这点之后马上动手吧,打开
MyFuckingGrid类,在其中写入下列代码:
  1. public class MyFuckingGrid extends UIComponent
  2. {
  3. ……               
  4.         override protected function measure():void
  5.         {
  6.                 super.measure();
  7.         }

  8. ……

  9. }
复制代码
记得在重载这些protected方法的时候必须调用其父类的同名方法,不然就无法按照规定的流程走完布局工作全程了。那么接下来我们要做什么事情呢?计算好组件的实际尺寸后应该赋给哪个属性呢?width和height吗?首先让我们熟悉一下Flex组件中的几个与尺寸有关的属性,请继续阅读《Flex企业应用实战》的2.5.2节:http://book.51cto.com/art/201006/208255.htm
阅读过一遍之后脑海里面应该对这些属性的区别有了一个大致的认识,贫道来总结一下,就是如果你设置了一个组件的width或height属性,那么就表示此组件的实际尺寸已经被你指定好了,且不会发生改变,那么就不会调用measure方法进行组件尺寸的计算;相反地,如果你没有指定组件的width或height属性,那么组件就会在布局阶段通过其measure方法计算组件的实际宽度,在此方法中计算得出的实际宽度应该赋值给measuredWidth和measuredHeight属性,在此之后,Flex一般会用这两个值来作为组件的实际width和height。那么在我们现在的情况中,由于没有指定过MyFuckingGrid的width和height,因此需要网格组件根据它里面的格子数量来决定最终它的尺寸。OK,知道该怎么办了,于是将重载的measure方法写完整:
  1. public class MyFuckingGrid extends UIComponent
  2. {
  3. ……               
  4.            override protected function measure():void
  5.         {
  6.                 super.measure();
  7.                
  8.                 if( _cellList && _cellList.length > 0 )
  9.                 {
  10.                         //由于全部格子尺寸都一样,所以寡人随便取一个格子来计算
  11.                         var sampleCell:MyFuckingCell = _cellList[0];
  12.                         var actualW:Number = sampleCell.width * _numCols;
  13.                         var actualH:Number = sampleCell.height * _numRows;
  14.                         this.measuredWidth = actualW;
  15.                         this.measuredHeight = actualH;
  16.                 }
  17.                
  18.         }

  19. ……

  20. }
复制代码
写完之后测试一下之前写的应用,发现不论在Panel还是HBox里面都能排列正确了,哦也,that`s fucking easy~妈妈再也不用担心我的学习。但是对于一些有代码洁癖的BOY来说,在网格类中有一些代码感觉在做重复工作,没错,就是在numCols和numRows的set方法中,我每次设置numCols和numRows的任意一个都会导致网格全部重建,如果我既要改变行和列数的话会多做一次冗余的重建工作。那么,现在你应该想起,之前我提到的Flex的失效/延时机制了么,先不要在属性改变后立即进行逻辑,先将更改了的组件属性“提交”上去,待到了下一帧来到时再由布局管理器统一处理,这样可以省掉大量的冗余工作。那么如何做这个“提交”的工作呢?我们可以参考一下Flex自带组件的写法,看看规范的写法是怎么样的。在Application中插入一个Label组件,之后按住Ctrl并用鼠标左键点击Label以打开Label的定义文件Label.as,在里面我们先通过Ctrl + O键打开类定义大纲,找到text的set方法:
5.JPG
双击进去,看看它text的set方法是如何写的,其他代码不用过多关心,我们只关心以下三句:
  1. public function set text(value:String):void
  2. {
  3.    
  4. ……

  5.     _text = value;
  6.     textChanged = true;

  7.     invalidateProperties();
  8.   
  9. ……
  10. }
复制代码
首先看到,在set方法中,Label组件没有立即将value的值设置给其内部的UITextField对象textField的text属性,因为一旦你为textField设置了text属性将会让FlashPlayer去做文本更改的渲染工作,这会花不少时间,于是,Label为了避免重复做冗余的渲染工作节省时间,就暂存这个value的值并保存在一个变量里面,并将文本更改标记textChanged 设置为true。如果你还记得我之前讲的内容,你应该明白,调用invalidateProperties会标记此组件为属性失效组件,那么在下一次组件布局阶段将会执行其commitProperties方法来做属性改变而会产生的一些逻辑。既然如此,我们就来看看Label中commitProperties方法里面会做什么事情,看到此方法的如下几句:
  1. override protected function commitProperties():void
  2. {
  3.     super.commitProperties();
  4.    
  5. ……

  6.     if (textChanged || htmlTextChanged)
  7.     {
  8.         // If the 'text' and 'htmlText' properties have both changed,
  9.         // the last one set wins.
  10.         if (isHTML)
  11.             textField.htmlText = explicitHTMLText;
  12.         else
  13.             textField.text = _text;
  14.         
  15. ……                    
  16.         textChanged = false;
  17.         htmlTextChanged = false;
  18.     }
  19. }
复制代码
看见没有?正如贫道刚才所说,Flex会将属性的更改所需要做的逻辑延迟到下一次布局阶段,不论你在一帧内调用多少次,改变多少次Label的text值,它只在下一帧的布局阶段设置一次textFiled的text属性,只做一次渲染工作,这的确是一种非常好的机制。
      于是,我们可以模仿Lable的这种做法来写我们的MyFuckingGrid类:
  1. public class MyFuckingGrid extends UIComponent
  2. {
  3.         ……
  4.        
  5.         override protected function commitProperties():void
  6.         {
  7.                 super.commitProperties();
  8.                 if( _sizeChanged )
  9.                 {
  10.                         _sizeChanged = false;
  11.                         createCells();
  12.                 }
  13.         }
  14.        
  15. ……

  16.         public function set numCols(value:int):void
  17.         {
  18.                 if( _numCols != value )
  19.                 {
  20.                         _numCols = value;
  21.                         _sizeChanged = true;
  22.                         invalidateProperties();
  23.                         invalidateSize();
  24.                 }
  25.                
  26.         }

  27. ……

  28.         public function set numRows(value:int):void
  29.         {
  30.                 if( _numRows != value )
  31.                 {
  32.                         _numRows = value;
  33.                         _sizeChanged = true;
  34.                         invalidateProperties();
  35.                         invalidateSize();
  36.                 }
  37.         }

  38. }
复制代码
首先,你要明确属性更改后会发生哪些东西的改变,这里我们的行列数改变会造成网格重绘以及尺寸变化,因此必须在属性被改变后标记组件属性与尺寸失效,之后,在commitProperties方法中进行网格重绘工作,由于在布局时“提交阶段”在“度量阶段”之前,所以不用担心measure方法会在commitProperties方法之前执行而造成计算错误。
    稍作休息,一会儿让寡人为列位讲一讲如何修改Flex自带组件中的一些功能~                                                

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多