Flex组件_Tree树控件_动态图标树

Flex的Tree控件是一个比较复杂比较重量级的控件,下面是一棵简单的Tree,只有一个非叶子节点和一个叶子节点。
 
图中并没有使用自定义的样式,Tree缺省的样式如下,可以看到folder和leaf的icon都是Embed的资源。
// C:\Program Files\Adobe\Adobe Flash Builder 4.7 (64 Bit)\sdks\3.6.0\frameworks\projects\framework\defaults.css
Tree
{
 defaultLeafIcon: Embed(source="Assets.swf",symbol="TreeNodeIcon");
 disclosureClosedIcon: Embed(source="Assets.swf",symbol="TreeDisclosureClosed");
 disclosureOpenIcon: Embed(source="Assets.swf",symbol="TreeDisclosureOpen");
 folderClosedIcon: Embed(source="Assets.swf",symbol="TreeFolderClosed");
 folderOpenIcon: Embed(source="Assets.swf",symbol="TreeFolderOpen");
 paddingLeft: 2;
 paddingRight: 0;
 verticalAlign: "middle";
}
但通常情况下,我们需要一棵可以动态修改这些icon的树,icon可能是一个URL,来自于后台数据,而不是一个预先嵌入到Flash中的资源。
下面就来看一下怎么实现将Tree的这些icon的来源改为URL。

Tree的内部结构
        Tree是继承自List,Tree中的一项(非叶子/叶子)相当于List中的一行,也就是每一项都是一个item renderer,Tree的缺省item renderer是TreeItemRenderer, TreeItemRenderer的子组件有三个:
  1. label,显示的文本,如本文第一张图中的节点1。
  2. icon,如果是非叶子,则默认是一个文件夹的图标,如果是叶子就是一个文件图标。
  3. disclosureIcon,如果是非叶子才显示,就是最前面用于展开/收缩的+/-按钮。
         这里我们只关注叶子节点icon,其它类型的icon和label、 disclosureIcon 我们将忽略说明。
         
         通过查看源码,我们可以发现,icon的初始化是在commitProperties中:
if (_listData.icon)
{
    var iconClass:Class = _listData.icon;
    icon = new iconClass();
    addChild(DisplayObject(icon)); 
}
这里的iconClass是来自于listData( Tree.itemToIcon返回 ),缺省情况下对应于dataProvider中每一个对象中的icon属性。

直接修改dataProvider的icon属性
Tree.itemToIcon方法是返回一个对应于每一个数据项item(dataProvider中的每一项)的icon的Class对象。
如果把dataProvider改为 [{icon: "assets/5.png"}],会实现所需的功能吗?
不会:
出现这个error的原因是,itemToIcon返回的是一个Class,通常的用法是返回一个Embed的资源,或者是一个自定义的可视化组件类。而如果dataProvider中的icon属性是一个URL字符串,itemToIcon将调用
else if (icon is String)
{
    iconClass = Class(systemManager.getDefinitionByName(String(icon)));
    if (iconClass)
      return iconClass;
    return document[icon]; 
}
然后在   return document[icon] 这一行就会出错,因为document中并没有icon这个属性(document中没有 "assets/5.png"属性 )。

修改子组件icon
上面的做法表明,只是简单把dataProvider修改是会报运行时错误的。那就只能干涉图标的创建过程了。
首先,dataProvider中必须存储URL,如果使用icon属性存储, [{icon: "assets/5.png"}],那运行时就会报上面的1069错误。我们需要先把这个错误处理掉:
override public function itemToIcon(item:Object):Class 
{
   // 这里可以添加自己的判断逻辑 ,这里简单的返回null.
   return null;
}
上面提到,子组件 icon的初始化是在commitProperties中进行,那修改icon的初始化可行吗?我们来试一下:
public class DynamicIconTreeItemRenderer extends TreeItemRenderer
{
override protected function commitProperties():void
  {
   super.commitProperties();
   var t:String = data.icon;
   icon = new Image();
   var img:Image = icon as Image;
   img.source = t;
   addChild(DisplayObject(icon));
  }
}
结果如下:
并没有出现我们期望的结果,icon并没有显示出来。

子组件没有显示,通常的原因有几个:
  1. 子组件其实没有被创建。
  2. 子组件没有添加到显示列表。
  3. 子组件并遮挡。
  4. 子组件width/height为0.

和一般的Flex组件一样,子组件的大小最终是有父组件确定(在updateDisplayList中),我们看一下icon的父亲组件TreeItemRenderer的 updateDisplayList:
if (icon)
  {
   icon.x = startx;
   startx = icon.x + icon.measuredWidth;
   icon.setActualSize(icon.measuredWidth, icon.measuredHeight);
  }
可见,icon的大小就是 measuredWidth/ measuredHeight,而 measuredWidth/ measuredHeight是在measure中确定的。
再来看Image的measure方法(在父类SWFLoader中)
override protected function measure():void
    {
        super.measure();
        if (isContentLoaded)
        {
            var oldScaleX:Number = contentHolder.scaleX;
            var oldScaleY:Number = contentHolder.scaleY;
            contentHolder.scaleX = 1.0;
            contentHolder.scaleY = 1.0;
            measuredWidth = contentHolderWidth;
            measuredHeight = contentHolderHeight;
            contentHolder.scaleX = oldScaleX;
            contentHolder.scaleY = oldScaleY;
        }
        else
        {
            if (!_source || _source == "")
            {
                measuredWidth = 0;
                measuredHeight = 0;
            }
        }
    }
可见,在Image的内容加载完毕前, measuredWidth/ measuredHeight均为0,只有在内容加载完毕后 measuredWidth/ measuredHeight才会设置为正确的值。

       按照Flex的机制,当子组件的大小改变后(也就是validateSize方法中,measureSizes返回true),会自动调用父组件的invalidateSize和invalidateDisplayList方法,从而保证子组件的大小发生改变后,所有的父组件都会进行刷新。那为什么上面的做法还是行不通呢?
       我们先了解一下Tree的内部流程:
上图展现的是Tree内部大概的流程(实际上的流程比这个复杂很多),可以看到当icon加载完毕后,最终会调用Tree.invalidateDisplayList,而此方法最终竟然会再次调用item renderer的invalidateProperties!
会出现不停创建新的icon( DynamicIconTreeItemRenderer  . commitProperties )的情况,所以,icon就永远都不会出现,icon刚要显示就被删除了,被新的icon替代了。
而且最致命的问题在于,这个过程不会停止,因为上图中的循环并没有退出条件。

使用子组件icon的问题
明显可见,上述的循环的症结在于DynamicIconTreeItemRenderer.commitProperties方法中每次都创建一个新的Image,导致最终会调用Tree.invalidateDisplayList方法,导致再次调用 DynamicIconTreeItemRenderer.commitProperties,那如果新的Image只创建一次是不是就解决问题呢?
public class DynamicIconTreeItemRenderer extends TreeItemRenderer
 {  
  protected var dataChanged:Boolean;
  override public function set data(value:Object):void
  {
   if (data != value)
   {
    dataChanged = true;
   }
   super.data = value;
  }
  override protected function commitProperties():void
  {
   super.commitProperties();
   if (!dataChanged)
   {
    return;
   }
   dataChanged = false;
   var o:DynamicIconTree = owner as DynamicIconTree;
   var t:String = data.icon;
   icon = new Image();
   var img:Image = icon as Image;
   img.source = t;
   addChild(DisplayObject(icon));
  }
}
结果一模一样,没有任何改变!
上面的代码先在set data方法中记录data是否发生改变,如果没有改变,在 commitProperties则不会创建新icon,如果有改变,才会创建新icon。通过调试我们可以容易的发现,现在不会有死循环的出现了。
但,图标还是没有显示!

查看TreeItemRenderer.commitProperties方法,我们发现有下面几行代码:
if (icon)
  {
   removeChild(DisplayObject(icon));
   icon = null;
  }
也就是说,每次的commitProperties调用,都会先删除旧的icon,再创建新的icon。
DynamicIconTreeItemRenderer中,由于没有再创建新icon,于是图标就永远都不会出现。

另立门户
上面的尝试已经很接近我们的目标了,只要在commitProperties中不预先删除icon,但删除操作又是在父类中,如果完全抛弃不用父类的commitProperties也不行,因为里面还有其它有用的代码。
其实我们可以完全不用内置的icon子组件,自己另外创建一个子组件用于展现动态图标,而内置的icon则隐藏起来。
我们先看看下面的代码:
public class DynamicIconTreeItemRenderer extends TreeItemRenderer
{
  protected var dynamicIcon:Image;
  override protected function commitProperties():void
  {
   super.commitProperties();
   if (!dynamicIcon)
   {
    dynamicIcon = new Image();
    addChild(dynamicIcon);
   }
   var t:String = data.icon;
   dynamicIcon.source = t;
  }
}
这里和 修改子组件icon的做法很相似,只是把icon换成了 dynamicIcon。注意到这里我们并没有使用 dataChanged变量用于标记data是否修改过,所以每次调用commitProperties都会创建一个新的 dynamicIcon,那是不是也会出现死循环呢?
调试证明并没有出现,这又是为什么呢?
我们回到死循环那个流程图,
当item renderer的Image子组件加载完毕后,由于Image的大小改变了(由0变成大于0的值),所以父组件item renderer也会被刷新,于是关键地方来了:
缺省的TreeItemRenderer的measure方法是检查内置子组件的icon的大小,并计算本身的measuredWidth/measuredHeight值,所以 TreeItemRenderer也会发生变化,于是最终会造成Tree被刷新,造成新的循环。
dynamicIcon的做法中,并没有修改 DynamicIconTreeItemRenderer的measure方法,于是 DynamicIconTreeItemRenderer  的大小并不会改变,就不会造成Tree的刷新,循环就会终结。

当然,最正统的做法, DynamicIconTreeItemRenderer.measure方法需要重写,那么循环就会出现。这时就需要使用 dataChanged变量。
最后,当然别忘了重写 DynamicIconTreeItemRenderer.updateDisplayList方法,设置 dynamicIcon的大小( dynamicIcon.setActualSize),并调整 dynamicIcon的位置。

结语
Flex的Tree组件是其中一个复杂度很高的组件,渲染过程中会出现经历几个帧的情况,也会出现主动调用validateClient的情况,导致commitProperites、measure、updateDisplayList方法被多次调用,在自定义组件过程中需要留意:
  1. 子组件被多次创建。
  2. 子组件加载完毕后重新刷新父组件。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值