Flex4组件教程:自定义两级导航菜单组件
声明:本文为RIAMeeting原创文章,您可以*转载,但请务必说明文章来源并保留原文链接,谢谢!
对于两级导航菜单,我们应该不会陌生,很多网站都使用了这个效果,如下面这张图片所示,当鼠标划过或点击一级菜单,会出现相应的二级菜单,也就是我们经常说的菜单联动效果。
当然图片中的这个效果是基于HTML和JavaScript实现的,那么在Flex开发中,我们能否达到这样的效果呢?当然是可以的,下面我们就来探讨一下实现步骤。
实现思路
在动手写代码之前,先别急,考虑一下如何实现。最先要考虑的,是Flex中是否已经提供了现成的两级菜单组件?如果有的话,我们直接拿来用就是,没必要自己重复造*了,但很可惜,Flex本身没有提供这样的组件。不过有一个和我们想实现的很类似的组件,就是ButtonBar。ButtonBar默认就支持对内部导航按钮进行排版,并维持选中状态。如下图所示:
但它默认只有一级结构,所以我们需要扩展一下ButtonBar,实现我们想要的二级结构。完成结果如下图所示:
准备工作
首先准备好定制皮肤所需的位图文件,当然您也可以用MXML图形直接在皮肤里绘制,但如果图形过于复杂的话,对于运行时性能会有影响,如果是这种情况还是用合并图层后的图片来处理比较合适。
因为是开发Flex组件,工具建议使用Flash Builder。
另外您需要了解为Spark组件定制皮肤的基本知识,下面是一些参考文章或视频:
- 视频教程:一周学习Flex4视频中文版 (重点看Spark组件的介绍和外观定制部分)
- 文章:在Flex组件外观实施中使用Scale9
实现过程
首先准备好数据源,采用ArrayList:
[Bindable ] private var menuData :ArrayList = new ArrayList ( [ {id : 1, name : "首页" }, {id : 2, name : "管理",subMenu : new ArrayList ( [ {id : 21, name : "管理" } ] ) }, {id : 3, name : "导航",subMenu : new ArrayList ( [ {id : 21, name : "导航1" }, {id : 22, name : "导航2" }, {id : 22, name : "导航3" } ] ) }, {id : 4, name : "吃饭" }, {id : 4, name : "睡觉" }, {id : 3, name : "打豆豆",subMenu : new ArrayList ( [ {id : 21, name : "豆豆 1" } ] ) }, {id : 4, name : "退出" }, ] );
然后创建自定义组件MenuSlider,里面放两个ButtonBar,一个是一级菜单,一个是二级菜单,为了提升用户体验,我们加了一些过渡动画,也就是其中Transition部分的定义。并且我们创建了两个ArrayList,分别为一级菜单和二级菜单供应数据,二级菜单的数据是从一级菜单中的选项中获取的。
您可能已经注意到,对于二级菜单,因为需要使用一些自定义属性,所以我们扩展了ButtonBar,但只是加了属性,并无其它逻辑修改。
MenuSlider.mxml
<s :SkinnableContainer xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark" xmlns :mx= "library://ns.adobe.com/flex/mx" width= "100%" height= "65" xmlns :comp= "comp.*" > <fx :Metadata > </fx :Metadata > <fx :Script > <! [CDATA [ import mx.collections.ArrayList; import spark.events.IndexChangeEvent; [Bindable ] /**一级数据源*/ public var dataProvider :ArrayList; [Bindable ] /**二级数据源*/ public var subDataProvider :ArrayList; /**当前选中项,可以是一级或二级中的项*/ [Bindable ] protected function mainButtonBarChangeHandler (event :IndexChangeEvent ) : void { currentState = "normal"; selectedItem = mainButtonBar.selectedItem; if (selectedItem== null ) return; if (mainButtonBar.selectedItem.subMenu != null ) { subDataProvider = null; currentState = "sub"; prevDataNotNull = true; } else { prevDataNotNull = false; } } protected function validateSubMenu ( ) : void { autoLayout= true; if (currentState == "sub" ) { subButtonBar. y = 35; subDataProvider = mainButtonBar.selectedItem.subMenu; } else { subButtonBar. y = 0; subDataProvider = null; } } private function subButtonBarChangeHandler (event :IndexChangeEvent ) : void { selectedItem = subButtonBar.selectedItem; if (selectedItem == null ) return; } ] ] > </fx :Script > <s :states > <s :State name= "normal" /> <s :State name= "sub" /> </s :states > <s :transitions > <s :Transition fromState= "normal" toState= "sub" > <s :Sequence target= "{subButtonBar}" effectStart= "autoLayout=false" effectEnd= "validateSubMenu()" > <s :Move yFrom= "35" yTo= "0" duration= "{prevDataNotNull?500:0}" /> <s :Move yFrom= "0" yTo= "35" /> </s :Sequence > </s :Transition > <s :Transition fromState= "sub" toState= "normal" > <s :Move target= "{subButtonBar}" yTo= "0" effectStart= "autoLayout=false" effectEnd= "validateSubMenu()" /> </s :Transition > </s :transitions > <comp :SubButtonBar id= "subButtonBar" width= "100%" height= "30" styleName= "subButtonBar" dataProvider= "{subDataProvider}" labelField= "name" mainDataProvider= "{dataProvider}" mainSelectedIndex= "{mainButtonBar.selectedIndex}" change= "subButtonBarChangeHandler(event)" /> <s :ButtonBar id= "mainButtonBar" width= "100%" height= "35" styleName= "mainButtonBar" dataProvider= "{dataProvider}" labelField= "name" change= "mainButtonBarChangeHandler(event)" /> </s :SkinnableContainer >
SubButtonBar.as
public class SubButtonBar extends ButtonBar { [Bindable ] public var mainDataProvider :IList; [Bindable ] public function SubButtonBar ( ) { super ( ); } }
因为ButtonBar是可以定制外观的,所以我们为MainButtonBar和SubButtonBar分别定义了外观:
MainButtonBar的外观定义:
<s :Skin xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark" alpha.disabled= "0.5" > <fx :Metadata > <! [CDATA [ /** * @copy spark.skins.spark.ApplicationSkin#hostComponent */ [HostComponent ( "spark.components.ButtonBar" ) ] ] ] > </fx :Metadata > <s :states > <s :State name= "normal" /> <s :State name= "disabled" /> </s :states > <fx :Declarations > <!--- @ copy spark.components.ButtonBar#firstButton @ default spark.skins.spark.ButtonBarFirstButtonSkin @see spark.skins.spark.ButtonBarFirstButtonSkin --> <fx :Component id= "firstButton" > <s :ButtonBarButton styleName= "mainSelectButton" /> </fx :Component > <!--- @ copy spark.components.ButtonBar#middleButton @ default spark.skins.spark.ButtonBarMiddleButtonSkin @see spark.skins.spark.ButtonBarMiddleButtonSkin --> <fx :Component id= "middleButton" > <s :ButtonBarButton styleName= "mainSelectButton" /> </fx :Component > <!--- @ copy spark.components.ButtonBar#lastButton @ default spark.skins.spark.ButtonBarLastButtonSkin @see spark.skins.spark.ButtonBarLastButtonSkin --> <fx :Component id= "lastButton" > <s :ButtonBarButton styleName= "mainSelectButton" /> </fx :Component > </fx :Declarations > <s :Rect id= "bgFill" width= "100%" height= "100%" > <s :fill > <s :SolidColor color= "#000000" /> </s :fill > </s :Rect > <!--- @ copy spark.components.SkinnableDataContainer#dataGroup --> <s :DataGroup id= "dataGroup" width= "100%" height= "100%" > <s :layout > <s :ButtonBarHorizontalLayout gap= "0" /> </s :layout > </s :DataGroup > </s :Skin >
注意外观部分我们没有做什么改变,只是将内部所需的ButtonBarButton的皮肤做了变更(参见源码中的CSS部分的定义,这里不再阐述)
SubButtonBar的皮肤定义:
<s :Skin xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark" alpha.disabled= "0.5" xmlns :layout= "layout.*" > <fx :Metadata > <! [CDATA [ /** * @copy spark.skins.spark.ApplicationSkin#hostComponent */ [HostComponent ( "comp.SubButtonBar" ) ] ] ] > </fx :Metadata > <s :states > <s :State name= "normal" /> <s :State name= "disabled" /> </s :states > <fx :Script > <! [CDATA [ [Bindable ] normalBgImage= getStyle ( "icon" ); super.updateDisplayList (unscaledWidth,unscaledHeight ); } ] ] > </fx :Script > <fx :Declarations > <!--- @ copy spark.components.ButtonBar#firstButton @ default spark.skins.spark.ButtonBarFirstButtonSkin @see spark.skins.spark.ButtonBarFirstButtonSkin --> <fx :Component id= "firstButton" > <s :ButtonBarButton styleName= "subSelectButton" /> </fx :Component > <!--- @ copy spark.components.ButtonBar#middleButton @ default spark.skins.spark.ButtonBarMiddleButtonSkin @see spark.skins.spark.ButtonBarMiddleButtonSkin --> <fx :Component id= "middleButton" > <s :ButtonBarButton styleName= "subSelectButton" /> </fx :Component > <!--- @ copy spark.components.ButtonBar#lastButton @ default spark.skins.spark.ButtonBarLastButtonSkin @see spark.skins.spark.ButtonBarLastButtonSkin --> <fx :Component id= "lastButton" > <s :ButtonBarButton styleName= "subSelectButton" /> </fx :Component > </fx :Declarations > <s :BitmapImage id= "bgFill" width= "100%" height= "100%" source= "{normalBgImage}" smooth= "true" /> <!--- @ copy spark.components.SkinnableDataContainer#dataGroup --> <s :DataGroup id= "dataGroup" width= "100%" height= "100%" > <s :layout > <layout :SubMenuBarLayout mainDataProvider= "{hostComponent.mainDataProvider}" mainSelectedIndex= "{hostComponent.mainSelectedIndex}" gap= "0" /> </s :layout > </s :DataGroup > </s :Skin >
注意我们除了替换内部按钮的样式,也将SubButtonBar的背景换成了一张图片,图片的嵌入定义参见CSS部分的定义。
对于ButtonBar内部的每个按钮,我们将外观改成了由嵌入图片组成的实现机制(注意bgImage的定义):
<s :SparkSkin xmlns :fx= "http://ns.adobe.com/mxml/2009" xmlns :s= "library://ns.adobe.com/flex/spark" xmlns :fb= "http://ns.adobe.com/flashbuilder/2009" minWidth= "21" minHeight= "21" alpha.disabledStates= "0.5" > <fx :Metadata > [HostComponent ( "spark.components.ButtonBarButton" ) ] </fx :Metadata > <!-- host component --> <fx :Script fb :purpose= "styling" > /* Define the skin elements that should not be colorized. For toggle button, the graphics are colorized but the label is not. */ [Bindable ] [Bindable ] [Bindable ] [Bindable ] [Bindable ] /** * @private */ /** * @private */ override protected function initializationComplete ( ) : void { useChromeColor = true; super.initializationComplete ( ); this. buttonMode = true; this. mouseChildren = false; } /** * @private */ { normalBgImage= getStyle ( "icon" ); downBgImage= getStyle ( "downIcon" ); overBgImage= getStyle ( "overIcon" ); disableBgImage= getStyle ( "disableIcon" ); selectedBgImage= getStyle ( "selectedIcon" ); super.updateDisplayList (unscaledWidth, unscaledHeight ); } </fx :Script > <!-- states --> <s :states > <s :State name= "up" /> <s :State name= "over" stateGroups= "overStates" /> <s :State name= "down" stateGroups= "downStates" /> <s :State name= "disabled" stateGroups= "disabledStates" /> <s :State name= "upAndSelected" stateGroups= "selectedStates, selectedUpStates" /> <s :State name= "overAndSelected" stateGroups= "overStates, selectedStates" /> <s :State name= "downAndSelected" stateGroups= "downStates, selectedStates" /> <s :State name= "disabledAndSelected" stateGroups= "selectedUpStates, disabledStates, selectedStates" /> </s :states > <s :transitions > <s :Transition fromState= "*" toState= "over" > <s :Fade target= "{bgImage}" alphaFrom= "0.6" alphaTo= "1" /> </s :Transition > </s :transitions > <s :BitmapImage id= "bgImage" width= "100%" height= "100%" source.up= "{normalBgImage}" source.over= "{overBgImage}" source.down= "{downBgImage}" source.disabled= "{disableBgImage}" source.upAndSelected= "{selectedBgImage}" source.overAndSelected= "{selectedBgImage}" source.downAndSelected= "{selectedBgImage}" source.disabledAndSelected= "{disableBgImage}" /> <!-- layer 8 : text --> <!--- @ copy spark.components.supportClasses.ButtonBase#labelDisplay --> <s :Label id= "labelDisplay" textAlign= "center" verticalAlign= "middle" maxDisplayedLines= "1" horizontalCenter= "0" verticalCenter= "1" left= "10" right= "10" top= "2" bottom= "2" > </s :Label > </s :SparkSkin >
最后我们发现,SubButtonBar的布局处理方式跟MainButtonBar是不一样的,它需要将按钮与MainButtonBar中的选中按钮的位置有对齐关系,这样看起来更美观一些。得益于Spark容器外观与布局的分离,我们可以扩展出一个布局类来实现这个功能:
public class SubMenuBarLayout extends ButtonBarHorizontalLayout { [Bindable ] public var mainDataProvider :IList; [Bindable ] public function SubMenuBarLayout ( ) { super ( ); } { var layoutTarget :GroupBase = target; if ( !layoutTarget ) return; itemWidth = width /mainDataProvider. length; var layoutElement :ILayoutElement; if (itemWidth * (count + 1 ) >= width ) { super.updateDisplayList ( width, height ); } else { // Resize and position children mainMenuItemX = itemWidth *mainSelectedIndex; if (paddingLeft +itemWidth *count < mainMenuItemX ) { paddingLeft = mainMenuItemX -itemWidth * (count - 1 ) +gap; } x +=paddingLeft; for (i = 0; i < count; i ++ ) { layoutElement = layoutTarget.getElementAt (i ); if ( !layoutElement || !layoutElement.includeInLayout ) continue; layoutElement.setLayoutBoundsSize (itemWidth, height ); layoutElement.setLayoutBoundsPosition ( x, 0 ); x += gap + layoutElement.getLayoutBoundsWidth ( ); } } } }
这个自定义的布局类在SubButtonBar的皮肤中得到了应用:
<s :DataGroup id= "dataGroup" width= "100%" height= "100%" > <s :layout > <layout :SubMenuBarLayout mainDataProvider= "{hostComponent.mainDataProvider}" mainSelectedIndex= "{hostComponent.mainSelectedIndex}" gap= "0" /> </s :layout > </s :DataGroup >
由于时间仓促,关于CSS中的定义步骤以及和Skin的整合,不在本文阐述了,请读者自行参阅源码和RIAMeeting的相关视频和教程:)