[转] Flash文本引擎, 第三部分: 布局
Flash Player 10的FTE是一个强大的字体渲染库, 但这也仅是将字符渲染到指定宽度的TextLine内. 想想TextField的全部功能, 你就会意识到, 渲染字形, 只是所有工作的一小部分(当然也很重要).
文本布局
回想第一篇文章, 说道, 主要的 "Controller" 是 TextBlock 类, TextBlock是TextLine的工厂. 你提供 ContentElement 对象给TextBlock, 它就会通过计算然后创建一个你所需要的TextLine对象来显示内容. TextBlock的渲染算法是作用于段落(paragraph)级别的. 每个段落一个TextBlock, 或是每个TextBlock一个段落. 例如在FTE中, 段落应表示为一个单独的TextBlock, 这个TextBlock包含了多个定义格式的ContentElement.
优秀的布局引擎范式
如果你熟悉HTML样式, 你肯定知道HTML有两个不同的布局范式: block layout and inline formatting.
Block layout影响整个文本块, padding(填充), indentation(缩进), 和margins(边距)这些样式会影响 block 级的布局. Inline formatting 是在block范围内渲染字符, 比如 color(颜色), size(大小), posture(位置), weight(粗细), justification(对齐)等等.
TextBlock的算法照顾了inline formatting, 因为inline formatting影响到TextLine之间的字符流, 剩下来你可以随意用什么Block格式了, 例如, 当你调用TextBlock的createTextLine方法时, 你可以指定TextLine的宽度. 这个简单的选项能让我们用一些花哨点的数学方法来实现块级的布局的许多特性.
布局算法
我们想用有点通用的方法来完成各种各样的布局. 一切都从最简单的开始, 先看单段布局, 然后块布局, 再到复杂的多TextBlock和多列(就象报纸那样) 的布局.
我已经演示了最简单的布局方法: 循环渲染所有行, 直到TextBlock.createTextLine返回null为止. 简单直接, 你可以应用你想用的任何块样式.
var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); }
package { import flash.display.Sprite; import flash.text.engine.*; [SWF(width="200", height="80")] public class FTEDemo9 extends Sprite { public function FTEDemo9() { super(); var e1:TextElement = new TextElement('"Saying what we think gives us a wider conversational range than saying what we know."', new ElementFormat(null, 16)); var e2:TextElement = new TextElement('\n- Cullen Hightower', new ElementFormat(null, 16, 0xCC3300)); var block:TextBlock = new TextBlock(new GroupElement(new <ContentElement>[e1, e2], new ElementFormat())); block.textJustifier = new SpaceJustifier("en", LineJustification.ALL_BUT_LAST, true); var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); } } } }
虽然上面的demo只不过像一个TextField, 但我已经完成了两个任务: 我已经按我的需要用TextBlock创建了每一个TextLine, 我用 y 变量作计数完成了对TextLine的最原始的布局.
缩进:
缩进非常简单, 将第一行的width变小的一点, 变小的尺寸用x右移作为补偿.
var y:Number = 0; var line:TextLine = block.createTextLine(null, 185); line.x = 15; while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); }
package { import flash.display.Sprite; import flash.text.engine.*; [SWF(width="200", height="80")] public class FTEDemo10 extends Sprite { public function FTEDemo10() { super(); var e1:TextElement = new TextElement('"Saying what we think gives us a wider conversational range than saying what we know."', new ElementFormat(null, 16)); var e2:TextElement = new TextElement('\n- Cullen Hightower', new ElementFormat(null, 16, 0xCC3300)); var block:TextBlock = new TextBlock(new GroupElement(new <ContentElement>[e1, e2], new ElementFormat())); block.textJustifier = new SpaceJustifier("en", LineJustification.ALL_BUT_LAST, true); var y:Number = 0; var line:TextLine = block.createTextLine(null, 185); line.x = 15; while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); } } } }
对齐:
居中对齐:
var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; line.x = (200 - line.width) * 0.5; y += line.descent; line = block.createTextLine(line, 200); }
package { import flash.display.Sprite; import flash.text.engine.*; [SWF(width="200", height="200")] public class FTEDemo11 extends Sprite { public function FTEDemo11() { super(); var e1:TextElement = new TextElement('"They\'ve just announced a Justin Bieber bio pic being shot in 3D. Wasn\'t that one of the signs of the "end times"?"', new ElementFormat(null, 20)); var e2:TextElement = new TextElement('\n- Rainn Wilson', new ElementFormat(null, 20, 0xCC3300)); var block:TextBlock = new TextBlock(new GroupElement(new <ContentElement>[e1, e2], new ElementFormat())); var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; line.x = (200 - line.width) * 0.5; y += line.descent; line = block.createTextLine(line, 200); } } } }
右对齐是一样的, 只是无需要乘以0.5了
package { import flash.display.Sprite; import flash.text.engine.*; [SWF(width="200", height="95")] public class FTEDemo12 extends Sprite { public function FTEDemo12() { super(); var e1:TextElement = new TextElement('"They should make a \'baby\' Baby Bjorne for babies to carry other babies."', new ElementFormat(null, 20)); var e2:TextElement = new TextElement('\n- Rainn Wilson', new ElementFormat(null, 20, 0xCC3300)); var block:TextBlock = new TextBlock(new GroupElement(new <ContentElement>[e1, e2], new ElementFormat())); var y:Number = 0; var line:TextLine = block.createTextLine(null, 200); while(line) { addChild(line); y += line.ascent; line.y = y; line.x = 200 - line.width; y += line.descent; line = block.createTextLine(line, 200); } } } }
看到没? 标准的布局实战! 最终, 我们用最常见数学实现了组件的布局. 你可能会说: "嗯! 缩进和对齐太简单了, 它们只需要计算x就行了". 没错!就很简单, 要知道其它的一些块格式, 比如 padding(填充), margin(边距), 和line spacing(行间距) 等等, 也都只是计算x,y 并且根据条件在循环内应用.
多TextBlock的布局
OK, 我们已经知道怎么在单个TextBlock内布局了, 重用上面的代码, 做多个TextBlock的布局就是小菜.
var blocks:Vector.<TextBlock> = new <TextBlock>[block1, block2]; var y:Number = 0; for(var i:int = 0; i < blocks.length; ++i) { y = layoutBlock(blocks[i], y); y += 5; } // Returns the aggregate y after this layout operation function layoutBlock(block:TextBlock, y:Number):Number { var line:TextLine = block.createTextLine(null, 185); line.x = 15; while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); } return y; }
package { import flash.display.Sprite; import flash.text.engine.*; [SWF(width="200", height="180")] public class FTEDemo13 extends Sprite { public function FTEDemo13() { var e1:TextElement = new TextElement('"Breaking Bad has inspired me to learn more about chemistry."', new ElementFormat(null, 20)); var e2:TextElement = new TextElement('\n- Rainn Wilson', new ElementFormat(null, 20, 0xCC3300)); var block1:TextBlock = new TextBlock(new GroupElement(new <ContentElement>[e1, e2], new ElementFormat())); var e3:TextElement = new TextElement('"\'Mad Men\' has inspired me to learn more about debauchery and self-loathing."', new ElementFormat(null, 20)); var e4:TextElement = new TextElement('\n- Rainn Wilson', new ElementFormat(null, 20, 0xCC3300)); var block2:TextBlock = new TextBlock(new GroupElement(new <ContentElement>[e3, e4], new ElementFormat())); var blocks:Vector.<TextBlock> = new <TextBlock>[block1, block2]; var y:Number = 0; for(var i:int = 0; i < blocks.length; ++i) { y = layoutBlock(blocks[i], y); y += 5; } } // Returns the aggregate y after this layout operation private function layoutBlock(block:TextBlock, y:Number):Number { var line:TextLine = block.createTextLine(null, 185); line.x = 15; while(line) { addChild(line); y += line.ascent; line.y = y; y += line.descent; line = block.createTextLine(line, 200); } return y; } } }
多容器布局
啊! 让我们进入到好东东, 多容器布局真的很cool, 因为它可以让文本从一个DisplayObjectContainer overflow到另一个DisplayObjectContainer. 还可以让我们实现列的布局.
总的想法是让尽可能多的TextLine放入到DisplayObjectContainer (DOC). 当碰到边界后, 就切换到下一个DOC. 我们只需将之前的方法略做修改就可以达到这个效果. 布局方法需要返回适合在DOC内的最后一个TextLine, 这样, 我们就可以重新进入布局, 并且是接着停止的那个TextBlock开始.
var line:TextLine = layoutBlock(block, null, container1); if(line) line = layoutBlock(block, line, container2); // Returns the last line rendered out of the TextBlock function layoutBlock(block:TextBlock, previousLine:TextLine, container:DisplayObjectContainer):TextLine { var line:TextLine = block.createTextLine(previousLine, container.width); var y:Number = 0; while(line) { container.addChild(line); y += line.ascent; line.y = y; y += line.descent; //If we reached the height boundary, return the last line that fit. if(y + line.height > container.height) return line; line = block.createTextLine(line, container.width); } return line; }
package { import flash.display.*; import flash.text.engine.*; [SWF(width="600", height="100")] public class FTEDemo14 extends Sprite { [Embed(source="assets/noreppachromus_small.jpg")] private var butterfly:Class; public function FTEDemo14() { super(); var str:String = 'Noreppa is a genus of neotropical ' + 'charaxine butterflies in the family ' + 'Nymphalidae, native to Colombia, Bolivia, ' + 'Venezuela, Peru and Argentina. It is a ' + 'monotypic genus. The single species is ' + 'Noreppa chromus (Guérin-Ménéville, 1844).'; var container1:Sprite = new Sprite(); container1.graphics.beginFill(0x00, 0.1); container1.graphics.drawRect(0, 0, 195, 100); addChild(container1); var container2:Sprite = new Sprite(); container2.graphics.beginFill(0x00, 0.1); container2.graphics.drawRect(0, 0, 195, 100); addChild(container2); container2.x = 205; var container3:Sprite = new Sprite(); container3.graphics.beginFill(0x00, 0.1); container3.graphics.drawRect(0, 0, 133, 100); addChild(container3); container3.x = 410; var block:TextBlock = new TextBlock(new TextElement(str, new ElementFormat(null, 18))); block.textJustifier = new SpaceJustifier("en", LineJustification.ALL_BUT_LAST, true); var line:TextLine = layoutBlock(block, null, container1); if(line) line = layoutBlock(block, line, container2); container3.addChild(new butterfly()); } // Returns the last line rendered out of the TextBlock private function layoutBlock(block:TextBlock, previousLine:TextLine, container:DisplayObjectContainer):TextLine { var line:TextLine = block.createTextLine(previousLine, container.width); var y:Number = 0; while(line) { container.addChild(line); y += line.ascent; line.y = y; y += line.descent; //If we reached the height boundary, return the last line that fit. if(y + line.height > container.height) return line; line = block.createTextLine(line, container.width); } return line; } } }
多文本块布局和多容器布局
这将是非常cool的事情, 我准备将上面两个方法合并起来.
为解决这个问题, 先让我们列一下问题:
1. 我们有一个TextBlock的列表
2. 我们有一个DisplayObjectContainer列表, 要将所有的TextBlock装入
3. 我们希望一个 DisplayObjectContainer装尽可能多的行
4. 当一个 DisplayObjectContainer 满了, 就到下一个DisplayObjectContainer , 并且接着断掉地方开始
5. 我们需要记录最后一个TextLine, 以便知道是哪个断掉了
前面的经验: 当没有line时TextBlock会返回null, 当Container满的时候, 会返回在Container的最后的line, 因此, 逻辑如下:
1. 循环每个TextBlock
2. 尽可能将更多的TextLine放入TextBlock, 并返回最后一个TextLine
如果TextLine是null, 那意味着TextBlock已经没有TextLine, 并且DOC还有空间, 那就保持DOC不变, 移到下一个TextBlock.
如果TextLine不是null, 那意味TextBlock还有TextLine, 但是DCO已经没有空间, 那就保持TextBlock不变, 移动下一个DCO.
3. 如果任何一个list结束(TextBlock list, 和DCO list), 就返回
代码如下:
var blocks:Vector.<TextBlock> = new <TextBlock>[block1, block2, block3, block4]; var containers:Vector.<DisplayObjectContainer> = new <DisplayObjectContainer>[container1, container2, container3]; layout(blocks, containers); function layout(blocks:Vector.<TextBlock>, containers:Vector.<DisplayObjectContainer>):void { var blockIndex:int = 0; var containerIndex:int = 0; var block:TextBlock; var container:DisplayObjectContainer; var line:TextLine; while(blockIndex < blocks.length) { block = blocks[blockIndex]; container = containers[containerIndex]; line = layoutInContainer(container, block, line); if(line && ++containerIndex < containers.length) { container = containers[containerIndex]; containerY = 0; } else if(++blockIndex < blocks.length) block = blocks[blockIndex]; else return; } } var containerY:Number = 0; function layoutInContainer(container:DisplayObjectContainer, block:TextBlock, previousLine:TextLine):TextLine { var line:TextLine = createTextLine(block, previousLine); while(line) { container.addChild(line); containerY += line.ascent; line.y = containerY; containerY += line.descent; if(containerY + line.height > container.height) return line; line = createTextLine(block, line); } //This will be null. return line; } function createTextLine(block:TextBlock, previousLine:TextLine):TextLine { var w:Number = 190; var x:Number = 0; //Apply indention properties here. if(previousLine == null) { w -= 15; x += 15; } var line:TextLine = block.createTextLine(previousLine, w, 0.0, true); if(line) line.x = x; return line; }
哇哦! 休息一下
我知道, 有太多内容需要好好消化. 但你现在已经知道TextLayout不是什么黑魔法巫术, 它完全是用FTE来实现的. 如果你有什么问题, 可以发表评论或是发邮件给我, 我非常乐意看到任何文本布局的技术问题, 或是评论. 我的这些demo还是不系统, 我已经构建出很多更复杂, 并且已经调优过的布局, 放到了tinytlf, 这是一个我已经秘密进行了几个月的小型文本布局框架.
旁白: 文本布局 vs 组件布局
典型的组件布局引擎(比如Flex), child的创建是和布局分开的. 通常是所有的child先加入到displaylist中, 然后在某个时候进行布局. 不会因为child的大小和位置而去创建或是销毁child, 布局也不会影响未来的child创建(这里假设不讨论 virtualized layout, 因为那是一个特例).
块级的布局属性(如padding, 缩进等) 决定了每个TextLine是如何创建和布局. 反过来又影响TextBlock怎么渲染下一个TextLine, 等等, 等等...
我一直无法将块级布局属性从TextLine的创建过程中分开. 这在实际情况在还不是那么糟糕, 但有时会让我走些弯路, 我觉得应该有一个更好的办法, 只是我还没有找到它.