[转] Flash文本引擎, 第二部分: 交互
FTE交互
在前一篇文章中, 我介绍了如何渲染TextLine, 本文将介绍如何和已经创建的TextLine交互.
TextLine是一个InteractiveObjects对象, 你可以直接增加event listener以侦听哪些交互事件。
FTE也能让你为每一个ContentElement指定EventDispatcher. 当用户和ContentElement的数据交互时, 会clone到用户指定的EventDispatcher. 我在下面的讨论中, 你会发现每种方法都有其长处和短处.
方法一: 将TextLine看作InteractiveObject
因为TextLine是InteractiveObject, 你可以监听每个TextLine实例的键盘和鼠标事件. 这种方式, 你能知道是在和哪个TextLine在交互, 但主要缺点是对其所正在渲染的ContextElement却一无所知. 一个TextLine可以渲染多个ContentElement, 多个TextLine又可以渲染同一个ContentElement.
看下面的Demo:
package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.text.engine.*; import flash.utils.Dictionary; [SWF(width="235", height="100")] public class SimpleDemo2 extends Sprite { public function SimpleDemo2() { super(); setup(); } private var lineNumbers:Dictionary = new Dictionary(false); private function setup():void { addChild(lineHolder); lineHolder.y = 40; lineHolder.x = 130; var elements:Vector.<ContentElement> = new Vector.<ContentElement>(); elements.push( createTextElement('Be careless ', 26), createTextElement('in your dress if you will, ', 16), createTextElement('but keep a ', 20), createTextElement('tidy soul.', 26), createTextElement('\n - Mark Twain', 20) ); var i:int = 0; var block:TextBlock = new TextBlock(new GroupElement(elements)); var line:TextLine = block.createTextLine(null, 125); var _y:Number = 0; while(line) { addChild(line); _y += line.height; line.y = _y; line.addEventListener(MouseEvent.ROLL_OVER, onMouseEvent); line.addEventListener(MouseEvent.ROLL_OUT, onMouseEvent); line.addEventListener(MouseEvent.CLICK, onMouseEvent); line.addEventListener(MouseEvent.MOUSE_DOWN, onMouseEvent); line.addEventListener(MouseEvent.MOUSE_UP, onMouseEvent); lineNumbers[line] = ++i; line = block.createTextLine(line, 125); } } private function createTextElement(text:String, size:int):TextElement { return new TextElement(text, new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), size) ); } private function onMouseEvent(event:MouseEvent):void { var target:TextLine = TextLine(event.target); renderNotification(lineNumbers[target] + ': ' + event.type); } private var lineHolder:Sprite = new Sprite(); private function renderNotification(text:String):void { while(lineHolder.numChildren) lineHolder.removeChildAt(0); var block:TextBlock = new TextBlock( new TextElement(text, new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18) ) ); var line:TextLine = block.createTextLine(null, 200); while(line) { lineHolder.addChild(line); line.y = line.height; line = block.createTextLine(line, 200); } } [Embed(source="assets/Times New Roman.ttf", embedAsCFF="true", fontFamily="Times")] private var times:Class; } }
实际上, 有的情况, 你也没有必要知道是哪些ContentElement, 比如, 你不关心TextLine的修饰: 下划线, 删除线, 是否被选中.
下面的Demo是可以选择文字的:
package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.geom.Point; import flash.geom.Rectangle; import flash.text.engine.*; import flash.ui.Mouse; import flash.ui.MouseCursor; [SWF(width="400", height="125")] public class FTEDemo7 extends Sprite { private var beginIndex:int = -1; private var endIndex:int = -1; private var lines:Array = []; public function FTEDemo7() { var elements:Vector.<ContentElement> = new Vector.<ContentElement>(); elements.push( createTextElement('He ', 20), createTextElement('who loves ', 16), createTextElement('practice ', 26), createTextElement('without ', 16), createTextElement('theory ', 26), createTextElement('is like the ', 16), createTextElement('sailor ', 20), createTextElement('who boards his ship without a ', 16), createTextElement('rudder ', 26), createTextElement('and ', 16), createTextElement('compass ', 26), createTextElement('and ', 16), createTextElement('never knows where he may cast.', 24), createTextElement('\n - Leonardo da Vinci', 22) ); var i:int = 0; var block:TextBlock = new TextBlock(new GroupElement(elements)); var line:TextLine = block.createTextLine(null, 400); var _y:Number = 0; while(line) { lines.push(addChild(line)); _y += line.height; line.y = _y; line.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); line.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); line.addEventListener(MouseEvent.ROLL_OVER, onRoll); line.addEventListener(MouseEvent.ROLL_OUT, onRoll); line = block.createTextLine(line, 400); } } private function createTextElement(text:String, size:int):TextElement { return new TextElement(text, new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), size) ); } private function onMouseDown(event:MouseEvent):void { stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); graphics.clear(); var line:TextLine = TextLine(event.target); beginIndex = line.textBlockBeginIndex + line.getAtomIndexAtPoint(event.stageX, event.stageY); } private function onMouseMove(event:MouseEvent):void { if(!event.buttonDown) return; var line:TextLine = TextLine(event.target); endIndex = line.textBlockBeginIndex + line.getAtomIndexAtPoint(event.stageX, event.stageY); drawBackground(beginIndex, endIndex); } private function onMouseUp(event:MouseEvent):void { stage.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp); var objs:Array = getObjectsUnderPoint(new Point(event.stageX, event.stageY)); var obj:Object; var found:Boolean = false; while(objs.length) { if(objs.pop() is TextLine) { found = true; break; } } Mouse.cursor = found ? MouseCursor.IBEAM : MouseCursor.ARROW; } private function drawBackground(begin:int, end:int):void { graphics.clear(); if(begin > end) { var begin2:int = begin; begin = end; end = begin2; } var line:TextLine; for(var i:int = 0; i < lines.length; i++) { line = lines[i]; if(line.textBlockBeginIndex <= begin && (line.textBlockBeginIndex + line.rawTextLength) >= begin) break; } var startLine:TextLine = line; for(i = 0; i < lines.length; i++) { line = lines[i]; if(line.textBlockBeginIndex <= end && (line.textBlockBeginIndex + line.rawTextLength) >= end) break; } var endLine:TextLine = line; line = startLine; var bounds:Rectangle; // Don't ever do this! // I'm only doing this because I don't want errors // and I didn't have time to vet this math 100%. try { while(true) { if(line == null) break; if(line == startLine) { if(startLine == endLine) bounds = line.getAtomBounds(Math.min(Math.max(begin - line.textBlockBeginIndex, 0), line.rawTextLength - 1)).union(line.getAtomBounds(Math.min(Math.max(end - line.textBlockBeginIndex, 0), line.rawTextLength - 1))); else bounds = line.getAtomBounds(Math.min(Math.max(begin - line.textBlockBeginIndex, 0), line.rawTextLength - 1)).union(line.getAtomBounds(line.rawTextLength - 1)); bounds.x += line.x; bounds.y += line.y; } else if(line == endLine) { bounds = line.getAtomBounds(Math.min(Math.max(end - line.textBlockBeginIndex, 0), line.rawTextLength - 1)).union(line.getAtomBounds(0)); bounds.x += line.x; bounds.y += line.y; } else bounds = line.getBounds(line.parent); graphics.beginFill(0x003399, 0.25); graphics.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); if(line == endLine) break; line = line.nextLine; } } catch(e:Error) { // Do noooothing } } private function onRoll(event:MouseEvent):void { Mouse.cursor = (event.type == MouseEvent.ROLL_OVER || event.buttonDown) ? MouseCursor.IBEAM : MouseCursor.ARROW; } [Embed(source="assets/Times New Roman.ttf", embedAsCFF="true", fontFamily="Times")] private var times:Class; } }
方法二: 用TextLineMirrorRegions (以面简写成TLMRs)
FTE交互的首选方式还是用 TextLineMirrorRegions, 上篇文章说过: 你必须用 TextElement, GraphicElement, or GroupElement 之一来创建文本实例. 创建后你可以设置ContentElement.eventMirror属性为你所指定的EventDispatcher. 这种方式能让你和特定的ContentElement交互.
在下面的demo代码中, 我创建了一个EventDispather对象, 并且设置给TextElement.eventMirror属性, 然后监听这个EventDispather对象的 mouseMove 事件, 每当Mouse over这个TextElement时, 就会trace下来.
var dispatcher:EventDispatcher = new EventDispatcher(); new TextElement('Inspiring quote here.', new ElementFormat( new FontDescription()), dispatcher); var onMouseMove:Function = function(e:MouseEvent):void{ trace('Mouse move on ' + e.target.toString()); } dispatcher.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
下面Demo中的两行文字是同一个TextElement的两个不同的部分:
package { import flash.display.Graphics; import flash.display.Sprite; import flash.events.EventDispatcher; import flash.events.MouseEvent; import flash.text.engine.*; [SWF(width="190", height="40")] public class SimpleDemo3 extends Sprite { public function SimpleDemo3() { super(); addChild(lineHolder); lineHolder.x = 100; var dispatcher:EventDispatcher = new EventDispatcher(); dispatcher.addEventListener(MouseEvent.MOUSE_MOVE, onMouseEvent); dispatcher.addEventListener(MouseEvent.MOUSE_OUT, onMouseEvent); dispatcher.addEventListener(MouseEvent.MOUSE_OVER, onMouseEvent); dispatcher.addEventListener(MouseEvent.MOUSE_DOWN, onMouseEvent); dispatcher.addEventListener(MouseEvent.MOUSE_UP, onMouseEvent); dispatcher.addEventListener(MouseEvent.CLICK, onMouseEvent); var block:TextBlock = new TextBlock(new TextElement('The quick brown fox...', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18), dispatcher)); var line:TextLine = block.createTextLine(null, 100); var _y:Number = 0; while(line) { addChild(line); _y += line.height; line.y = _y; line = block.createTextLine(line, 100); } } private function onMouseEvent(event:MouseEvent):void { var target:TextLine = TextLine(event.target); renderNotification(event.type); } private var lineHolder:Sprite = new Sprite(); private function renderNotification(text:String):void { while(lineHolder.numChildren) lineHolder.removeChildAt(0); var block:TextBlock = new TextBlock( new TextElement(text, new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18) ) ); var line:TextLine = block.createTextLine(null, 200); while(line) { lineHolder.addChild(line); line.y = line.height; line = block.createTextLine(line, 200); } } [Embed(source="assets/Times New Roman.ttf", embedAsCFF="true", fontFamily="Times")] private var times:Class; } }
和之前的Demo有什么不同之处? TextLine有一个mirrorRegions 属性, 保存了TextLineMirrorRegion数组(Vector). 由于多个ContentElement能被同一个TextLine所渲染, TextLine会为每个ContentElement创建TLMR实例, 并且分别赋给各个ContentElement.eventMirror属性.
TextLine会监听自己的交互事件, 当事件和任何一个TLMR的事件重叠时, TextLine会通知相应的TLMR. 在所有TextLine的正常事件处理结束后. 每个TLMR会用其eventMirror属性所指的EventDispather实例再次dispatch事件一次.
这个例子中, 我为TextLine和其ContentElement的eventMirror都监听了 "MouseDown" 事件. 注意eventMirror的事件触发的时间:
package { import flash.display.Sprite; import flash.events.EventDispatcher; import flash.events.MouseEvent; import flash.text.engine.*; import flash.utils.getTimer; [SWF(width="285", height="55")] public class SimpleDemo4 extends Sprite { public function SimpleDemo4() { addChild(lineHolder); lineHolder.x = 80; lineHolder.y = 5; addChild(lineHolder2); lineHolder2.x = 80; lineHolder2.y = 25; var dispatcher:EventDispatcher = new EventDispatcher(); dispatcher.addEventListener(MouseEvent.MOUSE_DOWN, onMouseMirrorEvent); var block:TextBlock = new TextBlock(new TextElement('Click Me.', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18), dispatcher)); var line:TextLine = block.createTextLine(null, 75); var _y:Number = 0; while(line) { addChild(line); _y += line.height; line.y = _y; line.addEventListener(MouseEvent.MOUSE_DOWN, onMouseLineEvent); line = block.createTextLine(line, 75); } } private var lineHolder:Sprite = new Sprite(); private var lineTime:Number = 0; private function onMouseLineEvent(event:MouseEvent):void { lineTime = getTimer(); var block:TextBlock = new TextBlock( new TextElement(event.type + ' from line', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 14) ) ); while(lineHolder.numChildren) lineHolder.removeChildAt(0); var line:TextLine = block.createTextLine(null, 200); var _y:Number = 0; while(line) { lineHolder.addChild(line); _y += line.height; line.y = _y; line = block.createTextLine(line, 200); } } private var lineHolder2:Sprite = new Sprite(); private function onMouseMirrorEvent(event:MouseEvent):void { var block:TextBlock = new TextBlock( new TextElement(event.type + ' from mirror, time between dispatches: ' + (getTimer() - lineTime) + 'ms', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 14) ) ); while(lineHolder2.numChildren) lineHolder2.removeChildAt(0); var line:TextLine = block.createTextLine(null, 200); var _y:Number = 0; while(line) { lineHolder2.addChild(line); _y += line.height; line.y = _y; line = block.createTextLine(line, 200); } } [Embed(source="assets/Times New Roman.ttf", embedAsCFF="true", fontFamily="Times")] private var times:Class; } }
下面的Demo, 我用TextLineMirrorRegion为每个Element设置了不同的样式
package { import flash.display.Sprite; import flash.events.EventDispatcher; import flash.events.MouseEvent; import flash.geom.Rectangle; import flash.text.engine.*; import flash.utils.Dictionary; [SWF(width="250", height="110")] public class FTEDemo5 extends Sprite { public function FTEDemo5() { super(); // Only two things are infinite, the universe and human stupidity, and I'm not sure about the former. -Albert Einstein var e1:TextElement = new TextElement('"Only ', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 16)); var e2:TextElement = new TextElement('two things', new ElementFormat( new FontDescription("Times", FontWeight.BOLD, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 24), new EventDispatcher()); var e3:TextElement = new TextElement(' are infinite, ', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 16)); var e4:TextElement = new TextElement('the universe', new ElementFormat( new FontDescription("Times", FontWeight.BOLD, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 26)); var e5:TextElement = new TextElement(' and ', new ElementFormat( new FontDescription("Times", FontWeight.BOLD, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 16)); var e6:TextElement = new TextElement('human stupidity', new ElementFormat( new FontDescription("Times", FontWeight.BOLD, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 26)); var e7:TextElement = new TextElement(', and ', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 16)); var e8:TextElement = new TextElement('I\'m not sure about the former.', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.ITALIC, FontLookup.EMBEDDED_CFF), 26), new EventDispatcher()); var e9:TextElement = new TextElement('" -', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.ITALIC, FontLookup.EMBEDDED_CFF), 16)); var e10:TextElement = new TextElement('Albert Einstein', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.ITALIC, FontLookup.EMBEDDED_CFF), 16), new EventDispatcher()); var colors:Array = [0x00FF00, 0xFF6600, 0x0066FF]; var mirrors:Dictionary = new Dictionary(); var color:uint; var regions:Vector.<TextLineMirrorRegion>; var region:TextLineMirrorRegion; var bounds:Rectangle; var vec:Vector.<ContentElement> = new Vector.<ContentElement>(); vec.push(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10); var block:TextBlock = new TextBlock(new GroupElement(vec)); var line:TextLine = block.createTextLine(null, 250); var _y:Number = 0; while(line) { addChild(line); _y += line.height; line.y = _y; regions = line.mirrorRegions; while(regions && regions.length) { region = regions.pop(); if(region.mirror in mirrors) color = mirrors[region.mirror]; else color = colors.pop(); mirrors[region.mirror] = color; bounds = region.bounds; graphics.beginFill(color, 0.3); graphics.drawRect(bounds.x, bounds.y + line.y, bounds.width, bounds.height); } line = block.createTextLine(line, 250); } } [Embed(source="assets/Times New Roman.ttf", embedAsCFF="true", fontFamily="Times")] private var times:Class; } }
注意事项:
如果没有<注意事项>, 那就不是flash player的功能了 :)
TLMR只是模拟了事件, 它不会re-dispatch它从TextLine所接受到的实例, 因为TLMR不是一个InteractiveObject.
如果你用eventMirror来监听 MouseEvent, 你要意识到那是一个伪造的事件 -- 就算是target是TextLine, 也是这样,
这些事件不是源自TextLine, 这点和player原生的事件不一样.
Rollover/rollout事件
这种模拟机制, 也意味着我们受到Adobe选择这样做的摆布(或是限制), 他们觉得没有必要模拟rollover/rollout事件. 你会发现eventMirror并没有roll相关的事件. 由于ContentElement并没有display-list children, roll event的行为就和mouseover, mouseout的行为完全一样, 相必Adobe是基于这一点认为没有必要实现roll事件的.
然而, Roll事件还是非常有必要的,
尽管 ContentElement没有 display-list children, 然而它还是有 ContentElement children的, 相当于 ContentElement的层次结构代替了 disply 的层次结构, 所以 roll 事件还是必须的.
举个例子, 看下面的xml model的渲染:
<p> Outside the group. <group> <text color="#44AA00"> First group child. </text> <text color="#AA0044"> Second group child. </text> </group> Outside the group. </p>
上面这种case, 你可能想让group这个node作为一个整体来看待, (就像一个带children的DisplayObjectContainer 那样).
下面就上这个xml model的例子, 你将鼠标在 First group child和 Second group child之间划动时, 你会发现你紧接着会看到来自group的 "mouseout", "mouseover"两个事件, 如果是roll 事件, 应该只会收到从child来的mouseover和mouseout, 应该没有group的相关mouseover和mouseout事件. 下面的这个demo, 按Mouse, 会清空 trace.
package { import flash.display.*; import flash.events.*; import flash.geom.Rectangle; import flash.text.engine.*; import flash.utils.Dictionary; import flashx.textLayout.formats.BaselineOffset; [SWF(width="400", height="110")] public class FTEDemo6 extends Sprite { public function FTEDemo6() { var e1:TextElement = new TextElement('Outside the group. ', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18)); var d2:EventDispatcher = new EventDispatcher(); d2.addEventListener(MouseEvent.MOUSE_OVER, function(e:Event):void{print("1: " + e.type);}); d2.addEventListener(MouseEvent.MOUSE_OUT, function(e:Event):void{print("1: " + e.type);}); var e2:TextElement = new TextElement('First group child. ', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18, 0x44AA00), d2); var d3:EventDispatcher = new EventDispatcher(); d3.addEventListener(MouseEvent.MOUSE_OVER, function(e:Event):void{print("2: " + e.type);}); d3.addEventListener(MouseEvent.MOUSE_OUT, function(e:Event):void{print("2: " + e.type);}); var e3:TextElement = new TextElement('Second group child. ', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18, 0xAA0044), d3); var e4:TextElement = new TextElement('Outside the group.', new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 18)); var vec:Vector.<ContentElement> = new Vector.<ContentElement>(); vec.push(e2, e3); var groupDispatcher:EventDispatcher = new EventDispatcher(); groupDispatcher.addEventListener(MouseEvent.MOUSE_OVER, function(e:Event):void{print("group: " + e.type);}); groupDispatcher.addEventListener(MouseEvent.MOUSE_OUT, function(e:Event):void{print("group: " + e.type);}); var group:GroupElement = new GroupElement(vec, null, groupDispatcher); var vec2:Vector.<ContentElement> = new Vector.<ContentElement>(); vec2.push(e1, group, e4); var block:TextBlock = new TextBlock(new GroupElement(vec2)); var line:TextLine = block.createTextLine(null, 200); var _y:Number = 0; while(line) { addChild(line); _y += line.height; line.y = _y; line = block.createTextLine(line, 200); } addChild(lineHolder); lineHolder.x = 200; addEventListener(MouseEvent.MOUSE_DOWN, clearLines); } private function clearLines(event:MouseEvent):void { while(lineHolder.numChildren) lineHolder.removeChildAt(0); } private var lineHolder:Sprite = new Sprite(); private function print(str:String):void { while(lineHolder.numChildren > 6) lineHolder.removeChildAt(0); var block:TextBlock = new TextBlock( new TextElement(str, new ElementFormat( new FontDescription("Times", FontWeight.NORMAL, FontPosture.NORMAL, FontLookup.EMBEDDED_CFF), 16) ) ); var line:TextLine = block.createTextLine(null, 200); while(line) { lineHolder.addChild(line); line = block.createTextLine(line, 200); } var child:DisplayObject; for(var i:int = 0; i < lineHolder.numChildren; i++) { child = lineHolder.getChildAt(i); child.y = (i+1) * child.height; } } [Embed(source="assets/Times New Roman.ttf", embedAsCFF="true", fontFamily="Times")] private var times:Class; } }
比较:
那, 怎么搭配这两种方法呢? 简而言之, 就是: 看情况. 如果你只是需要基本的交互功能, 而并不关心上下文(比如 selection你就需要关心上下文), 那就直接在TextLine上加listener. 如果你需要关心上下文, 需要知道是在和哪个ContentElement交互, 那你只能选择 event mirroring 的方式.
P.S.
也许我有点强迫症, 每次悬停在FTE文字上时, 我非常希望能看到 I 型的光标, 我最喜欢的Demo, 还是第二个, 因为我实现了 I 型的光标 :) , 总之, 希望你也喜欢! 祝你好运!