cocos2d-x 编辑器开发笔记(二)

这几天CocosCreator发布了!发布了!发布了!初步体验了一下,感觉还真是不错,起码UI的感觉和Unity不相上下,不过实际可用否还有待时间的检验。当时有那么一瞬间重新思考了一下我为什么要自己动手写个编辑器?而且进展的是如此龟速。嗯...mmmm,应该还是和最初的决定是一样的,处于学习和打法时间的目的。一边写写编辑器,一边思考,一边学习,即使是一些老生常谈的东西,但温故而知新,真不是假的。

我的废话总是那么的多……这次的主题是搭建一个UserCustomLayouts方案,就是Unity的Layouts自由布局,我照着Unity这个葫芦画了个瓢,实现的代码在Git可以找到(https://github.com/firegames/UserCustomLayouts)。

来介绍一下实现的思路,首先,先说明一下几个我定义的概念,在拖动一个Layout的过程中,鼠标点下时选中的Layout,我称之为Sender,在鼠标拖动过程中通过的Layout,我称之为Responser。这里,对Layout的布局有一个限定,Layout的frame不会有交集,所以,在单一时刻,Responser必然只有一个。为了处理Layout布局,我创建了一个Hanler,它的作用在之后会一一说明。一次Layout拖动过程从鼠标点下开始,到鼠标放开结束,整个流程的处理简单来看,如下图:

Sender接收到鼠标事件,然后将事件转化为LayoutDrag事件,交给Responser处理,根据鼠标事件和鼠标当时位置的不同,Responser会做出不同的处理,比如在条件符合时,通知Sender移除被拖动的Layout,同时将Layout放到新的位置。让我们一步步来解释一下整个过程。

第一步,Layout接收到鼠标事件,这一步非常简单,将鼠标事件直接转发到Hanler,交给它处理,在引入TabLayout之后,这里会有一点点小区别。

第二步,Hanlder处理Mouse事件。Hanlder的处理主要由2部分组成,一个是找到当时的Responser,另一个是将Drag事件通知到Responser。前一部分暂时先略过,稍后再讲,我们先将重点放在后一部分,Handler对Responser的事件通知使用Delegate的形式,可以看下ResponserDelegate的定义:

@protocol LayoutDragResponserDelegate

- (void)onLayoutDragIn; //有Layout被拖动到了Responser的frame内
- (void)onLayoutDragOut; //在Responser的frame内的Layout被拖出了frame范围
- (BOOL)onLayoutDragMove:(LayoutDragEvent*)event; //在Responser的frame内的Layout被拖动了
- (BOOL)onLayoutDragEndInside:(LayoutDragEvent*)event; //在Responser的frame内的Layout的拖动结束

@end

三种鼠标事件:MouseDown、MouseDrag、MouseUp,其中MouseDown不会触发Drag事件,MouseDrag会触发onLayoutDragIn、onLayoutDragOut、onLayoutDragMove,MouseUp会触发onLayoutDragEndInside。

第三步,Responser的最终处理。四种Drag事件中,Responser主要处理后两种事件,这两个事件无论哪个,都会先检查是否有可停靠的新位置,在没有找到可停靠的位置时就什么也不做,所以我们主要讨论找到新的停靠位置时的情况。首先,说下DragMove的处理,其实很简单,根据鼠标的当前位置检测到Sender有停靠的新位置时,通知DraggingPanel将新的停靠位置呈现给用户。这里说下DraggingPanel,在Handler发现有新的LayoutDrag事件开始时,会生成一个DraggingPanel,默认显示为Sender的快照,并且会跟随鼠标移动,在Responser找到新的停靠位置时,可以更改DraggingPanel的显示内容和位置(具体效果和Unity一样)。然后是DragEndInside的处理,简单来说,在找到新的位置之后,先将Layout从Hanlder的布局树中移走,然后将Layout根据新的显示位置重新放入布局树中。在移除时,Responser其实不是非常了解Sender要怎么处理被拖动的Layout,所以,Responser只是通知Sender,你被拖动的Layout被我接收了,这里就要引入一下SenderDelegate:

@protocol LayoutDragSenderDelegate

- (LayoutView*)layoutWillMove;

@optional
- (void)layoutDragDidBegin;
- (void)layoutDragDidDragging;
- (void)layoutDragDidEnd;
- (void)layoutDragDidCancel;

@end

和ResponserDelegate有些像,其中非常重要的就是layoutWillMove,这个方法要求Sender将被拖动的layout的移除工作处理好,并且将需要移到新位置的view返回,其实,之所以需要这样操作,主要就是为了满足TabLayout的需求。

了解了事件处理的流程之后,来关注一下详细的实现,先看一下主要的类结构:

LayoutHanlder就是上文提到的Handler,LayoutView即是Sender,同时也是Responser,子类中的RootLayoutView比较特殊,主要负责实现Layout的resize事件。之前没有提及的LayoutNode则是隐藏在LayoutHandler中负责处理Layout树的类,具体的功能和普通的树结构相差不大,只是增加了一些子节点排列的方法和一些额外的规则,这里稍微提及一下LayoutHanlder对树的根节点的存放方式,因为支持多个windows,每个window为一棵独立的树,所以在鼠标拖动时,会在处理drag事件前插入一步寻找当前优先响应的window对应的根节点。

然后,让我们把重点放到TabLayoutView,其实再具体实现之前,我一直在考虑是否需要抽象出LayoutView,因为所有用户实际中用到的全是TabLayoutView,而上文提到的LayoutView,其实对用户来说是完全不存在的。参照Unity的Layout操作,每个Layout都是一个TabView,Layout的修改都是通过拖动Tab来达到的。回到上文的设计中,因为每一个mouse的操作,都是由LayoutView发送到handler,所以LayoutView的子类完全有能力决定什么样的mouse事件需要发送,在TabLayoutView中,只有mouseDown事件发生在某个Tab的范围内,TabLayoutView才会讲mouse事件转发。还有一个非常重要的部分,就是在layout移除时,普通的Layout可能就是将自己移除,但是TabLayout不同,它不能将自己移除,因为你拖动的只是它其中的一个Tab。这个时候,layoutWillMove回调就起作用了,只有sender自己知道该怎么移除被拖动的layout,TabLayout可以在这个回调中仅仅是移除自己其中的一个Tab,只有在所有的Tab都被移走的时候,它才会通知Handler移除自己。

来看一下从mouse drag事件处理的完整流程图:

咳咳,其实这是我的草稿图,一些细节的问题不要太在意。focusedNode就是当前鼠标位置的LayoutNode,一个node对应了一个LayoutView。

再看一下mouse up的完整流程:

这里主要介绍了一下实现Layouts的核心思路,如果感兴趣的话,可以通过代码详细了解。代码里还是有不少花了心思的设计,当然之后还会继续更新这篇文章。

 

最后,有几个问题其实我也一直不太确定,一个就是之前提到过的是否需要抽象出一个LayoutView,虽然感觉抽象出来比较好,但是在实际实现中的确发现这一层抽象比较费力,而且没有带来多少实际的便利。还有一个问题是是否要单独搞一套LayoutNode,其实完全可以将node的功能放在LayoutView中,最初的设想是将layout树隐藏在handler中处理,这样对外使用时只需要重写或继承LayoutView部分,但是RootLayoutView的出现,的确有违当初的设想,仍然不是很确定是否值得将LayoutNode单独拿出来做一套处理。 

posted @ 2016-01-23 22:11  JasonZXX  阅读(378)  评论(0编辑  收藏  举报