文档章节

AS3聊天单行输入框图文混排完美实现

clschen
 clschen
发布于 2015/02/04 07:17
字数 2728
阅读 462
收藏 3

来源:NPC-峰博客

推荐:

几年前刚毕业,第一个游戏模块做的就是聊天。到现在,几个游戏写过几次聊天模块,

之前在4399做的《幻龙骑士》(又名《神骑士》),还有上周六刚上线的《疯狂的子弹》,

用的是同一套代码,聊天输入框没有图文混排,而是用符号代替,输出面板才有图文混排。

输出面板的图文混排由于内容没有键鼠操作,实现很简单,不在本文讨论之列;当然本

文的代码中抽出一小部分就可以实现了。以上两款游戏没有加密,有兴趣的可以去弄来看看,

《子弹》里面的键鼠、技能、射击、弹道搞得好累人啊 ლ(╹ε╹ლ )

特别是弹道计算,是高中毕业后到现在用过的最复杂的数学和物理知识。直线弹道、直

线贯穿弹道、激光弹道、散弹弹道、散弹贯穿弹道、能量弹道、矩形弹道、扇形弹道、圆形

弹道、扇形弹道、虚拟弹道、平行弹道,以及其中几种弹道的组合弹道。用到各种几何方程,

还有平面几何坐标系的平移和旋转,以及角度、弧度、向量计算等等;学校里学的方程、公

式只是基础,直接用的话效率很低,游戏里可以做一些简化计算以提高效率。。。。。。

貌似写跑题了,我是要写什么来着?哦,是输入框的图文混排哈。

现在在多玩YY做一款暂名为《神魔三国》的游戏,预计九月上线。恰巧又接手聊天模块,

需要做输入框的图文混排,花了两天时间摸索,于是就有了本文。

package { import flash.display.MovieClip; import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; import flash.events.KeyboardEvent; import flash.filters.GlowFilter; import flash.geom.Rectangle; import flash.text.TextField; import flash.text.TextFieldType; import flash.text.TextFormat; import flash.text.TextFormatAlign; import flash.ui.Keyboard; import flash.utils.getDefinitionByName; /**
 * 游戏聊天输入框图文混排完美实现
 * 没用到任何第三方类,看import就知道
 * 已添加一百多行注释,实际代码大概三百行
 * 主要测试点:
 * 1.选中一部分图文,再按删除,或插入表情,或Ctrl+X,或Ctrl+V等
 * 2.文本框满行后,按住左右键或鼠标拖选左右移动,触发横向滚动的情况
 * 用以上两点测试,很多第三方控件,或页游中都会出现问题。
 * 另外,本实现不会做成像RichTextField那样的通用的类库,
 * 越是通用的东西,效率越低,建议根据实际对代码做相应调整,Good Luck!
 * @since 2014.6.26
 * @email cceevv@163.com
 */ public class InputText extends Sprite { /*表情占位符,看起来像空格,但不是空格;
   输入框一般不能屏蔽空格输入,但可以屏蔽此符号的输入,因此用来做占位符很合适*/ private const PLACEHOLDER:String = String.fromCharCode(12288); public var textField:TextField; //文本输入框 private var mcLayer:Sprite; //用于放置表情的容器,在文本框上面一层 private var dataList:Vector.<Express>; //存储表情信息的VO private var begin:int = 0; //输入框中选中文本的开始索引 private var end:int = 0; //输入框中选中文本的结束索引,若无选择文本,则 begin==end,并且等于caretIndex  private var scrollH:int = 0; //文本框满行后向左滚动的距离 private var keyCode:uint = 0; //上一次所按的键盘码 private var defaultFormat:TextFormat; //文本默认格式 private var placeFormat:TextFormat; //表情占位符的格式 /**
 * 游戏聊天输入框图文混排
 * @param w 输入框宽度
 * @param h 输入框高度
 */ public function InputText(w:Number = 170, h:Number = 20)
{
defaultFormat = new TextFormat();
defaultFormat.color = 0xFFFFFF;
defaultFormat.size = 12;
defaultFormat.letterSpacing = 0; //此处不能省略 defaultFormat.align = TextFormatAlign.LEFT;
defaultFormat.font = "SimSun"; //宋体 //调整占位符的宽度,使之比表情的宽度大一点点 //不要用占位符的大小(size)去调整宽度,除非你的表情和字体差不多大小,多么痛的领悟 placeFormat = new TextFormat();
placeFormat.letterSpacing = 16;

textField = new TextField();
textField.width = w;
textField.height = h;
textField.type = TextFieldType.INPUT;
textField.defaultTextFormat = defaultFormat; /*
   设置为单行文本框,不建议像《仙侠道》那样用多行文本框,事情会变得更复杂,相信我
   《仙侠道》没有加密,去把代码弄来看看就知道了,用了多行还要处理上下键滚行的恶心问题
   而一般游戏中都会用上下键来处理聊天缓存功能,即上下键翻看前几次已发送的聊天内容,以避免重复输入
 */ textField.multiline = false;
textField.wordWrap = false;

textField.mouseWheelEnabled = false;
textField.restrict = "^" + PLACEHOLDER; //屏蔽占位符,让玩家不能输入此符号 textField.filters = [new GlowFilter(0x0, 1, 3, 3, 3)];
textField.maxChars = 100;
textField.addEventListener(Event.SCROLL, onTextScroll);
textField.addEventListener(Event.CHANGE, afterChange);
textField.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); this.addChild(textField); //遮罩很有必要,满行向左滚动后表情可能只显示一半 var mask:Shape = new Shape();
mask.graphics.beginFill(0);
mask.graphics.drawRect(0, 0, w, h);
mask.graphics.endFill(); this.addChild(mask);

mcLayer = new Sprite();
mcLayer.mask = mask; this.addChild(mcLayer);
dataList = new Vector.<Express>();
} /**
 * 文本框滚动事件,主要是处理横向滚动
 */ private function onTextScroll(e:Event):void { //只处理横向滚动,差异化处理很有必要,不加此判断可能导致递归堆栈溢出 if(textField.scrollH != scrollH)
{
scrollH = textField.scrollH;
render();
}
} /**
 * 键盘事件,不要在此处理Backspace、Delete等事件,相信我
 */ private function onKeyDown(e:KeyboardEvent):void {
begin = textField.selectionBeginIndex;
end = textField.selectionEndIndex;
keyCode = e.keyCode; //发送聊天数据 if(keyCode == Keyboard.ENTER)
{ if(textField.length > 0)
{
var chatContent:String = this.srcContent; //TODO: 这里是向服务端发送数据的逻辑 }
}
} /**
 * 文本内容改变事件, 此处是精华所在,理解了就会发现其实很简单
 */ private function afterChange(e:Event = null):void {
var $begin:int = begin; if(begin == end)
{ if(keyCode == Keyboard.BACKSPACE)
{
delExpress(begin - 1);
$begin = (begin > 0) ? begin - 1 : 0;
} else if(keyCode == Keyboard.DELETE)
{
delExpress(begin);
}
} else //选中了一部分文本,删除其中可能包含的表情 { for(var i:int = begin; i < end; i++)
{
delExpress(i);
}
} //删除表情后,更新表情索引 updateExpressIndex($begin);
render();
keyCode = 0;
} /**
 * 图文混排渲染,此处是难点所在,理解了就会发现其实没那么简单
 */ private function render():void { if(textField.length == 0)
{
clear(); return;
} /*设置表情占位符的宽度,上文已解释过,不赘述*/ textField.setTextFormat(defaultFormat, 0, textField.length);
var textStr:String = textField.text; for(var i:int = 0; i < textStr.length; i++)
{
var char:String = textStr.charAt(i); if(char == PLACEHOLDER)
{
textField.setTextFormat(placeFormat, i, i + 1);
}
} /*先清理所有的表情,此处有优化空间,但建议不要折腾,除非纯粹研究*/ while(mcLayer.numChildren > 0)
{
mcLayer.removeChildAt(0);
} /*下面这一段是处理文本满行后,按左右键或鼠标拖选左右移动,触发横向滚动的情况*/ var showBegin:int = -1; //输入框中能看到的第一个字符索引 var showEnd:int = -1; //输入框中能看到的最后一个字符索引 const sRight:int = textField.scrollH + textField.width; //输入框中能看到的第一个字符在「整个」文本行中的横坐标 /*遍历每个字符的坐标以找出showBegin和showEnd的值*/ for(i = 0; i < textField.length; i++)
{ //此处值得一提的是,不管文本横向怎么滚动,所有字符的坐标都是正数,你知道为什么吗? var rect:Rectangle = textField.getCharBoundaries(i); if(showBegin == -1 && rect.right > textField.scrollH)
{
showBegin = i;
} if(showEnd == -1 && rect.left > sRight)
{
showEnd = i - 1; break;
}
} //未满行的情况,所有字符都显示 if(showEnd == -1)
{
showEnd = textField.length - 1;
} /*把文本框中能看到的表情占位符都加上实际表情*/ for each(var data:Express in dataList)
{ if(data.index >= showBegin && data.index <= showEnd)
{
rect = textField.getCharBoundaries(data.index); //根据表情和文字大小,微调坐标,使表情在文本中居中 data.mc.x = rect.x + 2 - textField.scrollH;
data.mc.y = rect.y - 6;
mcLayer.addChild(data.mc);
}
}
} /**
 * 在文本框当前光标处插入表情
 * @param sign 表情符号,据此符号能够创建出表情显示对象
 */ public function insertExpression(sign:String):void { if(textField.length >= textField.maxChars)
{ return;
} //此处需要重新获取选中文本的开始和结束索引,文本框失去焦点后的怪异问题 begin = textField.selectionBeginIndex;
end = textField.selectionEndIndex; //删除选中文本中可能包含的表情 for(var i:int = begin; i < end; i++)
{
delExpress(i);
} //把选中的文本替换为表情占位符,没有选中则是插入 textField.replaceText(begin, end, PLACEHOLDER); //创建表情数据并保存,保持有序 var $i:int = -1; for(i = 0; i < dataList.length; i++)
{
var data:Express = dataList[i]; if(data.index >= begin)
{
$i = i; break;
}
}
var mc:MovieClip = getMovieClip(sign); if($i == -1)
{
dataList.push(new Express(begin, sign, mc));
} else {
dataList.splice($i, 0, new Express(begin, sign, mc));
} //插入表情后,更新表情索引 updateExpressIndex(begin);
render();
textField.setSelection(begin + 1, begin + 1);
} /**
 * 更新表情数据索引,索引为占位符的索引
 * @param $begin 只更新第$begin个字符后面的数据,前面的不变
 */ private function updateExpressIndex($begin:int):void {
var $i:int = -1; for(var i:int = 0; i < dataList.length; i++)
{ if(dataList[i].index >= $begin)
{
$i = i; break;
}
} if($i != -1)
{
var textStr:String = textField.text; for(i = $begin; i < textStr.length; i++)
{ if(textStr.charAt(i) == PLACEHOLDER)
{
dataList[$i++].index = i;
}
}
}
} /**
 * 取指定索引处的表情数据
 * @param index 表情索引,即占位符索引
 * @return 表情数据
 */ private function getExpress(index:int):Express
{ for each(var data:Express in dataList)
{ if(data.index == index)
{ return data;
}
} return null;
} /**
 * 删除指定索引处的表情数据,并移除表情显示对象
 * @param index 表情索引
 */ private function delExpress(index:int):void { for(var i:int = 0; i < dataList.length; i++)
{
var data:Express = dataList[i]; if(data.index == index)
{
dataList.splice(i, 1); if(mcLayer.contains(data.mc))
{
mcLayer.removeChild(data.mc);
} return;
}
}
} /**
 * 用插入的表情符号创建表情动画
 * @param sign 表情符号
 * @return 表情动画
 */ private function getMovieClip(sign:String):MovieClip
{ /*
   此处符号可根据实际游戏做些处理,
   比如符号为 /:01,重新构造为 chat_expression_01 等
   只要构造后的字符串为你在fla里导出的表情类即可
 */ var $_class:Class = getDefinitionByName(sign) as Class;
var $_item:MovieClip = new $_class();
$_item.mouseChildren = false;
$_item.mouseEnabled = false; return $_item;
} /**
 * 获取图文混排原始数据,用于发向服务端,并广播回来
 */ public function get srcContent():String
{
var charArr:Array = [];
var textStr:String = textField.text; for(var i:int = 0; i < textStr.length; i++)
{
var char:String = textStr.charAt(i); if(char == PLACEHOLDER)
{
var data:Express = getExpress(i);
charArr.push(data.sign);
} else {
charArr.push(char);
}
} return charArr.join("");
} /**
 * 设置图文混排原始数据,主要用于上下键翻看聊天历史记录时解析表情用
 * 输出面板的图文混排由于没有键鼠操作,更简单,不在本文讨论之列;
 * 不过,相信你看到这里也早知道该怎么做了
 */ public function set srcContent(content:String):void {
clear(); //这里的chat_expression_xx是我的游戏里的表情导出类,根据你的游戏自由调整 var reg:RegExp = new RegExp("chat_expression_[0-9]{2}", "ig");
var signArr:Array = content.match(reg);
content = content.replace(reg, PLACEHOLDER); if(content.length > textField.maxChars)
{
content = content.substr(0, textField.maxChars);
} for(var i:int = 0; i < content.length; i++)
{
var char:String = content.charAt(i); if(char == PLACEHOLDER)
{
var sign:String = signArr.shift() as String;
var mc:MovieClip = getMovieClip(sign);
dataList.push(new Express(i, sign, mc));
}
}
textField.text = content;
render();
} /**
 * 清空文本框即相关数据
 */ public function clear():void {
textField.htmlText = "";
begin = end = scrollH = keyCode = 0;
dataList = new Vector.<Express>(); if(mcLayer != null)
{ while(mcLayer.numChildren > 0)
{
mcLayer.removeChildAt(0);
}
}
}

}
} /*===============================================*/ import flash.display.MovieClip; class Express // 内部类,不用新建类文件
{ public var index:int; public var sign:String; public var mc:MovieClip; /**
 * 表情数据
 * @param index 表情索引,即表情占位符在文本中的索引
 * @param sign 表情符号,用于创建表情动画显示对象
 * @param mc 表情动画,已经用sign创建好的显示对象
 */ public function Express(index:int, sign:String, mc:MovieClip)
{ this.index = index; this.sign = sign; this.mc = mc;
}
}

本文转载自:http://www.chenlinsheng.com/?p=941

clschen
粉丝 4
博文 21
码字总数 10662
作品 1
广州
程序员
私信 提问
WLDragon/RichTextField

#图文混排组件(RichTextField) v1.0 as3版的图文混排组件,支持flash的所有显示对象,支持复制粘贴剪切功能,要求使用flash11及以上版本发布 ##如何使用 var rtf:RichTextField = new RichTe...

WLDragon
2015/08/31
0
0
iOS码农聊天室--zychat

ZYChat (一) 是一个实战项目的聊天UI框架,针对高频次高速率刷新最近会话列表和实际对话页面做了缓冲优化,经过测试会话使用的性能和体验非常稳定。 (二) UI框架参考MVVM思想设计,并采用自身...

ZYVincent
2016/01/04
3.2K
1
iOS常用的三方库

UI相关:(转载http://www.jianshu.com/p/be0401e35e06) 上拉下拉刷新控件: 链接:MJRefresh 动态启动图: 链接:YFStartView MBProgressHUD: 链接:MBProgressHUD 一个效果很好的弹出下拉框:...

oschina6k
2016/05/11
416
1
iphone聊天用几种图形(表情)与文字混排的实现与比较 [复制链接]

一、用HTML实现文字表情混合排列 用HTML实现图形文字混排的好处就是你不需要考虑表情在文字里面的位置问题,你只需要自己做一个HTML,然后用UIWEBVIEW加载一下,然后再把WENVIEW放到视图上,...

长平狐
2012/08/13
171
0
UI设计软件Balsamiq Mockups

Balsamiq Mockups出自加利福尼亚州的Balsamiq工作室,创始人Peldi在2008年6月推出了这款手绘风格的产品原型设计工具,并广受好评。2年多来,Balsamiq工作作为一个微型独立软件开发商,专注于...

liyu
2010/07/26
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Linux的基本命令

目录的操作命令(增删改查) 增: mkdir 目录名称; 查: ls 可以看到该目录下的所有的目录和文件 ls -a,可以看到该目录下的所有文件和目录,包括隐藏的 ls -l,可以看到该目录下的所有目录和...

凹凸凸
今天
2
0
在古老unix中增加新用户

Installing 4.3 BSD Quasijarus on SIMH 目标:要在4.3BSD中新增加用户dmr,指定目录/home/dmr,uid为10 gid=31(guest组,系统已建立) 4.3BSD还没有adduser或useradd 直接修改/etc/passwd...

wangxuwei
今天
2
0
Bootstrap(六)表单样式

基本样式 所有设置了 .form-control 类的 <input>、<textarea> 和 <select> 元素都将被默认设置宽度属性为 width: 100%;。 将 label 元素和前面提到的控件包裹在 .form-group 中可以获得最好...

ZeroBit
昨天
3
0
SSL 证书格式转换

SSL 证书格式转换 不同服务器情况下,需要不同的证书格式。 比如 pem 转 pfx。 pem在window 平台下可以导入,但是无法正常使用。 需要转换成pfx。 推荐在线转换工具,由中国数字证书网站提供...

DrChenXX
昨天
2
0
HAProxy

xx

Canaan_
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部