今天我们来看一下如何在Tree控件中动态增删数据。
此代码将会在程序启动的最后创建有5项数据的数组,并用Tree展现。效果:
运行,点击按钮,并没有效果。
结果竟然是有效!这里无需担心
verticalScrollPosition进行了+1,再减1,在下一帧还是使用原来的值的问题,下面再讲原因。
运行,点击按钮,结果:
Tree会监听dataProvider(已根据实际传入的dataProvider进行转换)的
CollectionEvent.COLLECTION_CHANGE事件,当dataProvider派发此事件(如d.addItem(n); ),监听后Flex会调用Tree.invalidateDisplayList方法,而在updateDisplayList方法会调用makeRowsAndColumns方法,将item renderer进行刷新。
invalidateList
很遗憾,没有任何效果!
很容易我们就可以发现是
itemsSizeChanged
变量在作怪,由于考虑到性能问题,不能每次调用invalidateDisplayList,都进行item renderer的刷新,所以需要itemsSizeChanged变量检测dataProvider中的item长度是否发生改变,如果没有改变,则不会进行update.
我们回到刚才不优雅的程序3,ListBase的源码,无论怎么设置,都会调用上面的两行代码,所以有效。
运行,点击按钮,一切都运行得很好:
我们先初始化一棵简单地树,代码如下:
// 程序1
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx=" http://www.adobe.com/2006/mxml" minWidth="955" minHeight="600" creationComplete="application1_creationCompleteHandler(event)"
layout="vertical">
<mx:Script> <![CDATA[ import mx.collections.ArrayCollection; import mx.events.FlexEvent; private var d:ArrayCollection; protected function application1_creationCompleteHandler(event:FlexEvent):void { d = new ArrayCollection(); for(var i:int = 0;i < 5;++i) { var n:Object = new Object(); n.label = "节点" + (i + 1); d.addItem(n); } t.dataProvider = d; } ]]> </mx:Script> <mx:Tree id="t" width="200" height="200"/> </mx:Application> |
现在我们假设,通过点击一个按钮,往第一个节点下添加5个子节点。
增加子节点数据
添加子节点的代码如下,直接设置第一个节点的children值为一个新的数组。
// 程序2
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
}
|
猜想原因应该是,虽然控件的数据刷新了,但控件并没有把新数据刷新到界面上,也就是控件中的可视化子控件(item renderer)并不知道数据改变了。
利用Tree的滚动条
在进行进一步分析前,我们先看一个有趣的现象,
我们做一点小调整:将Tree控件的高度从200改为100,这样,初始化的Tree就会出现滚动条:
然后我们再点击按钮,把新数据添加到第一个节点“节点1”下,没有任何变化。但我们拖动滚动条:
可以看到“节点1”已经变成了一个非叶子,展开节点,一切正常。
那拖动滚动条时究竟发生了什么呢?
我们知道Tree的每一行都是用item renderer进行渲染的,而且item renderer是使用了缓冲池的原理,也就是说item renderer不是固定用于显示某一行,而是根据需要显示的数据,同一个item renderer可能会渲染不同的数据,例如拖动滚动条的时候。当将滚动条往下拖动时,上面的部分节点会“隐藏”,这些item renderer将会被用于渲染下面“新出现”的节点,当把滚动条拖回到原来的位置时,这些item renderer重新渲染被“隐藏“的节点。由于拖动滚动条时,因为第一个节点会先“隐藏”再“显示”,所以第一个节点会被强制刷新,也就是第一个节点的item renderer在刷新时会显示最新的数据,所以会显示为一个非叶子节点。
从代码层面看,Flex是从Tree.scrollHandler到List.scrollVertically,再调用makeRowsAndColumns进行刷新。
那如果自动修改
verticalScrollPosition是不是就可以达到效果呢?我们试一试这个看起来不怎么优雅的方式:
// 程序3
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
t.verticalScrollPosition += 1;
t.verticalScrollPosition -= 1;
}
|
增加第一层节点数据会如何?
如果我们修改一下我们的任务,把添加子节点改为添加第一层节点。我们将上述代码稍微改动一下:
// 程序4
protected function button1_clickHandler(event:MouseEvent):void
{
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "节点" + (i + 6);
d.addItem(n);
}
}
|
为什么往第一层添加数据就会马上看到效果呢?而往第一层节点添加子节点就没有效果呢?
collectionChange事件
我们追溯一下Tree源码:
// Tree.commitProperties
super.dataProvider = wrappedCollection = (_dataDescriptor is ITreeDataDescriptor2) ?
ITreeDataDescriptor2(_dataDescriptor).getHierarchicalCollectionAdaptor(
tmpCollection != null ? tmpCollection : _rootModel,
itemToUID,
_openItems) :
new HierarchicalCollectionView(
tmpCollection != null ? tmpCollection : _rootModel,
_dataDescriptor,
itemToUID,
_openItems);
// not really a default handler, but we need to be later than the wrapper
wrappedCollection.addEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false,
EventPriority.DEFAULT_HANDLER, true);
|
那为什么添加子节点时界面没有刷新呢?
通过研究ArrayCollection源码发现,
CollectionEvent.COLLECTION_CHANGE只在ArrayCollection对象进行add/remove操作时才会被派发,而对于ArrayCollection对象中的每个item,也就是第一层节点进行add/remove操作,尽管对于item也会派发这个事件,但对于ArrayCollection则不会派发这个事件。也就是说ArrayCollection的CollectionEvent.COLLECTION_CHANGE事件只对第一层数据进行增删时才会出现,即浅操作,而对于深操作,即对ArrayCollection的第二层,乃至第n层进行增删,事件都不会上升到第一层,只会停留在进行增删的那一层。
所以,对第一层节点进行增删,Tree是可以监听到的,而对于增删子节点则无能为力。
那我们手动派发CollectionEvent.COLLECTION_CHANGE事件会怎么样呢?
// 程序5
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
(t.dataProvider as IEventDispatcher).dispatchEvent(new CollectionEvent(
CollectionEvent.COLLECTION_CHANGE, false, false, CollectionEventKind.ADD));
}
|
那我们手动派发CollectionEvent.COLLECTION_CHANGE事件会怎么样呢?
// 程序5
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
(t.dataProvider as IEventDispatcher).dispatchEvent(new CollectionEvent(
CollectionEvent.COLLECTION_CHANGE, false, false, CollectionEventKind.ADD));
}
|
运行,有效!只是这种代码看起来不那么优雅。
那我们手动派发CollectionEvent.COLLECTION_CHANGE事件会怎么样呢?
// 程序5
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
(t.dataProvider as IEventDispatcher).dispatchEvent(new CollectionEvent(
CollectionEvent.COLLECTION_CHANGE, false, false, CollectionEventKind.ADD));
}
|
invalidateList
下面我们看一下Flex是怎么处理
CollectionEvent.COLLECTION_CHANGE事件的:
由上面分析可知,CollectionEvent.COLLECTION_CHANGE的事件监听器会调用Tree.invalidateDisplayList方法,而最终导致item renderer的刷新。那改成这样又如何呢:
// 程序6
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
t.invalidateDisplayList();
}
|
那就证明了只是简单地调用
invalidateDisplayList并不能刷新item renderer。
我们回头再仔细研究一下CollectionEvent.COLLECTION_CHANGE的事件监听器(collectionChangeHandler)的处理逻辑。
// ListBase
protected function collectionChangeHandler(event:Event):void
{
// 其它代码
itemsSizeChanged = true;
invalidateDisplayList();
}
protected override function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if (oldUnscaledWidth == unscaledWidth &&
oldUnscaledHeight == unscaledHeight &&
!
itemsSizeChanged && !bSelectionChanged &&
!scrollAreaChanged)
{
return;
}
// 其它代码
}
|
那很明显,我们需要下面一段代码:
itemsSizeChanged = true;
invalidateDisplayList();
|
// ListBase
override public function set verticalScrollPolicy(value:String):void
{
super.verticalScrollPolicy = value;
itemsSizeChanged = true;
invalidateDisplayList();
}
|
当然,这两行代码不用自己再度封装,Flex已经实现了,其实ListBase.invalidateList()就是这样的!
// 程序7
protected function button1_clickHandler(event:MouseEvent):void
{
var p:Object = d.getItemAt(0);
p.children = new ArrayCollection();
for(var i:int = 0;i < 5;++i)
{
var n:Object = new Object();
n.label = "子节点" + (i + 1);
p.children.addItem(n);
}
t.invalidateList();
}
|
总结
通过本文,我们可以了解动态添加树空间的数据,可以有几种方式:
- verticalScrollPosition +-
- (t.dataProvider as IEventDispatcher).dispatchEvent( new CollectionEvent(CollectionEvent.COLLECTION_CHANGE, false, false, CollectionEventKind.ADD));
- invalidateList
并理解invalidateList方法的作用,以及Tree(或者List)的dataProvider的运行机制。