自动布局就是对数据元素中的节点进行定位,使节点分散在到一个比较好看的显示效果,自动布局的实质就是一种布局的算法的实现。比如TWaver默认提供圆形布局、交错布局、树形布局和弹簧布局,此外标签云、bus总线等也属于布局的应用,但需求总是变化无穷,数据结构不同,要求的呈现效果也不同,自动布局的定制在所难免,本文将介绍一种比较少见的布局效果。
默认的自动布局
首先我们来看看默认的自动布局的使用
var autoLayouter:AutoLayouter = new AutoLayouter(network); autoLayouter.doLayout(Consts.LAYOUT_SYMMETRY);
代码分两步,首先定义一个自动布局器关联了network组件,然后调用自动布局,进行交错布局。
可以看出,TWaver中自动布局都是通过AutoLayouter类来实现,而布局类型包括:Consts.LAYOUT_***
布局的效果如下,实际程序中可以看到布局过程的动画效果:
自动布局的实质
自动布局是自动对数据容器中所有网元进行重新定位,使节点分散开到好的显示效果,其实质是一种布局算法的实现,所以要定制自动布局就是要实现一种布局算法,以确定所有网元节点的位置。
TWaver并没有自动布局的接口,通常一个自动布局器会实现一个叫“doLayout()”的函数,以保持命名上的统一。
自动布局“接口函数”
CustomLayouter#public function getLayoutResult():Dictionary
CustomLayouter#public function doLayout():void
getLayoutResult
getLayoutResult()函数是布局算法的核心,其返回的是节点位置的计算结果,是一个Dictionary类型,< 键, 值>类型是。
doLayout
doLayout()函数包含两步,先调用getLayoutResult()得到布局位置信息,然后将这些信息,设置到网元节点上,类似下面的代码:
public function doLayout(box:ElementBox, root:IData = null):void{ var result:Dictionary = getLayoutResult(box, root); for(var element:* in result){ var location:Point = result[element]; (element as Node).location = location; } }
从上面的示例代码可以看出,实际使用中会传入一些参数,这些根据实际情况而定。此外这里也可以使用动画的平移效果,起一个Timer,动画的将节点从原始位置移动到新的位置。
开始定制
下面开始布局算法,这里我们将布出下面的效果,整体是一个树结构,从左到右四层,最后一层使用Bus布局:
因为只是一个示例,布局实现不可能尽善尽美,所以只提供些实现的思路,算是抛砖引玉,作为参考:
整理数据结构
首先理清数据的结构,本例是一个树状布局,具有固定的四层结构,故本例将每一层的节点找出来,如第一层一个节点,第二层四个节点,第三层十二个节点,第四层可能右八十多个节点。
本例中使用了dataBox的广度遍历,每遍历完一层,就对这层上的节点作布局,这样一次遍历完,布局也做完,也就是说“整理数据结构”和“分别对每层布局”是同时进行的,但这并不是很好的示范,通常还是老老实实先整理数据的结构,甚至做一些模型的封装了表示这些模型,然后再做布局。
使用dataBox的广度遍历,每遍历完一层,就对这层上的节点作布局:
public function getLayoutResult(box:ElementBox, root:IData = null):Dictionary{ var result:Dictionary = new Dictionary(); function _calculateLocations(queue:Array, layer:int):void{ var sum:int = queue.length; if(sum == 0){ return; } queue.forEach(function(node:IData, index:int, a:*):void{ var subIndex:int = index; var parentPoint:Point = null; var subSum:int = sum; if(node.parent){ subIndex = node.parent.children.getItemIndex(node); parentPoint = result[node.parent]; subSum = node.parent.childrenCount; } var location:Point = calculateLocation(node, layer, index , sum, subIndex, subSum, parentPoint); result[node] = location; }); }; var parent:IData = null; var layer:int = 0; var queue:Array = new Array(); box.forEachByBreadthFirst(function(node:IData):void{ if(!(node is Node)){ return; } if(node.parent != parent){ parent = node.parent; var l:int = 0; var p:IData = node.parent; while(p){ l++; p = p.parent; } if(l != layer){ _calculateLocations(queue, layer); layer++; queue = new Array(); } } queue.push(node); }, root); _calculateLocations(queue, layer); return result; }
分别对每层布局
有了上面这些信息,就很方便对各层作不同的布局:第一层到第四层,从左到右排列,每层节点从上到下排列,最后一层水平排列,本例中实现布局的函数是calculateLocation(…),传入的参数包含了该节点布局相关的信息(所在层,在同层的位置,该层总共有多少节点,在其父节点的位置,其父节点所有的孩子数,父节点的布局位置)
public function calculateLocation(node:IData, layer:int, index:int, sum:int, subIndex:int, subSum:int, parentPoint:Point = null):Point
var maxHeight:int = 700; var startX:int = 10; var startY:int = 10; public function calculateLocation(node:IData, layer:int, index:int, sum:int, subIndex:int, subSum:int, parentPoint:Point = null):Point{ var x:int = startX; var y:int = startY; var hGap:int = getHorizontalGap(layer); if(layer < 3){ x = parentPoint ? parentPoint.x + hGap : startX; y = startY + getVerticalGap(layer, sum) * (index + 0.5); }else if(layer == 3 && parentPoint){ x = parentPoint.x + hGap + subIndex/2 * 50; y = parentPoint.y + ((subIndex % 2 == 0 ) ? 15 : -15); } return new Point(x, y); } public function getHorizontalGap(layer:int):int{ return layer == 3 ? 90 : 150; } public function getVerticalGap(layer:Number, sum:Number):Number{ return Math.max(maxHeight/(sum + 0.5), 75); }
连线样式的定制
本例中连线样式的配置也是关键,对于第四层Bus布局的呈现主要通过正交连线样式来体现,所以在数据添加时,对第四层的连线设置如下:
var link:Link = new Link(parent, node); link.setStyle(Styles.LINK_TYPE, Consts.LINK_TYPE_HORIZONTAL_VERTICAL); link.setStyle(Styles.LINK_CORNER, Consts.LINK_CORNER_NONE); box.add(link);
查看完整源代码,请看这里