首页 > 代码库 > Flex4 双选下拉列表的实现(源代码)

Flex4 双选下拉列表的实现(源代码)

本文属原创文章,如需转载,请注明出处,谢谢

企业应用中少不了双选下拉列表控件,但几乎都没有独立的控件,Flex在这上面得天独厚,ArrayCollection的过滤功能使得我们只需要一个数据源就可以将数据展示在两个下拉列表中

有呆毛才有真相:

为了实现上图我想要的控件效果,我需要先明确我希望得到的控件该怎么用:

<ui:DDList width="400" height="300" labelField="label" labelFunction="labelFunc" 
  source
="{mySource}" values="@{myValues}" valueField="value"/>

这个控件应该能让我像其他List控件一样可以实现自定义label字段或labelFuntion实现,然后,指定一个数据源source:Array,这里没有使用ArrayCollection,我觉得双选组件一般情况下很少会对数据源进行增删操作,主要是左右移动,然后values非常重要,这是一个方便控件使用的属性,values:ArrayCollection表示选中的项是哪些
例如:

[Bindable]
private var source:Array = [
    {label:‘王大锤‘, value:0},
    {label:‘李小虎‘, value:1},
    {label:‘舒克‘, value:2},
    {label:‘贝塔‘, value:3},
    {label:‘金三胖‘, value:4},
    {label:‘白小飞‘, value:5},
    {label:‘路飞‘, value:6},
    {label:‘鸣人‘, value:7},
    {label:‘吐槽‘, value:8}
];

我只要告诉控件,选中项为[2,4,7],那么控件自动将value字段为2,4,7的项目放在右边的下拉列表,同理,操作控件时,values自动更新,这里特别注意value字段必须是String,int等类型,如果是Object等以内存地址做相等判断的,这个就不灵了,不过相信我,你会喜欢这种方式的。

好了,现在开始吧,建立一个Skinnable组件,命名为DDList:

import spark.components.supportClasses.SkinnableComponent;
    
public class DDList extends SkinnableComponent
{
    public function DDList()
    {
        super();
    }
}

然后熟练的建立一个对应的MXML外观,名称DDListSkin,主机主件指定为刚才的DDList:

<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx">
    <!-- host component -->
    <fx:Metadata>
        [HostComponent("DDList")]
    </fx:Metadata>
</s:Skin>

现在来考虑界面吧,我需要左右两个List下拉框,以及中间四个按钮,分别表示全选,选择选中项,取消选中项,全部取消四个功能,来吧,MXML写上:

<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        xmlns:mx="library://ns.adobe.com/flex/mx">
    <!-- host component -->
    <fx:Metadata>
        [HostComponent("DDList")]
    </fx:Metadata>
    
    <s:HGroup width="100%" height="100%" verticalAlign="middle">
        <s:List id="listLeft" width="50%" height="100%" allowMultipleSelection="true"/>
        <s:VGroup>
            <s:Button icon="@Embed(source=‘arrow-rightAll-16.png‘)" label="全选" 
                      enabled="{listLeft.dataProvider &amp;&amp; listLeft.dataProvider.length>0}" />
            <s:Button icon="@Embed(source=‘arrow-right-16.png‘)" label="添加" 
                      enabled="{listLeft.selectedItems.length>0}" />
            <s:Button icon="@Embed(source=‘arrow-left-16.png‘)" label="移除" 
                      enabled="{listRight.selectedItems.length>0}" />
            <s:Button icon="@Embed(source=‘arrow-leftAll-16.png‘)" label="清空" 
                      enabled="{listRight.dataProvider &amp;&amp; listRight.dataProvider.length>0}" />
        </s:VGroup>
        <s:List id="listRight" width="50%" height="100%" allowMultipleSelection="true"/>
    </s:HGroup>
    
</s:Skin>

这里的图标就不用说了吧,找了四个同名图标放在相同路径下。
好了,给组件披上外衣吧:

import spark.components.supportClasses.SkinnableComponent;
    
public class DDList extends SkinnableComponent
{
  public function DDList()
  {
    super();
    setStyle(‘skinClass‘, DDListSkin);
  }
}

现在可以测试一下了,看下图:

有点意思了吗?开始实现数据源的展示吧,给组件加上相关属性,labelField,labelFunction,valueField,values以及作为保存选择项列表的selectedItems:

import mx.collections.ArrayCollection;
    
import spark.components.supportClasses.SkinnableComponent;
    
public class DDList extends SkinnableComponent
{
  public function DDList()
  {
    super();
    setStyle(‘skinClass‘, DDListSkin);
  }
        
  [Bindable]public var labelField:String = "label";
  [Bindable]public var labelFunction:Function;
        
  [Bindable]internal var leftDataProvider:ArrayCollection;
  [Bindable]internal var rightDataProvider:ArrayCollection;
        
  private var _valueField:String = "value";
  [Bindable]
  public function get valueField():String
  {
    return _valueField;
  }
  public function set valueField(value:String):void
  {
    if(value=http://www.mamicode.com/=_valueField)
    {
      return;
    }
    _valueField = value;
  }
        
  private var _values:ArrayCollection = new ArrayCollection();
  [Bindable]
  public function get values():ArrayCollection
  {
    return _values;
  }
  public function set values(value:ArrayCollection):void
  {
    if(null==value || value=http://www.mamicode.com/=_values)
    {
      return;
    }
    _values = value;
  }
        
  private var _selectedItems:Vector.<Object> = new Vector.<Object>();
  [Bindable("selectedItemsChanged")]
  public function get selectedItems():Vector.<Object>
  {
    return _selectedItems;
  }
}

我们的核心就在于这个_selectedItems,用于保存当前选中项的数组,通过维护这个数组,来将数据展示给用户,也能将values反馈给程序
现在要来实现对这个数组的维护,当source,valueField,values变化时,我们都要根据values来更新_selectedItems数组,使它反映出最新的选中项列表,通过下面的代码来实现对项目的选中设置:

private function setItemState(item:Object, selected:Boolean):void
{
  var index:int = _selectedItems.indexOf(item);
  if(selected)
  {
    if(index<0)
    {
      _selectedItems.push(item);
    }
    if(item.hasOwnProperty(valueField) && !_values.contains(item[valueField]))
    {
      _values.source.push(item[valueField]);
    }
  }
  else
  {
    if(index>=0)
    {
      _selectedItems.splice(index, 1);
    }
    if(item.hasOwnProperty(valueField))
    {
      while(_values.contains(item[valueField]))
      {
        _values.source.splice(_values.getItemIndex(item[valueField]), 1);
      }
    }
  }
}

如果item选中,就调用setItemState(item, true),如果item被移除,就调用setItemState(item, false);里面会为我们维护好selectedItems和values。
有了这个方法,就好办多了,当设定values时,遍历values的每一个value,看是否在source中找到对应的item,找到了就setItemState(item, true),否则就是无效项,从values中删掉吧。

public class DDList extends SkinnableComponent
{
  public function DDList()
  {
    super();
    setStyle(‘skinClass‘, DDListSkin);
  }
        
  [Bindable]public var labelField:String = "label";
  [Bindable]public var labelFunction:Function;
        
  [Bindable]internal var leftDataProvider:ArrayCollection;
  [Bindable]internal var rightDataProvider:ArrayCollection;
        
  private var _valueField:String = "value";
  [Bindable]
  public function get valueField():String
  {
    return _valueField;
  }
  public function set valueField(value:String):void
  {
    if(value=http://www.mamicode.com/=_valueField)
    {
      return;
    }
    _valueField = value;
    validateValues();
  }
        
  private var _values:ArrayCollection = new ArrayCollection();
  [Bindable]
  public function get values():ArrayCollection
  {
    return _values;
  }
  public function set values(value:ArrayCollection):void
  {
    if(null==value || value=http://www.mamicode.com/=_values)
    {
      return;
    }
    _values = value;
    validateValues();
  }
        
  private var _selectedItems:Vector.<Object> = new Vector.<Object>();
  [Bindable("selectedItemsChanged")]
  public function get selectedItems():Vector.<Object>
  {
    return _selectedItems;
  }
        
  private var _source:Array;
  [Bindable]
  public function get source():Array
  {
    return _source;
  }
  public function set source(value:Array):void
  {
    if(value=http://www.mamicode.com/=_source)
    {
      return;
    }
    _source = value;
    validateValues();
  }
        
  private function validateValues(event:CollectionEvent=null):void
  {
    if(source)
    {
      _selectedItems = new Vector.<Object>();
                
      for each(var value:* in _values)
      {
        var b:Boolean = false;
        for each(var item:Object in source)
        {
          if(item && item.hasOwnProperty(valueField) && item[valueField]==value)
          {
            setItemState(item, true);
            b = true;
          }
        }
        if(!b)
        {
          _values.source.splice(_values.getItemIndex(value), 1);
        }
      }
      leftDataProvider.refresh();
      rightDataProvider.refresh();
      dispatchEvent(new FlexEvent("selectedItemsChanged", true));
    }
  }
        
  private function setItemState(item:Object, selected:Boolean):void
  {
    var index:int = _selectedItems.indexOf(item);
    if(selected)
    {
      if(index<0)
      {
        _selectedItems.push(item);
      }
      if(item.hasOwnProperty(valueField) && !_values.contains(item[valueField]))
      {
        _values.source.push(item[valueField]);
      }
    }
    else
    {
      if(index>=0)
      {
        _selectedItems.splice(index, 1);
      }
      if(item.hasOwnProperty(valueField))
      {
        while(_values.contains(item[valueField]))
        {
          _values.source.splice(_values.getItemIndex(item[valueField]), 1);
        }
      }
    }
  }

当source,values,valueField变化时,都需要及时维护好selectedItems和values,所以在他们的setter中,都加上了validateValues();,我们通过对左右两个dataProvider进行refresh来保证数据及时刷新,好了,左右两个dataProvider还没有设置filterFunction呢,来改写一下source的setter方法:

public function set source(value:Array):void
{
  if(value=http://www.mamicode.com/=_source)
  {
    return;
  }
  _source = value;
            
  leftDataProvider = new ArrayCollection(_source);
  leftDataProvider.filterFunction = leftFilterFunction;
            
  rightDataProvider = new ArrayCollection(_source);
  rightDataProvider.filterFunction = rightFilterFunction;
            
  validateValues();
}
        
private function leftFilterFunction(item:Object):Boolean
{
  return !rightFilterFunction(item);
}
        
private function rightFilterFunction(item:Object):Boolean
{
  return _selectedItems.indexOf(item)>=0;
}

看见了吗,leftDataProvider和rightDataProvider都是来自同一数据源source,但设置了不同的过滤函数,右边的只显示在selectedItems中存在的项,左边的正好相反!

嗯,好像有地方不对劲,对了,values一旦在外部发生变化,需要通知控件自动刷新,没问题,values加上事件监听:

public function set values(value:ArrayCollection):void
{
  if(null==value || value=http://www.mamicode.com/=_values)
  {
    return;
  }
  if(_values.hasEventListener(CollectionEvent.COLLECTION_CHANGE))
  {
    _values.removeEventListener(CollectionEvent.COLLECTION_CHANGE, validateValues);
  }
  _values = value;
  _values.addEventListener(CollectionEvent.COLLECTION_CHANGE, validateValues, false, 0 ,true);
            
  validateValues();
}

这样,一旦程序通过编程方式改动了values的内容,立刻会被控件监听到,自动执行validateValues函数,自动维护好selectedItems和values;
看样子就要成功了,还少了什么?嗯,组件的交互,点击四个按钮后也需要对selectedItem和values进行维护,没问题,先定义一个内部事件,用来向组件冒泡:

import flash.events.Event;
    
internal final class DDListEvent extends Event
{
  public static const ADD_ALL:String = "addAll";
  public static const ADD:String = "addItems";
  public static const REMOVE:String = "removeItems";
  public static const REMOVE_ALL:String = "removeAll";
        
  public function DDListEvent(type:String, items:Vector.<Object>=null, bubbles:Boolean=false, cancelable:Boolean=false)
  {
    _items = items;
    super(type, bubbles, cancelable);
  }
        
  private var _items:Vector.<Object>;
  public function get items():Vector.<Object>
  {
    return _items;
  }
        
  override public function clone():Event
  {
    return new DDListEvent(type, items, bubbles, cancelable);
  }
}

然后给四个按钮加上click事件处理吧:

protected function button1_clickHandler(event:MouseEvent):void
{
  event.stopPropagation();
  dispatchEvent(new DDListEvent(DDListEvent.ADD_ALL, null, true));
}
            
protected function button2_clickHandler(event:MouseEvent):void
{
  event.stopPropagation();
  dispatchEvent(new DDListEvent(DDListEvent.ADD, listLeft.selectedItems, true));
}
            
protected function button3_clickHandler(event:MouseEvent):void
{
  event.stopPropagation();
  dispatchEvent(new DDListEvent(DDListEvent.REMOVE, listRight.selectedItems, true));
}
            
protected function button4_clickHandler(event:MouseEvent):void
{
  event.stopPropagation();
  dispatchEvent(new DDListEvent(DDListEvent.REMOVE_ALL, null, true));
}

好了,给我们的组件添加事件监听,侦听DDListEvent吧:

public function DDList()
{
  super();
  setStyle(‘skinClass‘, DDListSkin);
            
  addEventListener(DDListEvent.ADD_ALL, changeHandler);
  addEventListener(DDListEvent.ADD, changeHandler);
  addEventListener(DDListEvent.REMOVE, changeHandler);
  addEventListener(DDListEvent.REMOVE_ALL, changeHandler);
}
        
private function changeHandler(event:DDListEvent):void
{
  event.stopPropagation();
            
  switch(event.type)
  {
    case DDListEvent.ADD_ALL:
      for each(var item:Object in source)
      {
        setItemState(item, true);
      }
      break;
    case DDListEvent.ADD:
      for each(var item1:Object in event.items)
      {
        setItemState(item1, true);
      }
      break;
    case DDListEvent.REMOVE:
      for each(var item2:Object in event.items)
      {
        setItemState(item2, false);
      }
      break;
    case DDListEvent.REMOVE_ALL:
      for each(var item3:Object in source)
      {
        setItemState(item3, false);
      }
      break;
  }
  leftDataProvider.refresh();
  rightDataProvider.refresh();
            
  dispatchEvent(new FlexEvent("selectedItemsChanged", true));
}

最后一步,在皮肤中给两个List绑定数据源:

<s:HGroup width="100%" height="100%" verticalAlign="middle">
        <s:List id="listLeft" width="50%" height="100%" allowMultipleSelection="true"
                labelField="{hostComponent.labelField}" labelFunction="{hostComponent.labelFunction}"
                dataProvider="{hostComponent.leftDataProvider}" />
        <s:VGroup>
            <s:Button icon="@Embed(source=‘arrow-rightAll-16.png‘)" label="全选" 
                      enabled="{listLeft.dataProvider &amp;&amp; listLeft.dataProvider.length>0}" 
                      click="button1_clickHandler(event)"/>
            <s:Button icon="@Embed(source=‘arrow-right-16.png‘)" label="添加" 
                      enabled="{listLeft.selectedItems.length>0}" 
                      click="button2_clickHandler(event)"/>
            <s:Button icon="@Embed(source=‘arrow-left-16.png‘)" label="移除" 
                      enabled="{listRight.selectedItems.length>0}" 
                      click="button3_clickHandler(event)"/>
            <s:Button icon="@Embed(source=‘arrow-leftAll-16.png‘)" label="清空" 
                      enabled="{listRight.dataProvider &amp;&amp; listRight.dataProvider.length>0}"
                      click="button4_clickHandler(event)"/>
        </s:VGroup>
        <s:List id="listRight" width="50%" height="100%" allowMultipleSelection="true"
                labelField="{hostComponent.labelField}" labelFunction="{hostComponent.labelFunction}"
                dataProvider="{hostComponent.rightDataProvider}"/>
    </s:HGroup>

大功告成,测试一下吧:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx" 
               xmlns:local="*">
    
    <fx:Style>
        global
        {
            fontFamily:"微软雅黑";
            fontSize:12px;
        }
    </fx:Style>
    
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            import mx.controls.Alert;
            import mx.utils.StringUtil;

            private var data:Array = [
                {label:‘王大锤‘, value:0},
                {label:‘李小虎‘, value:1},
                {label:‘舒克‘, value:2},
                {label:‘贝塔‘, value:3},
                {label:‘金三胖‘, value:4},
                {label:‘白小飞‘, value:5},
                {label:‘路飞‘, value:6},
                {label:‘鸣人‘, value:7},
                {label:‘吐槽‘, value:8}
            ];
            
            [Bindable]private var source:Array;
            [Bindable]private var values:ArrayCollection;
            
            private function   labelFunc(item:Object):String
            {
                return StringUtil.substitute("{0}-{1}", item.value,item.label);
            }
            
            protected function button1_clickHandler(event:MouseEvent):void
            {
                source = data.concat();
            }
            
            protected function button2_clickHandler(event:MouseEvent):void
            {
                values = new ArrayCollection([3,6,8,22]);
            }
            
            protected function button3_clickHandler(event:MouseEvent):void
            {
                Alert.show(values.toArray().join(‘,‘));
            }
            
            protected function button4_clickHandler(event:MouseEvent):void
            {
                values.addItem(7);
                values.addItem(19);
            }
        ]]>
    </fx:Script>
    
    <s:HGroup horizontalCenter="0" verticalCenter="0">
        <s:VGroup horizontalCenter="0" verticalCenter="0">
            <s:Button label="加载数据" click="button1_clickHandler(event)" />
            <s:Button label="初始化" click="button2_clickHandler(event)" />
            <s:Button label="设置values" click="button4_clickHandler(event)" />
            <s:Button label="查看values" click="button3_clickHandler(event)"/>
            <local:DDList width="400" height="300" labelFunction="labelFunc" source="{source}" values="@{values}" />
        </s:VGroup>
        
    </s:HGroup>
    
</s:Application>

此即本文最开始的Demo(呆毛)。