Starling 框架帮助手册中文版《二》

显示列表

可以说,Starling 遵循了和原生 Flash 显示列表一样的规则。Starling 中的显示对象将会无法

访问其 stage属性,直到其被添加到显示列表中。在以往的编程中,我们为了更加安全地访
问 stage 对象,我们需要用到一些重要的事件(如 Event.ADDED_TO_STAGE),幸运的是,在
Starling中我们也可以使用这一些事件(但这些事件所在的包与以往不同): 
*  Event.ADDED :  当显示对象被添加到一个容器中抛出   
*  Event.ADDED_TO_STAGE :  当显示对象被添加到一个已存在于舞台上的容器中时,也就是
其变得可见时抛出 
*  Event.REMOVED :  当显示对象被从一个容器中移除时抛出 
*  Event.REMOVED_FROM_STAGE :  当显示对象被从一个已存在于舞台上的容器中移除时,
也就是其变得不可见时抛出 
在之后的实验中我们将会用到这一些事件来帮助我们在合适的时候初始化一些东西或让一
些东西失效,这样可以让代码执行效率更佳。 
下面是 Starling中 DisplayObject 类提供的一系列共有方法: 
*  removeFromParent :  从父对象中移除,如果它有父对象的话 
*  getTransformationMatrixToSpace :   创建一个用于表现本地坐标系和另一个坐标系转换关
系的矩阵   
*  getBounds :  得到一个以某个坐标系为参考系的能包含该显示对象的最小矩形。 
*  hitTestPoint :  返回当前坐标系中某个点位置下层次最高(挡在最前面)的显示对象 
*  globalToLocal :  将一个点由舞台坐标转换为当前坐标系坐标 
*  localToGlobal :  将一个点由当前坐标系坐标转换为舞台坐标   
下面列出 DispObject 中提供的属性, 作为一个 Flash 开发人员,你会很开心地看见在 Starling
中的 DisplayObject 保留了大多数和原生 Flash 中的 DisplayObject一样名字的属性(功能也一
样),且还提供了一些额外的功能,如 pivotX 和pivotY属性,允许开发者在运行时动态改变
DisplayObject的注册点 : 
*   transformationMatrix :  当前显示对象位置相与其父容器的转换矩阵 
*   bounds :  当前显示对象在其父容器中的矩形(Rectangle)对象 
*   width、height、root、x、y、scaleX、scaleY、alpha、visible、parent、stage、root:不
解释了,和原生 DisplayObject 一样的功能 
*   rotation:当前显示对象绕其注册点的旋转弧度(非角度) 
*   pivotX、pivotY :  当前显示对象的注册点,默认为(0,0) 
*   touchable  :  指定当前显示对象是否能够接受 Touch 事件(相当于原生 DisplayObject 的
mouseEnable,因为在 Starling中所有鼠标事件都被定义为 Touch 事件) 
跟原生 Flash  API 一样,在 Starling中,Sprite类也是你能使用的最轻量级的容器。当然,其
作为 DisplayObject 的子类,自然就继承了之前提到过的那些 DisplayObject中拥有的 API,这
就提供了 Sprite对象之间可以互相嵌套的能力    
作为一个容器,Sprite 对象继承自 DisplayObjectContainer 类,下面是 DisplayObjectContainer
类中提供的 API: 
*  addChild :  不解释,和原生Flash 中的一样,下同 
*  addChildAt :  略 
*  dispose :  完全销毁一个对象,释放其在 GPU 中所占内存,移除其全部事件侦听。   
*  removeFromParent :  略 
*  removeChild :  略 
*  removeChildAt :  略 
*  removeChildren :  移除一个容器中所有的子对象   
*  getChildAt :  略 
*  getChildByName :  根据名称搜索一个子对象   
*  getChildIndex :  略 
*  setChildIndex :  略 
*  swapChildren :  略 
*  swapChildrenAt :  略 
*   contains :  略   
一旦你可以访问一个显示对象的 stage 属性,你就可以通过此属性来对舞台进行一系列的设
置,舞台对象继承于 DisplayObjectContainer  ,因此你可以对它调用大多数
DisplayObjectContainer  的 API,不仅如此,你还可以通过 stage对象的 color属性来直接改变
Starling 舞台的颜色。默认情况下,Starling 舞台的颜色会和原生 Flash 舞台颜色一致,这意
味着你可以直接通过[SWF]元标签设置原生舞台颜色来间接地设置 Starling舞台颜色: 
[SWF(width="1280", height="752", frameRate="60", backgroundColor="#990000")] 
若你想重载此行为(即我在[SWF]元标签中设置了原生 Flash 舞台颜色为某一颜色,现在我想
把 Starling 舞台颜色改成另一种颜色),你只需要简单地在任何一个被添加到舞台上的
DisplayObject中访问 stage属性并设置其 color 属性即可:

[plain] view plaincopy
  1. package      
  2. {     
  3.      import starling.display.Quad;     
  4.      import starling.display.Sprite;     
  5.      import starling.events.Event;     
  6.      
  7.      public class Game extends Sprite     
  8.      {     
  9.        private var q:Quad;     
  10.          
  11.          public function Game()     
  12.          {      
  13.        addEventListener(Event.ADDED_TO_STAGE, onAdded);     
  14.          }     
  15.          
  16.      private function onAdded ( e:Event ):void     
  17.      {     
  18.        // set the background color to blue     
  19.        stage.color = 0x002143;     
  20.            
  21.        q = new Quad(200, 200);     
  22.        q.setVertexColor(0, 0x000000);     
  23.        q.setVertexColor(1, 0xAA0000);     
  24.        q.setVertexColor(2, 0x00FF00);     
  25.        q.setVertexColor(3, 0x0000FF);     
  26.        addChild ( q );   
  27.      }     
  28.      }     
  29. }   

我们之前只是简单地用了一对三角形来形成了一个方块,此方块并没有使用任何的纹理,而
只是为它各个顶点设置了不同的颜色,这样会让 GPU 自动为其内部填充上渐变色。当然,
如果你想要创建一个实色的方块,只需要设置 Quad 对象的color 属性即可:

[plain] view plaincopy
  1. package      
  2. {     
  3.      import starling.display.Quad;     
  4.      import starling.display.Sprite;     
  5.      import starling.events.Event;     
  6.      
  7.      public class Game extends Sprite     
  8.      {     
  9.      private var q:Quad;     
  10.          
  11.          public function Game()     
  12.          {      
  13.        addEventListener(Event.ADDED_TO_STAGE, onAdded);     
  14.          }     
  15.          
  16.      private function onAdded ( e:Event ):void     
  17.      {     
  18.        q = new Quad(200, 200);     
  19.        q.color = 0x00FF00;     
  20.        q.x = stage.stageWidth - q.width >> 1;     
  21.        q.y = stage.stageHeight - q.height >> 1;     
  22.        addChild ( q );     
  23.      }     
  24.      }     
  25. }   

之后你会得到如下结果:

 

                                                                               图 1.12 
                                                                   一个绿色实色的方块

我们现在将开始侦听Event.ENTER_FRAME事件并在事件处理函数中为当前方块增加
一个颜色变化的补间动画,让其颜色在不同的随机颜色间变化:
 

[plain] view plaincopy
  1. package   
  2. {   
  3. import starling.display.Quad;   
  4. import starling.display.Sprite;   
  5. import starling.events.Event;   
  6. public class Game extends Sprite   
  7. {   
  8. private var q:Quad;   
  9. private var r:Number = 0;   
  10. private var g:Number = 0;   
  11. private var b:Number = 0;   
  12. private var rDest:Number;   
  13. private var gDest:Number;   
  14. private var bDest:Number;   
  15. public function Game()   
  16. {   
  17. addEventListener(Event.ADDED_TO_STAGE, onAdded);   
  18. }   
  19. private function onAdded ( e:Event ):void   
  20. {   
  21. resetColors();   
  22. q = new Quad(200, 200);   
  23. q.x = stage.stageWidth - q.width >> 1; q.y = stage.stageHeight - q.height >> 1;   
  24. addChild ( q );   
  25. s.addEventListener(Event.ENTER_FRAME, onFrame);   
  26. }   
  27. private function onFrame (e:Event):void   
  28. {   
  29. r -= (r - rDest) * .01;   
  30. g -= (g - gDest) * .01;   
  31. b -= (b - bDest) * .01;   
  32. var color:uint = r << 16 | g << 8 | b;   
  33. q.color = color;   
  34. // when reaching the color, pick another one   
  35. if ( Math.abs( r - rDest) < 1 && Math.abs( g - gDest) < 1 && Math.abs( b - bDest) )   
  36. resetColors();   
  37. }   
  38. private function resetColors():void   
  39. {   
  40. rDest = Math.random()*255;   
  41. gDest = Math.random()*255;   
  42. bDest = Math.random()*255;   
  43. }   
  44. }   
  45. }  


之后,为了让我们的方块能够再旋转起来,我们需要用上 rotation 这个属性,不
过值得注意的是,Starling 中的 DisplayObject 的 rotation 属性是以弧度为单位的,而非原生
Flash 中的以角度为单位。Starling 这样设计的目的是为了和其前身 Sparrow 的规则一致。如
果你执意要使用角度作为单位的话,你可以用上 starling.utils.deg2rad 这个方法来将角度转换
为弧度: 
sprite.rotation = deg2rad(Math.random()*360); 
由于 Starling 中的全部 DisplayObject 都具有 pivotX 及 pivotY 属性,我们可以非常便捷地在
运行时改变其注册点,以满足我们在进行如缩放、旋转时的需要。这里我们将让方块以其中
心点为轴心进行旋转,所以需要将其注册点放在其中心位置: 
q.pivotX = q.width >> 1; 
q.pivotY = q.height >> 1; 
对于 Flash 开发人员来说,原生 Flash 中的编程理念在 Starling 中几乎完全一致。作为
DisplayObject 的子类,我们现在用的 Quad 对象可以和一个 Textfield 对象嵌套添加到一个
Sprite 对象中,且我们对此 Sprite 对象所做的一些操作,如缩放、位移等都会同时影响其中
所有的子对象,和原生 Flash 中的规则一样,对吧?

[plain] view plaincopy
  1. package   
  2. {   
  3. import starling.display.DisplayObject;   
  4. import starling.display.Quad;   
  5. import starling.display.Sprite;   
  6. import starling.events.Event;   
  7. import starling.text.TextField;   
  8. public class Game extends Sprite   
  9. {   
  10. private var q:Quad;   
  11. private var s:Sprite;   
  12. private var r:Number = 0;   
  13. private var g:Number = 0;   
  14. private var b:Number = 0;   
  15. private var rDest:Number;   
  16. private var gDest:Number;   
  17. private var bDest:Number;   
  18. public function Game()   
  19. {   
  20. addEventListener(Event.ADDED_TO_STAGE, onAdded);   
  21. }   
  22. private function onAdded ( e:Event ):void   
  23. {   
  24. resetColors();   
  25. q = new Quad(200, 200);   
  26. s = new Sprite();   
  27. var legend:TextField = new TextField(100, 20, "Hello Starling!", "Arial", 14, 0xFFFFF  
  28. s.addChild(q);   
  29. s.addChild(legend);   
  30. s.pivotX = s.width >> 1;   
  31. s.pivotY = s.height >> 1;   
  32. s.x = (stage.stageWidth - s.width >> 1 ) + (s.width >> 1);   
  33. s.y = (stage.stageHeight - s.height >> 1) + (s.height >> 1);   
  34. addChild(s);   
  35. s.addEventListener(Event.ENTER_FRAME, onFrame);   
  36. }   
  37. private function onFrame (e:Event):void   
  38. {   
  39. r -= (r - rDest) * .01;   
  40. g -= (g - gDest) * .01;   
  41. b -= (b - bDest) * .01;   
  42. var color:uint = r << 16 | g << 8 | b;   
  43. q.color = color;   
  44. // when reaching the color, pick another one   
  45. if ( Math.abs( r - rDest) < 1 && Math.abs( g - gDest) < 1 && Math.abs( b - bDest) )   
  46. resetColors();   
  47. (e.currentTarget as DisplayObject).rotation += .01;   
  48. }   
  49. private function resetColors():void   
  50. {   
  51. rDest = Math.random()*255;   
  52. gDest = Math.random()*255;   
  53. bDest = Math.random()*255;   
  54. }   
  55. }   
  56. }   

我们现在就可以把包含有我们的 Quad 对象和 Textfield 对象的 Sprite 对象围绕其中心点进行
旋转了,颜色也在不停地变哦~

 

                                                                                          图 1.13 
                                             一个包含有 Quad 及 Textfield 对象的 Sprite对象在旋转

我们的代码现在看起来感觉有点凌乱了,所以我们现在把一些代码封装到一个
CustomSprite 类里面去。下面给出的就是其全部代码:

[plain] view plaincopy
  1. package   
  2. {   
  3. import starling.display.Quad;   
  4. import starling.display.Sprite;   
  5. import starling.events.Event;   
  6. import starling.text.TextField;   
  7. public class CustomSprite extends Sprite   
  8. {   
  9. private var quad:Quad;   
  10. private var legend:TextField;   
  11. private var quadWidth:uint;   
  12. private var quadHeight:uint;   
  13. private var r:Number = 0;   
  14. private var g:Number = 0;   
  15. private var b:Number = 0;   
  16. private var rDest:Number;   
  17. private var gDest:Number;   
  18. private var bDest:Number;   
  19. public function CustomSprite(width:Number, height:Number, color:uint=16777215)   
  20. {   
  21. // reset the destination color component   
  22. resetColors();   
  23. // set the width and height   
  24. quadWidth = width;   
  25. quadHeight = height;   
  26. // when added to stage, activate it   
  27. addEventListener(Event.ADDED_TO_STAGE, activate);   
  28. }   
  29. private function activate(e:Event):void   
  30. {   
  31. // create a quad of the specified width   
  32. quad = new Quad(quadWidth, quadHeight);   
  33. // add the legend   
  34. legend = new TextField(100, 20, "Hello Starling!", "Arial", 14, 0xFFFFFF);   
  35. // add the children   
  36. addChild(quad);   
  37. addChild(legend);   
  38. // change the registration point   
  39. pivotX = width >> 1;   
  40. pivotY = height >> 1;   
  41. }   
  42. private function resetColors():void   
  43. {   
  44. // pick random color components   
  45. rDest = Math.random()*255;   
  46. gDest = Math.random()*255;   
  47. bDest = Math.random()*255;   
  48. }   
  49. /**   
  50. * Updates the internal behavior   
  51. *   
  52. */   
  53. public function update ():void   
  54. {   
  55. // easing on the components   
  56. r -= (r - rDest) * .01;   
  57. g -= (g - gDest) * .01;   
  58. b -= (b - bDest) * .01;   
  59. // assemble the color   
  60. var color:uint = r << 16 | g << 8 | b;   
  61. quad.color = color;   
  62. // when reaching the color, pick another one   
  63. if ( Math.abs( r - rDest) < 1 && Math.abs( g - gDest) < 1 && Math.abs( b - bDest) )   
  64. resetColors();   
  65. // rotate it!   
  66. //rotation += .01;   
  67. }   
  68. }   
  69. }   


现在是我们修改过的 Game 类:


 

[plain] view plaincopy
  1. package   
  2. {   
  3. import starling.display.Sprite;   
  4. import starling.events.Event;   
  5. public class Game extends Sprite   
  6. {   
  7. private var customSprite:CustomSprite;   
  8. public function Game()   
  9. {   
  10. addEventListener(Event.ADDED_TO_STAGE, onAdded);   
  11. }   
  12. private function onAdded ( e:Event ):void   
  13. {   
  14. // create the custom sprite   
  15. customSprite = new CustomSprite(200, 200);   
  16. // positions it by default in the center of the stage   
  17. // we add half width because of the registration point of the custom sprite (middle)   
  18. customSprite.x = (stage.stageWidth - customSprite.width >> 1 ) + (customSprite.width  
  19. customSprite.y = (stage.stageHeight - customSprite.height >> 1) + (customSprite.heig  
  20. // show it   
  21. addChild(customSprite);   
  22. // need to comment this one ? ;)   
  23. stage.addEventListener(Event.ENTER_FRAME, onFrame);   
  24. }   
  25. private function onFrame (e:Event):void   
  26. {   
  27. // we update our custom sprite   
  28. customSprite.update();   
  29. }   
  30. }   
  31.  }   

在 CustomSprite 中开放了的 update 的接口允许外部调用之以实现 CustomSprite 中设置

[plain] view plaincopy
  1. package   
  2. {   
  3. import flash.geom.Point;   
  4. import starling.display.Sprite;   
  5. import starling.events.Event;   
  6. import starling.events.Touch;   
  7. import starling.events.TouchEvent;   
  8. public class Game extends Sprite   
  9. {   
  10. private var customSprite:CustomSprite;   
  11. private var mouseX:Number = 0;   
  12. private var mouseY:Number = 0;   
  13. public function Game()   
  14. {   
  15. addEventListener(Event.ADDED_TO_STAGE, onAdded);   
  16. }   
  17. private function onAdded ( e:Event ):void   
  18. {   
  19. // create the custom sprite   
  20. customSprite = new CustomSprite(200, 200);   
  21. // positions it by default in the center of the stage   
  22. // we add half width because of the registration point of the custom sprite (middle)   
  23. customSprite.x = (stage.stageWidth - customSprite.width >> 1 ) + (customSprite.width >> 1);   
  24. customSprite.y = (stage.stageHeight - customSprite.height >> 1) + (customSprite.height >> 1);  
  25. // show it   
  26. addChild(customSprite);   
  27. // we listen to the mouse movement on the stage   
  28. stage.addEventListener(TouchEvent.TOUCH, onTouch);   
  29. // need to comment this one ? ;)   
  30. stage.addEventListener(Event.ENTER_FRAME, onFrame);   
  31. }   
  32. private function onFrame (e:Event):void   
  33. {   
  34. // easing on the custom sprite position   
  35. customSprite.x -= ( customSprite.x - mouseX ) * .1;   
  36. customSprite.y -= ( customSprite.y - mouseY ) * .1;   
  37. // we update our custom sprite   
  38. customSprite.update();   
  39. }   
  40. private function onTouch (e:TouchEvent):void   
  41. {   
  42. // get the mouse location related to the stage   
  43. var touch:Touch = e.getTouch(stage);   
  44. var pos:Point = touch.getLocation(stage);   
  45. // store the mouse coordinates   
  46. mouseX = pos.x;   
  47. mouseY = pos.y;   
  48. }   
  49. }   
  50. }   

值得注意的是,在这段代码中我们没有使用任何 Mouse  API(指代鼠标事件),事实上,在
Starling中并没有设计鼠标的概念,稍后我们会讨论到。 
通过对 TouchEvent.TOUCH 事件的侦听,我们可以对鼠标/手指移动的侦测,这个事件的
用法就像我们经典的 MousEvent.MOUSE_MOVE 事件一样。每一帧我们都可以依靠
TouchEvent 中的辅助 API 如 getTouch 及 getLocation 来获取并保存当前鼠标位置。在
onFrame 这个事件处理函数中我们使用存储的鼠标位置作为目的地并运用一个简单的缓动
方程式来让方块渐渐运动至此。 
就像之前我们说的,Starling不仅让我们能享受到更加便捷的 GPU应用编码过程,还能让我
们在清理、回收对象时更加地方便。举个例子,假如我们需要在鼠标点击方块的时候将此方
块对象从舞台上移除,那么你就需要写下面这样的几句代码: 

的逻辑循环。 
现在,让我们来为我们这个小小的测试程序再多加一个交互功能:让我们的方块跟着鼠标运
动。因我们需要添加一些代码(粗体部分)来实现之: 

[plain] view plaincopy
  1. package   
  2. {   
  3. import flash.geom.Point;   
  4. import starling.display.DisplayObject;   
  5. import starling.display.Sprite;   
  6. import starling.events.Event;   
  7. import starling.events.Touch;   
  8. import starling.events.TouchEvent;   
  9. import starling.events.TouchPhase;   
  10. public class Game extends Sprite   
  11. {   
  12. private var customSprite:CustomSprite;   
  13. private var mouseX:Number = 0;   
  14. private var mouseY:Number = 0;   
  15. public function Game()   
  16. {   
  17. addEventListener(Event.ADDED_TO_STAGE, onAdded);   
  18. }   
  19. private function onAdded ( e:Event ):void   
  20. {   
  21. // create the custom sprite   
  22. customSprite = new CustomSprite(200, 200);   
  23. // positions it by default in the center of the stage   
  24. // we add half width because of the registration point of the custom s  
  25. customSprite.x = (stage.stageWidth - customSprite.width >> 1 ) + (c  
  26. customSprite.y = (stage.stageHeight - customSprite.height >> 1) + (  
  27. // show it   
  28. addChild(customSprite);   
  29. // we listen to the mouse movement on the stage   
  30. stage.addEventListener(TouchEvent.TOUCH, onTouch);   
  31. // need to comment this one ? ;)   
  32. stage.addEventListener(Event.ENTER_FRAME, onFrame);   
  33. // when the sprite is touched   
  34. customSprite.addEventListener(TouchEvent.TOUCH, onTouche  
  35. }   
  36. private function onFrame (e:Event):void   
  37. {   
  38. // easing on the custom sprite position   
  39. customSprite.x -= ( customSprite.x - mouseX ) * .1;   
  40. customSprite.y -= ( customSprite.y - mouseY ) * .1;   
  41. // we update our custom sprite   
  42. customSprite.update();   
  43. }   
  44. private function onTouch (e:TouchEvent):void   
  45. {   
  46. // get the mouse location related to the stage   
  47. var touch:Touch = e.getTouch(stage);   
  48. var pos:Point = touch.getLocation(stage);   
  49. // store the mouse coordinates   
  50. mouseX = pos.x;   
  51. mouseY = pos.y;   
  52. }   
  53. private function onTouchedSprite(e:TouchEvent):void   
  54. {   
  55. // get the touch points (can be multiple because of multitouch)   
  56. var touch:Touch = e.getTouch(stage);   
  57. var clicked:DisplayObject = e.currentTarget as DisplayObject;   
  58. // detect the click/release phase   
  59. if ( touch.phase == TouchPhase.ENDED )   
  60. {   
  61. // remove the clicked object   
  62. removeChild(clicked);   
  63. }   
  64. }   
  65. }   
  66. }  


注意,我们虽然将对象从显示列表中移除了,但是我们还未移除其 Event.ENTER_FRAME

事件的侦听器。为了证实这一点,我们使用 hasEventListener 这个 API 来对 Sprite 对象做
一个测试。

[plain] view plaincopy
  1. private function onTouchedSprite(e:TouchEvent):void   
  2. {   
  3. // get the touch points (can be multiple because of multitouch)   
  4. var touch:Touch = e.getTouch(stage);   
  5. var clicked:DisplayObject = e.currentTarget as DisplayObject;   
  6. // detect the click/release phase   
  7. if ( touch.phase == TouchPhase.ENDED )   
  8. {   
  9. // remove the clicked object   
  10. removeChild(clicked);   
  11. // outputs : true   
  12. trace ( clicked.hasEventListener(e.type) );   
  13. }   
  14. }   


可见,即使将对象从显示列表中移除了,它的事件侦听器依然残留着。为了更加安全、彻底
地移除一个对象,我们需要给 removeChild 方法设置其第二个参数 dispose 为 true,这样
可以让我们在从显示列表中移除一个对象的时候自动移除其所有的事件侦听器:

[plain] view plaincopy
  1. private function onTouchedSprite(e:TouchEvent):void   
  2. {   
  3. // get the touch points (can be multiple because of multitouch)   
  4. var touch:Touch = e.getTouch(stage);   
  5. var clicked:DisplayObject = e.currentTarget as DisplayObject;   
  6. // detect the click/release phase   
  7. if ( touch.phase == TouchPhase.ENDED )   
  8. {   
  9. // remove and dispose all the listeners   
  10. removeChild(clicked, true);   
  11. // outputs : false   
  12. trace ( clicked.hasEventListener(e.type) );   
  13. }   
  14. }   


我们这样做可以确保完全地移除一个对象,且如果此对象中还有子对象,那么这些子对象也
都会被完全移除。dispose 这个参数在其他移除对象的 API 中均存在,如 removeChildren、
removeChildAt。需要注意的是,完全移除一个对象,不仅仅移除了其纹理,其在 GPU 中
所占内存也会被一并释放掉。若你只是想移除一个纹理,那么你可以通过调用 Texture 或
TextureAtlas 对象的dispose 方法来做到这一点。 
除了使用上面提到的一系列 remove 开头的方法来完全移除一个子对象之外,你还可以直接
调用一个 DisplayObject 对象的 dispose 方法来实现对象的自移除。 
clicked.dispose() 
我们在刚刚那个小测试中初次使用了一下 Starling 中的事件机制,是不是感觉和原生 Flash
使用方式没多大差别呢?那么现在让我们再花一点时间来了解一下 Starling 中的
EventDispatcher 这个 API。

posted on 2013-05-21 18:26  alvin.zhang  阅读(251)  评论(0编辑  收藏  举报

导航