赶在过年前为列位道友奉献一篇文章解决一下很多人头疼的“图文混排”问题。何谓图文混排问题呢?我们知道在Flash里面使用TextField可以输入或显示文本,但是它并不支持像QQ聊天框一样能够输入表情和图片的功能。那怎么办呢?怎么办呢?到底怎么办?999皮炎平来啦,迅速消灭真菌……啊?啊?刚才发生什么事了?我只感觉闻到一阵脚臭味之后就两眼一黑,之后神马都不知道了。好吧,回到正题。我们知道TextField中有一个htmlText属性,可以设置它为一串带HTML标签的字符串,如:
为此,别无选择的我们只好使用拼接的方式来达到预期效果,即做把图文混排组件做成两层,底层是一个TextField,负责显示文本,上面盖一层Sprite负责显示表情与图片。说起来简单做起来难,因为我们还需要考虑很多其他的因素,比如:在一个点插入表情后怎样不让上层表情图片覆盖住文本;删除表情的时候事实上我们是在删除一段表请图案下的文本,那如何让文本被删除的过程中实现表情的删除呢;如何控制根据表情图片的大小动态调整一行文本的行高与行宽呢…… 达奇同学在Google Code(Google公司提供的一个在线的开发人员共享库,更多信息请自行搜索)上找到一个很不错的AS组件,名字叫做RichTextField,比较不错地解决了图文混排的难题,细节做得也非常到位,体积小,运行效率高。(点击此链接来查看作者博客中对此组件的相关介绍及演示:http://www.riaidea.com/blog/archives/295.html)对于此组件的作者:Flashlizi 我了解不是很多,但是看他的代码让我学了不少东西,小生对其表示强烈的敬意,并对我们国人能开发出如此不错的组件表示自豪啊。他的博客也值得我们关注哦~(http://www.riaidea.com) 下面让我们开始学习如何使用之吧。首先去Google Code上下载最新版本的RichTextField代码压缩包(RichTextField2.0_src.zip )。压缩包中有好多文件夹,我们看到: <ignore_js_op> 在bin文件夹中能够看到组件演示的swf,要是想在Flash Builder中自己编译此演示应用的话就需要用到src和lib文件夹中的文件。首先,新建一个ActionScript项目,把src文件夹下面的全部文件拷贝到我们项目的src目录下,然后右键点击tests目录下的Test.as文件,在弹出目录中选择“将此文件作为默认应用程序”(Flash CS工具就直接把src\tests目录下的Test.as文件作为文档类即可)。之后如果你编译,将会报错提示文档类中有一些类没有定义。这是因为我们没有将那些定义在swc中的代码编译进项目中。复制lib文件夹并粘贴至我们工程根目录下,然后设置项目属性把lib文件夹作为swc目录编译进我们的项目(在Flash Buidler中进行此操作的具体流程参看案例16的开始部分)。这样我们就可以运行查看演示应用的效果了。 在doc目录下面我们可以双击index.html文件查看RichTextField组件提供的一些API信息,再结合演示应用Test.as文件的代码掌握此组件的使用方法。如果正在浏览此帖子的你对自己阅读代码的能力信心不足,就让我带着可爱的,卡哇伊的你们一起来熟悉一下此组件的使用方法好了。 打开Test.as文件,结合说明文档我们可以很快理解RichTextField一些属性的意义,这样就迅速略过一系列的初始化的代码,看到下面这一段代码段
在init函数中作者声明了两个RichTextField组件,分别是_output 和 _input,他们的type属性是不一样的,RichTextField的type属性可选参数有: RichTextField.DYNAMIC: 动态文本; RichTextField.INPUT:可输入文本; 在不设置type属性时默认类型是动态文本,作者没有设置_output的类型,所以它是动态文本,仅负责显示;设置了 _input类型为可输入文本,所以它就成了用户的文本输入框。之后作者写了这么一段:
1.如不需要超链接,设置颜色等需要用到HTML标签元素时还是把html属性设置为false较好。 2.告诉我们用一个表情替换一段字符串的方法。 3.设置caretIndex 属性可以设置文本框中光标的位置 接下来我们看到作者在为表情容器_smileyContainer进行初始化。_smileyContainer中将存放所有可用的表情,而这些表情类都存放于_smileys数组当中,而且他这个例子中用到的表情都集成在了lib文件夹下的smileys.swc中了。了解到这一点后我们就知道应该怎样添加我们想添加的表情了。我带着大家一起来做一个QQ表情放进来吧。先打开QQ,随便找一个倒霉蛋聊天,在聊天框里面选择一个表情,之后右键点击此表情并选择“另存为”: <ignore_js_op> 在另存为框中随便取个名字,如果是有动画的表情请选择保存类型为.gif类型,若是静态图片表情么格式就随便你了。之后,打开Flash CS工具,选择文件-->导入-->导入到库,之后选择我们刚才保存的gif表情文件。这样我们就会看到库中出现了很多gif动画用到的图片以及CS工具为我军自动生成的一个已做成动画的元件。 <ignore_js_op> 虽然我们需要的动画元件CS工具已自动帮咱们做好了,但该元件的属性还没有设置过呢,打开此元件的属性设置面板,将它导出为Actionscript,让“在第一帧中导出”的钩被勾选上以保证它能被顺利地编译成swc。 <ignore_js_op> 之后要做的就是去文件-->发布设置中把“导出SWC”的钩钩上 好,最后保存,然后Ctrl + Enter测试影片,之后我们可以在fla保存目录下找到编译生成的swc文件,把它拷贝到我们的lib目录下面,这样就把我们的自定义表情编译到项目里面啦。把自定义表情元件的类定义加入到_smileys数组中去吧:
最后,让我们属性一下RichTextField组件为我们提供的强大的外部插件功能。作者已经为我们提供了一个元素快捷符号输入插件ShortcutPlugin,并在Test.as中使用上去了。
|
<ignore_js_op>
-
5.jpg (54.63 KB, 下载次数: 8)
- 虽然在Flex4以及Flash CS5中存在textLayout这个东西可以用,不过这需要最新版本SDK的支持,且如果使用Flex4 SDK的话会使编译结果大小增大数倍,代价有些大,所以一般的开发者大可使用这款存AS的轻量级组件。
请允许寡人做一些小小的拓展
RichTextField组件提供给我们了图文混排的功能,但是在一些应用中我们仍需要更多的一些自定义功能,让我们一起来做两个需求度最高的功能。
改变字体颜色与大小
当遇到要改变字体颜色,大小等格式时第一时间就会想到TextFormat,于是我们先试着这样做一下看看:复制代码- public function setTextColor( newColor:uint ):void{
- _textRenderer.defaultTextFormat.color = newColor;
- _textRenderer.setTextFormat( _textRenderer.defaultTextFormat );
- }
- public function setTextSize( newSize:uint ):void{
- _textRenderer.defaultTextFormat.size = newSize;
- _textRenderer.setTextFormat( _textRenderer.defaultTextFormat );
- }
改变字体颜色:
<ignore_js_op>
设置更大号的字体:
<ignore_js_op>
纳尼?what the fuck!!我们设置了新格式的文本有的会被表情所遮挡,这是为什么?!看来事情并没有想象中的那样简单啊。先来研究一下RichTextField实现图文混排的原理再说,我们看到RichTextField中插入表情的方法insertSprite:复制代码- public function insertSprite(newSprite:Object, index:int = -1, autoRender:Boolean = true, cache:Boolean = false):void
- {
- ……
- //insert a placeholder into textfield by using replaceText method
- _textRenderer.replaceText(index, index, _placeholder);
- //calculate a special textFormat for spriteObj's placeholder
- var format:TextFormat = calcPlaceholderFormat(spriteObj.width, spriteObj.height);
- //apply the textFormat to placeholder to make it as same size as the spriteObj
- _textRenderer.setTextFormat(format, index, index + 1);
- ……
- }
所以我们就发现了问题的所在,不能把一般文字和表情占位符应用同一个textFormat,否则就不能保证表情占位符和其所对应的表情等宽。既然这样我们就只能在改变颜色,字体等格式时忽略表情占位符,在RichTextField中写出我们新增的方法:复制代码- private var _startIndex:int = -1;
- /**
- * 设置字体颜色
- * @param newColor
- *
- */
- public function setTextColor( newColor:uint ):void{
- _textRenderer.defaultTextFormat.color = newColor;
- _startIndex = -1;
- LoopFunction();
- }
- /**
- * 设置字体大小
- * @param newSize
- *
- */
- public function setTextSize( newSize:uint ):void{
- _textRenderer.defaultTextFormat.size = newSize;
- _startIndex = -1;
- LoopFunction();
- }
- //递归寻找占位符所在索引,把除占位符以外字符全部更新字体格式
- private function LoopFunction( ):void{
- var index:int = _textRenderer.text.indexOf( _placeholder, _startIndex );
- if( index > -1 ){
- _textRenderer.setTextFormat( _textRenderer.defaultTextFormat, _startIndex, index );
- _startIndex = index+1;
- LoopFunction();
- }else{
- _textRenderer.setTextFormat( _textRenderer.defaultTextFormat, _startIndex, _textRenderer.length );
- }
- }
复制代码- private function renderSprite(sprite:DisplayObject, index:int):void
- {
- var rect:Rectangle = textRenderer.getCharBoundaries(index);
- if (rect != null)
- {
- ……
- //仅在未渲染此表情时才addChild
- if( !_spriteContainer.contains(sprite) )_spriteContainer.addChild(sprite);
- }
- }
复制代码- private function LoopFunction( ):void{
- ……
- else{
- _textRenderer.setTextFormat( _textRenderer.defaultTextFormat, _startIndex, _textRenderer.length );
- _spriteRenderer.render(); //刷新表情坐标
- }
- }[/
嵌入特殊元素
玩过魔兽世界或者其他类似网游的道友们应该见识过在聊天框里面出现一个以特殊格式出现的武器名或者人物名,点击之会弹出一个窗口显示该武器或人物的详细信息,我把这一类可点击的东西叫做特殊元素。
那么这是如何做到的呢?在网上搜了半天也没有搜索到什么参考资料,看来国内的高手们都比较低调,那只能让贫道来给出一些引导性的方案仅供参考。我在参考手册里找到TextField拥有一个叫做Link的事件,对于此事件,帮助手册上的解释如下:
当用户单击启用 HTML 的文本字段中的超链接(其中的 URL 以“event:”开头)时调度。 URL 中“event:”后的其余部分将放在 LINK 事件的文本属性中。
在帮助手册中给出的例子里我们能看到详细的用法,若想触发此事件,我们必须以如下格式来修饰一段文本:
<a href="event:track1.mp3">Track 1</a>
这样做的话当鼠标移到Track 1上时会显示手型光标,且点击后TextField会抛出一个Link事件,此事件中的text属性中保存了event后面的一段文本,即track1.mp3。但Link事件的一个局限性就是只能通过text属性来传递一个字符串,而不能传递别的对象,那为了让外部得知用户点击的是哪个物品或者人物,就只能给每个特殊元素对象设置一个唯一的id,并且让Link事件来携带此id字符串,当外部侦听到Link事件时可以根据text属性所携带的id信息去数据模型里面查找此id所对应的特殊元素对象。我们来看看代码:
首先需要定义一个特殊元素的VO类型:
ItemVO.as:复制代码- public class ItemVO
- {
- public var id:String;
- public var name:String;
- public var type:String;
- public var attack:Number;
- public var defence:Number;
- public var weight:Number;
- }
GameModel.as:复制代码- public class GameModel
- {
- public var itemList:Array = new Array();
- private static var instance:GameModel = null;
- public function GameModel()
- {
- }
- public static function getInstance():GameModel{
- return instance ||= new GameModel();
- }
- }
ElemClickedEvent.as:复制代码- public class ElemClickedEvent extends Event
- {
- public var ElemType:int;
- public var id:String;
- public function ElemClickedEvent(type:String, ElemType:int=0, id:String="", bubbles:Boolean=false, cancelable:Boolean=false)
- {
- super(type, bubbles, cancelable);
- this.ElemType = ElemType;
- this.id = id;
- }
- }
- 好了,各位休息够了吧?Are you fucking ready? Let`s fucking go!
首先要做的自然是在RichTextField构造函数中为文本区域对象_textRenderer侦听Link事件,不过不是每个RichTextField对象中都会存在嵌入元素,所以如果直接为他们都添加Link事件侦听将会让那些没有嵌入元素的RichTextField对象中存在着没有用的Link侦听器使Flash Player占有更多的CPU。有的人认为用不到的东西都会被Flash Player垃圾回收,实则不然,对于一般的事件侦听器一旦被添加就不会被垃圾回收,除非给它们设置弱引用,所幸Adobe官方在addEventListener提供了一个参数来为事件侦听器设置弱引用,这个参数在addEventListener的第5个参数位置,我们保持第3,4个参数为默认值,给第五个参数设置为true即可让事件侦听器采用弱引用机制,当该侦听器一段时间被闲置后就会被垃圾回收,节省内存。复制代码- public function RichTextField()
- {
- ……
- _textRenderer.addEventListener( TextEvent.LINK, onLink, false, 0, true );
- }
复制代码- public static const ELEM_CLICKED_EVENT:String = "elem clicked event";
- /**
- * 嵌入特殊元素,特殊元素在被点击时会显示出其详细信息
- * @param type 元素类别
- * @param text 元素文本
- * @param info 元素实例
- *
- */
- public function insertSpecialElem(type:int, text:String, info:ItemVO):void{
- //注意在</a>末尾加一个空格,不然在超链接文本之后编辑的任何文字都会带有超链接
- _textRenderer.htmlText += "<a href='event:" + type + "_" + info.id + "'><font color='#9681b6'><u>" + text + "</u></font></a> ";
- GameModel.getInstance().itemList.push( info );
- }
- private function onLink(event:TextEvent):void{
- var ary:Array = event.text.split("_");
- dispatchEvent( new ElemClickedEvent(ELEM_CLICKED_EVENT, ary[0], ary[1]) );
- }
第二个方法是Link事件的侦听函数,当点击一个特殊元素的超链接文本时我们可以捕获Link事件,由于之前我们把type和id信息合并到了一个字符串中,所以现在需要把这两个信息从event.text中拆分出来并传递给一个ElemClickedEvent事件里面让它携带出去。
好了,是时候去应用程序里面把之前写的API用起来了,在Test.as的初始化函数init里面末尾位置写上以下代码:复制代码- public static const WEAPON_TYPE:int = 1001;//武器类型
- private function init(e:Event = null):void
- {
- ……
- //嵌入聊天框中的道具
- var item:ItemVO = new ItemVO();
- item.id = "drak";
- item.name = "黑暗之剑";
- item.type = "剑";
- item.attack = 10;
- item.defence = 10;
- item.weight = 5;
- _input.insertSpecialElem( WEAPON_TYPE, "黑暗之剑", item );
- _input.addEventListener( RichTextField.ELEM_CLICKED_EVENT, onElemClick);
- _output.addEventListener( RichTextField.ELEM_CLICKED_EVENT, onElemClick);
- }
- private function onElemClick(event:ElemClickedEvent):void{
- var ary:Array = GameModel.getInstance().itemList;
- var item:ItemVO;
- for( var i:int=0; i<ary.length; i++ ){
- var elem:ItemVO = ary[i] as ItemVO;
- if( elem.id == event.id ){
- item = elem;
- break;
- }
- }
- }
ElemInfoWindow.as:复制代码- public class ElemInfoWindow extends Sprite
- {
- public var item:ItemVO;
- private var closeBtn:Sprite;
- private var tf:TextFormat = new TextFormat(null, null, 0xffffff);
- public function ElemInfoWindow( item:ItemVO )
- {
- this.item = item;
- draw();
- }
- public function draw():void{
- var X:int = 20;
- var Y:int = 40;
- if( item.name.length > 0 ){
- generateNewTF( "name", new Point(X,Y), new Point(X+50,Y) );
- Y += 20;
- }
- if( item.type.length > 0 ){
- generateNewTF( "type", new Point(X,Y), new Point(X+50,Y) );
- Y += 20;
- }
- if( item.attack != 0 ){
- generateNewTF( "attack", new Point(X,Y), new Point(X+50,Y) );
- Y += 20;
- }
- if( item.defence != 0 ){
- generateNewTF( "defence", new Point(X,Y), new Point(X+50,Y) );
- Y += 20;
- }
- if( item.weight != 0 ){
- generateNewTF( "weight", new Point(X,Y), new Point(X+50,Y) );
- Y += 20;
- }
- this.graphics.lineStyle(1);
- this.graphics.beginFill(0x000000, 0.5);
- this.graphics.drawRect( 0, 0, this.width + 20, this.height );
- this.graphics.endFill();
- drawCloseBtn();
- }
- private function generateNewTF( property:String, desPos:Point, valuePos:Point ):void{
- var description:TextField = new TextField();
- description.defaultTextFormat = tf;//注意
- switch( property ){
- case "name":
- description.text = "名称:";
- break;
- case "attack":
- description.text = "攻击力:";
- break;
- case "defence":
- description.text = "防御力:";
- break;
- case "type":
- description.text = "类型:";
- break;
- case "weight":
- description.text = "重量:";
- break;
- }
- description.selectable = false;
- description.x = desPos.x;
- description.y = desPos.y;
- addChild( description );
- var value:TextField = new TextField();
- value.defaultTextFormat = tf;
- value.selectable = false;
- value.text = item[property];
- value.x = valuePos.x;
- value.y = valuePos.y;
- addChild( value );
- }
- private function drawCloseBtn():void{
- closeBtn = new Sprite();
- closeBtn.graphics.lineStyle(1);
- closeBtn.graphics.beginFill(0);
- closeBtn.graphics.drawRect(0,0,20,20);
- closeBtn.graphics.endFill();
- closeBtn.graphics.lineStyle(1, 0xff0000);
- closeBtn.graphics.moveTo(5,5);
- closeBtn.graphics.lineTo(15,15);
- closeBtn.graphics.moveTo(5,15);
- closeBtn.graphics.lineTo(15,5);
- closeBtn.addEventListener(MouseEvent.CLICK, onClick);
- addChild( closeBtn );
- closeBtn.x = this.width - 25;
- closeBtn.y = 5;
- closeBtn.buttonMode = true;
- }
- private function onClick(event:MouseEvent):void{
- if(this.parent){
- this.parent.removeChild( this );
- closeBtn.removeEventListener(MouseEvent.CLICK, onClick);
- }
- }
- }
复制代码- private function onElemClick(event:ElemClickedEvent):void{
- var ary:Array = GameModel.getInstance().itemList;
- var item:ItemVO;
- for( var i:int=0; i<ary.length; i++ ){
- var elem:ItemVO = ary[i] as ItemVO;
- if( elem.id == event.id ){
- item = elem;
- break;
- }
- }
- if( item ){
- var window:ElemInfoWindow = new ElemInfoWindow( item );
- window.x = stage.mouseX;
- window.y = stage.mouseY;
- addChild( window );
- }
- }
这里存在一个问题想必各位也发现了,一旦设置了字体格式后原先紫色带下划线的特殊元素格式也会被同化,至于这一点我们刚才在做改变字体格式功能时也碰到过,要解决此问题还需要把特殊元素的文本,一般文本,表情占位符的格式分开来处理,感觉比较麻烦我也懒得去做了,不过在魔兽世界里面是仅提供了插入嵌入元素,并没有提供改变字体格式的功能,所以如果这样的话你就不必为设置了字体格式后会改变嵌入元素格式而烦恼了,因为你没有开放改变字体格式的功能~
对于聊天框的开发,存在许多不同的解决方案,我这里只是给出其中一个思路,列位爱卿可以尽管拿去参考不要给寡人留面子~
源码奉上: <ignore_js_op> RichTextFieldTest.rar (242.84 KB, 下载次数: 867)
最后申明:依此组件目前的开发进度,并不适合用于实际项目中,此帖子仅供学习图文混排基本原理。若要在实际项目中使用,可选择:6DN RichTextArea