你是不是奇怪为什么 Node、Sprite、Layer等原生节点不能响应触摸事件,而 ImageView、Layout等GUI 控件就可以?
恩,事实上我也不知道这份设定的原因。
而实际上,我们经常需要原生节点可以响应触摸,为了实现目的,通常我们会这么做:在节点上盖上一层UILayout,让UIlayout去处理触摸事件。这么做的好处是处理简单;坏处是步骤繁琐,而且还需要管理一个无关紧要的节点。
不!能!忍!那就只好自己来写一套了。
我们看源码知道,GUI 控件是这么响应触摸事件的:
void Widget::setTouchEnabled(bool enable)
{
if (enable == _touchEnabled)
{
return;
}
_touchEnabled = enable;
if (_touchEnabled)
{
_touchListener = EventListenerTouchOneByOne::create();
CC_SAFE_RETAIN(_touchListener);
_touchListener->setSwallowTouches(true);
_touchListener->onTouchBegan = CC_CALLBACK_2(Widget::onTouchBegan, this);
_touchListener->onTouchMoved = CC_CALLBACK_2(Widget::onTouchMoved, this);
_touchListener->onTouchEnded = CC_CALLBACK_2(Widget::onTouchEnded, this);
_touchListener->onTouchCancelled = CC_CALLBACK_2(Widget::onTouchCancelled, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(_touchListener, this);
}
else
{
_eventDispatcher->removeEventListener(_touchListener);
CC_SAFE_RELEASE_NULL(_touchListener);
}
}
从代码上看,给加上TouchOneByOne的事件监听就好了,是不是很简单?
好,那我们就来模仿一下。
为了秉持尽量不改动源码的宗旨,我们在lua层做修改。
首先,我们需要定义一些会经常用到的基础的参数:_touchBegan
、_touchEnded
、_touchMoved
分别代表触摸开始、触摸结束和触摸移动事件回调,_isTouchEnabled
是触摸使能标识,_touchListener
是触摸事件监听句柄 :
local TouchNode = cc.Node
TouchNode._touchBegan = nil
TouchNode._touchEnded = nil
TouchNode._touchMoved = nil
TouchNode._isTouchEnabled = nil
TouchNode._touchListener = nil
接着,我们开始添加触摸事件监听:
function TouchNode:enableTouchEvent()
self:setTouchEnabled(true)
self:onListenTouchEvent()
end
function TouchNode:disableTouchEvent()
self:setTouchEnabled(false)
end
function TouchNode:isTouchEnabled()
return self._isTouchEnabled
end
function TouchNode:setTouchEnabled(var)
if var == self._isTouchEnabled then
return
end
self._isTouchEnabled = var and true or false
return self._isTouchEnabled and self:onListenTouchEvent() or self:unListenTouchEvent()
end
function TouchNode:onListenTouchBegan(callback)
if type(callback) == 'function' then
self._touchBegan = callback
end
end
function TouchNode:onListenTouchEnded(callback)
if type(callback) == 'function' then
self._touchEnded = callback
end
end
function TouchNode:onListenTouchMoved(callback)
if type(callback) == 'function' then
self._touchMoved = callback
end
end
-- 监听触摸事件响应
function TouchNode:onListenTouchEvent()
local function onTouchBegan(touch, event)
local isFocus = isTouchFocusNode(touch, self)
if isFocus then
if self._touchBegan then
print('onListenTouchBegan')
self._touchBegan(self, ccui.TouchEventType.began)
end
end
return isFocus
end
local function onTouchEnded(touch, event)
local isFocus = isTouchFocusNode(touch, self)
if isFocus then
if self._touchEnded then
print('onListenTouchEnded')
self._touchEnded(self, ccui.TouchEventType.ended)
end
end
end
local function onTouchMoved(touch, event)
if self._touchMoved then
print('onListenTouchMoved')
self._touchMoved(self, ccui.TouchEventType.moved)
end
end
local listener = cc.EventListenerTouchOneByOne:create()
listener:registerScriptHandler(onTouchBegan, cc.Handler.EVENT_TOUCH_BEGAN)
listener:registerScriptHandler(onTouchEnded, cc.Handler.EVENT_TOUCH_ENDED)
listener:registerScriptHandler(onTouchMoved, cc.Handler.EVENT_TOUCH_MOVED)
self:getEventDispatcher():addEventListenerWithSceneGraphPriority(listener, self)
self._touchListener = listener
end
-- 设置是否吞噬响应
function TouchNode:setSwallowTouches(var)
if not self._touchListener then return end
var = var and true or false
self._touchListener:setSwallowTouches(false)
end
-- 取消监听
function TouchNode:unListenTouchEvent()
if not self._touchListener then return end
self:getEventDispatcher():removeEventListener(self._touchListener)
self._touchListener = nil
end
注意,isTouchFocusNode
还没有实现,你可能奇怪这是什么鬼?从字面上看,这是一个判断触摸点是否在节点上的方法。那为什么需要这个判断?因为如果不判断,触摸点满屏幕都是,我们无法确定触摸点是否被节点正确接收,因此这步判断是非常有必要的。
最后,我们就来实现isTouchFocusNode
:
function isTouchFocusNode(touch, node)
local touchP = touch:getLocation()
local bound = node:getBoundingBox()
local point = node:convertTouchToNodeSpaceAR(touch)
local anchor = node:getAnchorPoint()
if (point.x >= -bound.width * anchor.x) and (point.x <= bound.width * (1-anchor.x)) and
(point.y >= -bound.height * anchor.y) and (point.y <= bound.height * (1-anchor.y)) then
print('Focus ...')
return true
end
return false
end
注意:由于有些Node的BoundingBox为0,因此并不能接收到触摸事件,这时候需要根据具体情况调整节点的ContentSize。
PS:
源码中确定控件的触摸响应区域的具体实现与以上确定节点的实现是不一样的,有兴趣的可以看看 UIWidget的hitTest
方法。