Flex4组件教程:自定义两级导航菜单组件

时间:2022-06-06 16:19:19

Flex4组件教程:自定义两级导航菜单组件

声明:本文为RIAMeeting原创文章,您可以*转载,但请务必说明文章来源并保留原文链接,谢谢!

对于两级导航菜单,我们应该不会陌生,很多网站都使用了这个效果,如下面这张图片所示,当鼠标划过或点击一级菜单,会出现相应的二级菜单,也就是我们经常说的菜单联动效果。

Flex4组件教程:自定义两级导航菜单组件

当然图片中的这个效果是基于HTML和JavaScript实现的,那么在Flex开发中,我们能否达到这样的效果呢?当然是可以的,下面我们就来探讨一下实现步骤。

实现思路

在动手写代码之前,先别急,考虑一下如何实现。最先要考虑的,是Flex中是否已经提供了现成的两级菜单组件?如果有的话,我们直接拿来用就是,没必要自己重复造*了,但很可惜,Flex本身没有提供这样的组件。不过有一个和我们想实现的很类似的组件,就是ButtonBar。ButtonBar默认就支持对内部导航按钮进行排版,并维持选中状态。如下图所示:

Flex4组件教程:自定义两级导航菜单组件

但它默认只有一级结构,所以我们需要扩展一下ButtonBar,实现我们想要的二级结构。完成结果如下图所示:

Flex4组件教程:自定义两级导航菜单组件

点击这里查看编译后的SWF文件

准备工作

首先准备好定制皮肤所需的位图文件,当然您也可以用MXML图形直接在皮肤里绘制,但如果图形过于复杂的话,对于运行时性能会有影响,如果是这种情况还是用合并图层后的图片来处理比较合适。

因为是开发Flex组件,工具建议使用Flash Builder。

另外您需要了解为Spark组件定制皮肤的基本知识,下面是一些参考文章或视频:

实现过程

首先准备好数据源,采用ArrayList:

 

     
     
    
    
  1. [Bindable ]
  2. private var menuData :ArrayList = new ArrayList ( [
  3. {id : 1, name : "首页" },
  4. {id : 2, name : "管理",subMenu : new ArrayList ( [
  5. {id : 21, name : "管理" }
  6. ] ) },
  7. {id : 3, name : "导航",subMenu : new ArrayList ( [
  8. {id : 21, name : "导航1" },
  9. {id : 22, name : "导航2" },
  10. {id : 22, name : "导航3" }
  11. ] ) },
  12. {id : 4, name : "吃饭" },
  13. {id : 4, name : "睡觉" },
  14. {id : 3, name : "打豆豆",subMenu : new ArrayList ( [
  15. {id : 21, name : "豆豆 1" }
  16. ] ) },
  17. {id : 4, name : "退出" },
  18. ] );

 

然后创建自定义组件MenuSlider,里面放两个ButtonBar,一个是一级菜单,一个是二级菜单,为了提升用户体验,我们加了一些过渡动画,也就是其中Transition部分的定义。并且我们创建了两个ArrayList,分别为一级菜单和二级菜单供应数据,二级菜单的数据是从一级菜单中的选项中获取的。

您可能已经注意到,对于二级菜单,因为需要使用一些自定义属性,所以我们扩展了ButtonBar,但只是加了属性,并无其它逻辑修改。

MenuSlider.mxml

 

     
     
    
    
  1. <s :SkinnableContainer xmlns :fx= "http://ns.adobe.com/mxml/2009"
  2. xmlns :s= "library://ns.adobe.com/flex/spark"
  3. xmlns :mx= "library://ns.adobe.com/flex/mx" width= "100%" height= "65" xmlns :comp= "comp.*" >
  4. <fx :Metadata >
  5. [ Event ( name= "change", type= "flash.events.Event" ) ]
  6. </fx :Metadata >
  7. <fx :Script >
  8. <! [CDATA [
  9. import mx.collections.ArrayList;
  10.  
  11. import spark.events.IndexChangeEvent;
  12. [Bindable ]
  13. /**一级数据源*/
  14. public var dataProvider :ArrayList;
  15. [Bindable ]
  16. /**二级数据源*/
  17. public var subDataProvider :ArrayList;
  18. /**当前选中项,可以是一级或二级中的项*/
  19. public var selectedItem : Object;
  20.  
  21. [Bindable ]
  22. private var prevDataNotNull : Boolean;
  23.  
  24. protected function mainButtonBarChangeHandler (event :IndexChangeEvent ) : void {
  25. currentState = "normal";
  26. selectedItem = mainButtonBar.selectedItem;
  27. if (selectedItem== null ) return;
  28. if (mainButtonBar.selectedItem.subMenu != null ) {
  29. subDataProvider = null;
  30. currentState = "sub";
  31. prevDataNotNull = true;
  32. } else {
  33. prevDataNotNull = false;
  34. }
  35. dispatchEvent ( new Event ( "change" ) );
  36. }
  37.  
  38. protected function validateSubMenu ( ) : void {
  39. autoLayout= true;
  40. if (currentState == "sub" ) {
  41. subButtonBar. y = 35;
  42. subDataProvider = mainButtonBar.selectedItem.subMenu;
  43. } else {
  44. subButtonBar. y = 0;
  45. subDataProvider = null;
  46. }
  47. }
  48.  
  49. private function subButtonBarChangeHandler (event :IndexChangeEvent ) : void {
  50. selectedItem = subButtonBar.selectedItem;
  51. if (selectedItem == null ) return;
  52. dispatchEvent ( new Event ( "change" ) );
  53. }
  54.  
  55. ] ] >
  56. </fx :Script >
  57.  
  58. <s :states >
  59. <s :State name= "normal" />
  60. <s :State name= "sub" />
  61. </s :states >
  62.  
  63. <s :transitions >
  64. <s :Transition fromState= "normal" toState= "sub" >
  65. <s :Sequence target= "{subButtonBar}" effectStart= "autoLayout=false" effectEnd= "validateSubMenu()" >
  66. <s :Move yFrom= "35" yTo= "0" duration= "{prevDataNotNull?500:0}" />
  67. <s :Move yFrom= "0" yTo= "35" />
  68. </s :Sequence >
  69. </s :Transition >
  70. <s :Transition fromState= "sub" toState= "normal" >
  71. <s :Move target= "{subButtonBar}" yTo= "0" effectStart= "autoLayout=false" effectEnd= "validateSubMenu()" />
  72. </s :Transition >
  73. </s :transitions >
  74.  
  75. <comp :SubButtonBar id= "subButtonBar"
  76. width= "100%" height= "30" styleName= "subButtonBar"
  77. dataProvider= "{subDataProvider}" labelField= "name"
  78. mainDataProvider= "{dataProvider}"
  79. mainSelectedIndex= "{mainButtonBar.selectedIndex}"
  80. change= "subButtonBarChangeHandler(event)"
  81. />
  82. <s :ButtonBar id= "mainButtonBar"
  83. width= "100%" height= "35" styleName= "mainButtonBar"
  84. dataProvider= "{dataProvider}" labelField= "name"
  85. change= "mainButtonBarChangeHandler(event)"
  86. />
  87.  
  88. </s :SkinnableContainer >

 

SubButtonBar.as

 

     
     
    
    
  1. public class SubButtonBar extends ButtonBar
  2. {
  3. [Bindable ]
  4. public var mainDataProvider :IList;
  5.  
  6. [Bindable ]
  7. public var mainSelectedIndex : int;
  8.  
  9. public function SubButtonBar ( )
  10. {
  11. super ( );
  12. }
  13. }

 

因为ButtonBar是可以定制外观的,所以我们为MainButtonBar和SubButtonBar分别定义了外观:

MainButtonBar的外观定义:

 

     
     
    
    
  1. <s :Skin xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark"
  2. alpha.disabled= "0.5" >
  3.  
  4. <fx :Metadata >
  5. <! [CDATA [
  6. /**
  7.   * @copy spark.skins.spark.ApplicationSkin#hostComponent
  8.   */
  9. [HostComponent ( "spark.components.ButtonBar" ) ]
  10. ] ] >
  11. </fx :Metadata >
  12.  
  13. <s :states >
  14. <s :State name= "normal" />
  15. <s :State name= "disabled" />
  16. </s :states >
  17.  
  18. <fx :Declarations >
  19. <!---
  20. @ copy spark.components.ButtonBar#firstButton
  21. @ default spark.skins.spark.ButtonBarFirstButtonSkin
  22. @see spark.skins.spark.ButtonBarFirstButtonSkin
  23. -->
  24. <fx :Component id= "firstButton" >
  25. <s :ButtonBarButton styleName= "mainSelectButton" />
  26. </fx :Component >
  27.  
  28. <!---
  29. @ copy spark.components.ButtonBar#middleButton
  30. @ default spark.skins.spark.ButtonBarMiddleButtonSkin
  31. @see spark.skins.spark.ButtonBarMiddleButtonSkin
  32. -->
  33. <fx :Component id= "middleButton" >
  34. <s :ButtonBarButton styleName= "mainSelectButton" />
  35. </fx :Component >
  36.  
  37. <!---
  38. @ copy spark.components.ButtonBar#lastButton
  39. @ default spark.skins.spark.ButtonBarLastButtonSkin
  40. @see spark.skins.spark.ButtonBarLastButtonSkin
  41. -->
  42. <fx :Component id= "lastButton" >
  43. <s :ButtonBarButton styleName= "mainSelectButton" />
  44. </fx :Component >
  45.  
  46. </fx :Declarations >
  47.  
  48. <s :Rect id= "bgFill" width= "100%" height= "100%" >
  49. <s :fill >
  50. <s :SolidColor color= "#000000" />
  51. </s :fill >
  52. </s :Rect >
  53.  
  54. <!--- @ copy spark.components.SkinnableDataContainer#dataGroup -->
  55. <s :DataGroup id= "dataGroup" width= "100%" height= "100%" >
  56. <s :layout >
  57. <s :ButtonBarHorizontalLayout gap= "0" />
  58. </s :layout >
  59. </s :DataGroup >
  60.  
  61. </s :Skin >

 

注意外观部分我们没有做什么改变,只是将内部所需的ButtonBarButton的皮肤做了变更(参见源码中的CSS部分的定义,这里不再阐述)

SubButtonBar的皮肤定义:

 

     
     
    
    
  1. <s :Skin xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark"
  2. alpha.disabled= "0.5" xmlns :layout= "layout.*" >
  3.  
  4. <fx :Metadata >
  5. <! [CDATA [
  6. /**
  7.   * @copy spark.skins.spark.ApplicationSkin#hostComponent
  8.   */
  9. [HostComponent ( "comp.SubButtonBar" ) ]
  10. ] ] >
  11. </fx :Metadata >
  12.  
  13. <s :states >
  14. <s :State name= "normal" />
  15. <s :State name= "disabled" />
  16. </s :states >
  17.  
  18. <fx :Script >
  19. <! [CDATA [
  20. [Bindable ]
  21. private var normalBgImage : Class;
  22. override protected function updateDisplayList (unscaledWidth : Number, unscaledHeight : Number ) : void {
  23. normalBgImage= getStyle ( "icon" );
  24. super.updateDisplayList (unscaledWidth,unscaledHeight );
  25. }
  26. ] ] >
  27. </fx :Script >
  28.  
  29. <fx :Declarations >
  30. <!---
  31. @ copy spark.components.ButtonBar#firstButton
  32. @ default spark.skins.spark.ButtonBarFirstButtonSkin
  33. @see spark.skins.spark.ButtonBarFirstButtonSkin
  34. -->
  35. <fx :Component id= "firstButton" >
  36. <s :ButtonBarButton styleName= "subSelectButton" />
  37. </fx :Component >
  38.  
  39. <!---
  40. @ copy spark.components.ButtonBar#middleButton
  41. @ default spark.skins.spark.ButtonBarMiddleButtonSkin
  42. @see spark.skins.spark.ButtonBarMiddleButtonSkin
  43. -->
  44. <fx :Component id= "middleButton" >
  45. <s :ButtonBarButton styleName= "subSelectButton" />
  46. </fx :Component >
  47.  
  48. <!---
  49. @ copy spark.components.ButtonBar#lastButton
  50. @ default spark.skins.spark.ButtonBarLastButtonSkin
  51. @see spark.skins.spark.ButtonBarLastButtonSkin
  52. -->
  53. <fx :Component id= "lastButton" >
  54. <s :ButtonBarButton styleName= "subSelectButton" />
  55. </fx :Component >
  56.  
  57. </fx :Declarations >
  58.  
  59. <s :BitmapImage id= "bgFill" width= "100%" height= "100%" source= "{normalBgImage}" smooth= "true" />
  60.  
  61. <!--- @ copy spark.components.SkinnableDataContainer#dataGroup -->
  62. <s :DataGroup id= "dataGroup" width= "100%" height= "100%" >
  63. <s :layout >
  64. <layout :SubMenuBarLayout
  65. mainDataProvider= "{hostComponent.mainDataProvider}"
  66. mainSelectedIndex= "{hostComponent.mainSelectedIndex}"
  67. gap= "0" />
  68. </s :layout >
  69. </s :DataGroup >
  70.  
  71. </s :Skin >

 

注意我们除了替换内部按钮的样式,也将SubButtonBar的背景换成了一张图片,图片的嵌入定义参见CSS部分的定义。

对于ButtonBar内部的每个按钮,我们将外观改成了由嵌入图片组成的实现机制(注意bgImage的定义):

 

     
     
    
    
  1. <s :SparkSkin xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark"
  2. xmlns :fb= "http://ns.adobe.com/flashbuilder/2009" minWidth= "21" minHeight= "21" alpha.disabledStates= "0.5" >
  3. <fx :Metadata > [HostComponent ( "spark.components.ButtonBarButton" ) ] </fx :Metadata >
  4.  
  5. <!-- host component -->
  6. <fx :Script fb :purpose= "styling" >
  7. /* Define the skin elements that should not be colorized.
  8.   For toggle button, the graphics are colorized but the label is not. */
  9. static private const exclusions : Array = [ "labelDisplay" ];
  10.  
  11. [Bindable ]
  12. private var normalBgImage : Class;
  13. [Bindable ]
  14. private var downBgImage : Class;
  15. [Bindable ]
  16. private var overBgImage : Class;
  17. [Bindable ]
  18. private var disableBgImage : Class;
  19. [Bindable ]
  20. private var selectedBgImage : Class;
  21.  
  22. /**
  23.   * @private
  24.   */
  25. override public function get colorizeExclusions ( ) : Array { return exclusions; }
  26.  
  27. /**
  28.   * @private
  29.   */
  30. override protected function initializationComplete ( ) : void
  31. {
  32. useChromeColor = true;
  33. super.initializationComplete ( );
  34. this. buttonMode = true;
  35. this. mouseChildren = false;
  36. }
  37.  
  38. /**
  39.   * @private
  40.   */
  41. override protected function updateDisplayList (unscaledWidth : Number, unscaledHeight : Number ) : void
  42. {
  43. normalBgImage= getStyle ( "icon" );
  44. downBgImage= getStyle ( "downIcon" );
  45. overBgImage= getStyle ( "overIcon" );
  46. disableBgImage= getStyle ( "disableIcon" );
  47. selectedBgImage= getStyle ( "selectedIcon" );
  48.  
  49. super.updateDisplayList (unscaledWidth, unscaledHeight );
  50. }
  51.  
  52. private var cornerRadius : Number = 2;
  53. </fx :Script >
  54.  
  55. <!-- states -->
  56. <s :states >
  57. <s :State name= "up" />
  58. <s :State name= "over" stateGroups= "overStates" />
  59. <s :State name= "down" stateGroups= "downStates" />
  60. <s :State name= "disabled" stateGroups= "disabledStates" />
  61. <s :State name= "upAndSelected" stateGroups= "selectedStates, selectedUpStates" />
  62. <s :State name= "overAndSelected" stateGroups= "overStates, selectedStates" />
  63. <s :State name= "downAndSelected" stateGroups= "downStates, selectedStates" />
  64. <s :State name= "disabledAndSelected" stateGroups= "selectedUpStates, disabledStates, selectedStates" />
  65. </s :states >
  66.  
  67. <s :transitions >
  68. <s :Transition fromState= "*" toState= "over" >
  69. <s :Fade target= "{bgImage}" alphaFrom= "0.6" alphaTo= "1" />
  70. </s :Transition >
  71. </s :transitions >
  72.  
  73. <s :BitmapImage id= "bgImage"
  74. width= "100%" height= "100%"
  75. source.up= "{normalBgImage}"
  76. source.over= "{overBgImage}"
  77. source.down= "{downBgImage}"
  78. source.disabled= "{disableBgImage}"
  79. source.upAndSelected= "{selectedBgImage}"
  80. source.overAndSelected= "{selectedBgImage}"
  81. source.downAndSelected= "{selectedBgImage}"
  82. source.disabledAndSelected= "{disableBgImage}"
  83. />
  84.  
  85. <!-- layer 8 : text -->
  86. <!--- @ copy spark.components.supportClasses.ButtonBase#labelDisplay -->
  87. <s :Label id= "labelDisplay"
  88. textAlign= "center"
  89. verticalAlign= "middle"
  90. maxDisplayedLines= "1"
  91. horizontalCenter= "0" verticalCenter= "1"
  92. left= "10" right= "10" top= "2" bottom= "2" >
  93. </s :Label >
  94.  
  95. </s :SparkSkin >

 

最后我们发现,SubButtonBar的布局处理方式跟MainButtonBar是不一样的,它需要将按钮与MainButtonBar中的选中按钮的位置有对齐关系,这样看起来更美观一些。得益于Spark容器外观与布局的分离,我们可以扩展出一个布局类来实现这个功能:

 

     
     
    
    
  1. public class SubMenuBarLayout extends ButtonBarHorizontalLayout
  2. {
  3. [Bindable ]
  4. public var mainDataProvider :IList;
  5.  
  6. [Bindable ]
  7. public var mainSelectedIndex : int;
  8.  
  9. private var itemWidth : Number;
  10. private var mainMenuItemX : Number;
  11.  
  12. public function SubMenuBarLayout ( )
  13. {
  14. super ( );
  15. }
  16.  
  17. override public function updateDisplayList ( width : Number, height : Number ) : void
  18. {
  19. var layoutTarget :GroupBase = target;
  20. if ( !layoutTarget ) return;
  21. itemWidth = width /mainDataProvider. length;
  22. var count : int = layoutTarget.numElements;
  23. var layoutElement :ILayoutElement;
  24. if (itemWidth * (count + 1 ) >= width ) {
  25. super.updateDisplayList ( width, height );
  26. } else {
  27. // Resize and position children
  28. var i : int = 0;
  29. var x : Number = 0;
  30. var paddingLeft : Number = itemWidth;
  31. mainMenuItemX = itemWidth *mainSelectedIndex;
  32. if (paddingLeft +itemWidth *count < mainMenuItemX ) {
  33. paddingLeft = mainMenuItemX -itemWidth * (count - 1 ) +gap;
  34. }
  35. x +=paddingLeft;
  36. for (i = 0; i < count; i ++ )
  37. {
  38. layoutElement = layoutTarget.getElementAt (i );
  39. if ( !layoutElement || !layoutElement.includeInLayout )
  40. continue;
  41. layoutElement.setLayoutBoundsSize (itemWidth, height );
  42. layoutElement.setLayoutBoundsPosition ( x, 0 );
  43. x += gap + layoutElement.getLayoutBoundsWidth ( );
  44. }
  45. }
  46. }
  47.  
  48. }

 

这个自定义的布局类在SubButtonBar的皮肤中得到了应用:

 

     
     
    
    
  1. <s :DataGroup id= "dataGroup" width= "100%" height= "100%" >
  2. <s :layout >
  3. <layout :SubMenuBarLayout
  4. mainDataProvider= "{hostComponent.mainDataProvider}"
  5. mainSelectedIndex= "{hostComponent.mainSelectedIndex}"
  6. gap= "0" />
  7. </s :layout >
  8. </s :DataGroup >

 

由于时间仓促,关于CSS中的定义步骤以及和Skin的整合,不在本文阐述了,请读者自行参阅源码和RIAMeeting的相关视频和教程:)

源码下载

http://www.riameeting.com/examples/button_bar/srcview/