贴吧社区上线了用户等级权限系统,“涂鸦”属于“等级权限”项目中的单项权限功能,有助于丰富完善等级权限体系,为高等级用户提供更强大的功能,帮助产出差异化内容。
“涂鸦”使用Actionscript3开发。本文主要介绍功能实现方式和开发过程中值得注意的地方。
Actionscript3 画图板
AS: Actionscript3.0;
DOOUM: AS的鼠标五个事件,MouseDown、MouseOver、MouseOut、MouseUp、MouseMove事件;
CheckJsReady:负责初始化时和JS互相确认初始化完毕;
SayToJs:负责ActionScript和JavaScript的交互工作;
ImageUpload: 负责ActionScript和Server的交互;
1) View & Model 因为View层的元素不会出现多例的情况,View和Model层已结合在一起。
ViewList: 保存所有主要View对象引用,单例;
Paper:涂鸦画板;
ToolPanel: 工具栏,放置涂鸦相关工具;
BackgroundPicture: 画板背景对象;
WaterMarker:涂鸦水印;
2) Control
ControlCore: 控制中心,单例;
BaseConf: 基本配置,包括Flash所有的默认配置;
FunctionalButtonImages: 加载所有的按钮上的Icon图片,默认编译在swf中;
MouseCursorImages: 加载所有的鼠标指针,默认编译在swf中;
MsgList: 消息文本列表,程序所有的提醒消息文本。
单例模式可以不用在多个类中产生实例,而只是产生一个实例。单例模式可以实现在多个类中可以共享一个实例的数据。同时也可以避免在多个类中反复创建某个实例的工作,因为实际上我们并不需要也不想要每次都new一遍。
如前面所述,ControlCore和 ViewList均使用的是单例模式。
ControlCore作为控制中心,应该是以一个全局的对象存在。所以它不应该在每个类中都出现一个实例。
ViewList保存的是主要View层对象的引用,其实质是对象引用的List,也应该是一个全局的对象。
在单例模式中,使用单例模式的类应该是无法被实例化的。这样才能保证这个类的正确使用。一般将构造函数定义成私有的(private)即可达到这个目的。但是在AS中,构造函数是无法定义成私有的。所以在AS选择了另一种做法。
例: 以下是 A.as代码
01 |
Package { |
02 |
public class A{ |
03 |
static private var _a:A; |
04 |
//构造函数需要传入Class N的实例 |
05 |
public function A(n:N){} |
06 |
07 |
public static function g():A{ |
08 |
if (A._a == null) { |
09 |
A._a = new A(new N()); |
10 |
} |
11 |
return A._a; |
12 |
} |
13 |
14 |
public function doSomething():void{} |
15 |
} |
16 |
} |
17 |
18 |
//在package外再定义一个类N |
19 |
Class N{} |
说明:
类A的构造函数需要传入类N的实例,但是类N是在A.as中定义的,只有在这个文件才能被访问。这样就保证了,类A的构造函数在A.as以外是无法正常被调用的,类似于构造函数私有化了。
在类A中声明一个私有的属性-类A静态对象,该属性将会在g()方法第一次被调用的时候被定义。g()方法的返回是一个类A的实例化对象,他有类A的所有的属性和方法。所以类A的其他公共方法都可以通过g()的返回值来调用。
这种方式还有一种好处是在g()没有被调用之前,私有的_a是不会被定义的。这样可以不用一开始就占用资源。同样,不把类中A所有方法定义成静态方法也是这个原因。
如 doSomething方法 可以这样调用 A.g().dosomething()。
通过这种形式,在AS中也能很方便的使用单例模式。
涂鸦最主要的逻辑实现都集中在Paper上。
一是因为Paper是画纸,是用户主要操作区域;
二是因为当初设计时,没有再进行细分,一些附属的功能也加在Paper里。
其实实现画笔很简单,监听好鼠标的DOOUM五个事件即可。
在实现上,Paper只是一个容器,装载着已画好的图像和正在画图像。并提供给ControlCore一个提取图像数据的接口当做提交之用。
背景BackgroundPicture处于Paper之下。当用户选择加载本地的图片之后,显示该图片。
大致的层次关系如(图3)所示:
如图所示,在用户开始绘画的时候(触发Paper的MouseDown事件),Paper则会创建一个和自己一般大小的A。Paper通过监听用户鼠标事件获得的鼠标轨迹数据,A通过接口获得数据,并draw出。
由于一些原因,在鼠标快速移入移出Paper的时候会导致笔迹与画纸边界出现断裂的现象。通过监听Paper的MouseOver & MouseOut,在触发这两个事件的时候获取鼠标触发以上两个事件的坐标,在A中进行一些偏差容错的计算,使得笔迹连贯自然。(不过应该有更好的方式来实现)
用户画完后,触发Paper的MouseUp事件。在此时将A的数据与B的数据进行合并,同时将A移除。用户看到的就是最新画好的图像了。
橡皮擦,可以看做是一种比较独特的画笔。记得以前有一种笔,笔迹是透明的,而且可以把笔迹经过的地方的其他的颜色抹去,很类似这里的橡皮擦。
其实橡皮擦只是在draw的模式上有所区别。
亦如上图。
默认情况下,A的blendMode为BlendMode.Normal,在两层数据合并时的模式也是一样。
用户选择橡皮擦之后, A的BlendMode则被定义为BlendMode.LAYER,在两层数据合并时的模式改为BlendMode. ERASE。
这样用户用橡皮擦“画笔”画过的地方就变成了透明的。
在最开始设计的时候,思维形成了定式。从表面上看每次撤销和重做,都是回滚或者回退用户操作的某一笔。程序需要操作的是某一笔的信息—笔迹的坐标记录。
实际实现中发现,这样不靠谱。因为一笔可以无限长,这样就会导致撤销和重做会都抖需要遍历一个长度不可控的数组。即便是使用Vector,时间和空间的复杂度都是单位计量*N。
通过参考其他的绘画应用,使用了以下方式来实现撤销和重做,使得时间和空间都变得可控:
为撤销和重做自定义两个固定长度的“队列”。同时插入B的图像初始数据对象(BitmapData)入撤销队列。
在每次A与B的数据合并之后(参考图3),将B的数据对象的副本插入撤销队列。
当用户撤销时将队尾的数据对象弹出并插入重做队列中,再将此时的顶部数据对象显示在B中。
当用户重做时将重做队尾数据对象弹出并插入撤销队列中,并将对象显示在B中。
具体如图 4.1 – 4.3所示,
图4.1 画好的图像压入撤销栈
从图中可以看出,撤销队列中始终是需要有数据对象的,而且当前队尾的数据对象和当前显示的图像数据对象一致。当确定了撤销的步数为N的之后,那么撤销队列的最大值则为N+1,而重做队列的最大值则为N。
撤销队列满了时,将队头的数据对象弹出并销毁。