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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

补充:游戏里私聊面板多行输入框的图文混排(类似QQ聊天面板)请看这里:

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

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 [email protected]
	 */
	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;
	}
}

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

时间: 2024-08-09 02:56:23

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

仿QQ聊天图文混排流程图【适用于XMPP】

图文混排流程图.graffle4.8 KB 下面附上图片素材: 表情.zip692.5 KB 下面是字符串与图片的详细对应关系: "[呲牙]"字符串对应于图片名    f000.gif; "[调皮]"字符串对应于图片名    f001.gif; "[流汗]"字符串对应于图片名    f002.gif; "[偷笑]"字符串对应于图片名    f003.gif; "[再见]"字符串对应于图片名    f004.

(一一一)图文混排基础 -利用正则分割和拼接属性字符串

很多时候需要用到图文混排,例如聊天气泡中的表情,空间.微博中的表情,例如下图: 红心和文字在一起. 比较复杂的情况是表情夹杂在文字之间. 要实现这种功能,首先要介绍iOS中用于显示属性文字的类. 用于文字显示的类除了text属性之外,还有attributedText属性,这个属性是NSAttributedString类型,通过这个属性可以实现不同文字的不同字体.颜色甚至把图片作为文字显示的功能. 下面介绍这个字符串的使用. 以一条微博内容为应用场景,介绍如何从中找出表情.话题等内容,其中表情替换

用NSAttributedString实现简单的图文混排

iOS7以后,因为TextKit的强大,可以用NSAttributedString很方便的实现图文混排(主要是利用了NSTextAttachment). 关于Textkit的牛逼之处,可以参考objcio上的文章(https://objccn.io/issue-5-1/) 我用NSAttributedString仿写了一个知乎的输入框(低仿,就是有个功能而已),效果如图: demo在这里:(https://github.com/Phelthas/TEST_XMLCommon   的第四个) 有几

图文混排组件(RichTextField)

前两天一个网友请我帮忙用as3实现图文混排的组件,要求支持复制粘贴和剪切功能,还好这段时间不忙,便答应了尝试一下.刚着手的时候考虑使用占位字符,类似[smile]表示笑的表情,不过都还没开始敲代码就发现这样不行,文本框里会显示[smile]的文本,必须要用空白字符才行.那就计算图片的宽度以及空格的宽度,用多个空格来占位吧.那高度呢?用回车?如果一行中有多个不一样大小的图片那不是还要计算最大高度?还有复制粘贴的时候怎么知道复制了哪个?天啊,原来图文混排还要考虑这么多东西的.最怕遇到这些想不通的问题

超全css解决方案之图文混排解决方案

第一种方法:背景图片法 这种方法适用于非动态内容,图片用于装饰的情况下.主要是设置父元素的padding的宽度为背景图片的宽度加上图片和文字的间距,然后把背景图片定位到padding里面就可以了 图文混排demo1,背景图片法    //因是转载的,我又是菜鸟,下面图片乱了,正确的是图片跟文字都在一行. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pel

NGUI研究之有点坑爹的图文混排

 最近研究了了一下NGUI的图文混排工具,得出的结论是有点坑爹..大家看看我的测试步骤,我用的是目前NGUI的版本3.6.6 . 当我在 Open BitMapFont Marker 的时候界面上出现Assets/NGUI/Editor/FreeType.dylib is missing 的字样.于是查了一下,大概意思是必须要把FreeType.dylib 文件拷贝在/usr/local/lib/FreeType.dylib路径下面.FreeType.dylib 文件在NGUI/Editor

浅析微博页面图文混排中遇到的问题

在表情键盘的图文混排中,会有很多细节问题.有的时候不理解其中的原因是很难改正的.本文主要是整理我遇到的各种问题及解决方案,供大家分享.如果你以前也遇到过类似的问题可以用我的方法修正了,希望能够对博友们有所帮助.本文使用swift语言,OC的可能看不惯,但大多方法基本是一样的就是语法不同. 上期回顾:关于微博编辑页面添加表情键盘 如果你不是在董铂然博客园看到本文,请点击查看原文. 1.默认每添加一个表情是添加到最后,但是我们想添加到光标位置 默认做法(其中的str是通过点击某个表情通过代理传来的表

[UGUI]图文混排(一):标签制定和解析

参考链接: https://github.com/SylarLi/RichText/tree/master/Assets/Scripts 正则表达式: https://blog.csdn.net/lyh916/article/details/49201195 图文混排主要用于聊天,其实就是传输某种格式的字符串,然后解析这个字符串,生成表情文字等.图文混排的第一步,就是确定好格式,这里使用html的标签格式,对于代码中出现的start和end字段可以先忽略.标签格式如下: <material=un

Laya的位图文字,高亮文字,图文混排

测试版本:Laya 2.1.1.1 位图文字 白鹭的位图文字是由TextureMerger制作,然后在exml里使用. Laya的则直接使用FontClip组件. 在编辑模式,层级窗口中右键,选择创建组件UI,选择FontClip 将美术提供的位图文字赋值给FontClip的属性面板的skin属性,这样就是个位图文字了.比白鹭要方便些. 高亮文字 白鹭的高亮文字有文本样式 tx.textFlow = <Array<egret.ITextElement>>[ {text: "