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的相关视频和教程:)


被折叠的 条评论
为什么被折叠?



