[译] Flash文本引擎, 第三部分: 布局

flash fte layout

[原文链接: http://guyinthechair.com/2010/08/the-fte-part-3-textblocks-textlines-and-text-layout/  ]
[原文作者: Paul Taylor  原文时间:  Aug. 9, 2010 ]
[原创翻译: http://www.smithfox.com/?e=184 ,转载请保留此声明, 谢谢]

这是介绍Flash文本引擎系列文章的第三部分: 第一部分, 第二部分, 第三部分.

首先需要澄清, 这些系列文章不是写Adobe的文本布局框架(Text Layout Framework, 以下简称TLF)的, TLF是一个高级的排版和文字布局框架, TLF是建立在FTE(Flash Text Engine)之上的, FTE是一个低层的Player native API, 它在flash.text.engine package内.

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);
}

Source

虽然上面的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);
}

Demo:

Source

对齐:

居中对齐:

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);
}


Source

右对齐是一样的, 只是无需要乘以0.5了

Source

看到没? 标准的布局实战! 最终, 我们用最常见数学实现了组件的布局. 你可能会说: "嗯! 缩进和对齐太简单了, 它们只需要计算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;
}


Source

多容器布局

啊! 让我们进入到好东东, 多容器布局真的很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;
}


Source

多文本块布局和多容器布局

这将是非常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;
}


Source
Text Source

哇哦! 休息一下

我知道, 有太多内容需要好好消化. 但你现在已经知道TextLayout不是什么魔法巫术, 它完全是用FTE来实现的. 如果你有什么问题, 可以发表评论或是发邮件给我, 我非常乐意看到任何文本布局的技术问题, 或是评论. 我的这些demo还是不系统, 我已经构建出很多更复杂, 并且已经调优过的布局, 放到了tinytlf, 这是一个我已经秘密进行了几个月的小型文本布局框架.

旁白: 文本布局 vs 组件布局

典型的组件布局引擎(比如Flex), child的创建是和布局分开的. 通常是所有的child先加入到displaylist中, 然后在某个时候进行布局.  不会因为child的大小和位置而去创建或是销毁child, 布局也不会影响未来的child创建(这里假设不讨论 virtualized layout, 因为那是一个特例). 

块级的布局属性(如padding, 缩进等) 决定了每个TextLine是如何创建和布局. 反过来又影响TextBlock怎么渲染下一个TextLine, 等等, 等等...

我一直无法将块级布局属性从TextLine的创建过程中分开. 这在实际情况在还不是那么糟糕, 但有时会让我走些弯路, 我觉得应该有一个更好的办法, 只是我还没有找到.

smithfox | Wednesday 24 August 2011 at 10:07 am | | UI        | Used tags: , ,

One comment

Louis Vuitton iPhone 6 Plus Case

One of the biggest areas of improvement for the iPhone 7 could be the camera. Rumours that emerged in November 2014 suggested that the camera will represent the biggest camera jump in the history of the iPhone, with a two-lens system that could capture “DSLR-quality imagery,” according to Daring Fireball’s John Gruber.

Louis Vuitton iPhone 6 Plus Case, (URL) - 04-06-’15 15:05
(optional field)
(optional field)
为阻止垃圾广告, 请在提交评论前, 回答一个简单问题(Please answer an simple question)
Remember personal info?
Notify
Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.