上篇文章说到使用 TableView 可以大幅提升界面的创建速度, 这篇文章我们来看看如何在 quick 中使用它.

一. 基本用法

1. 创建对象

首先, 创建一个 TableView 对象:

1
local view = cc.TableView:create(cc.size(480,320))

传入的那个 size 是 viewsize, 即可视区域的尺寸, 后期也可以通过 setViewSize 来调节.

2. 设置填充顺序

下面设置填充顺序, 默认是从下往上填充, 我们习惯了使用从上往下填充, 所以需要修改下:

1
view:setVerticalFillOrder(cc.TABLEVIEW_FILL_TOPDOWN)

其可选值如下:

1
2
cc.TABLEVIEW_FILL_TOPDOWN = 0  -- 从上自下
cc.TABLEVIEW_FILL_BOTTOMUP = 1 -- 从下自上

这个值最终会影响所有 cell 的顺序, 具体些是第 0 个元素在最上面还是在最下面.

3. 注册事件监听

TableView 提供了好多事件, 具体作用如下:

1
2
3
4
5
6
7
8
9
cc.SCROLLVIEW_SCRIPT_SCROLL = 0
cc.SCROLLVIEW_SCRIPT_ZOOM = 1
cc.TABLECELL_TOUCHED = 2
cc.TABLECELL_HIGH_LIGHT = 3
cc.TABLECELL_UNHIGH_LIGHT = 4
cc.TABLECELL_WILL_RECYCLE = 5
cc.TABLECELL_SIZE_FOR_INDEX = 6 -- 获取节点尺寸的回调
cc.TABLECELL_SIZE_AT_INDEX = 7 -- 获取对应位置节点的回调
cc.NUMBER_OF_CELLS_IN_TABLEVIEW = 8 -- 获取节点数量的回调

但其实真正有意义的就三个, 6,7,8. 其他的根本不会回调, 注册了也没有什么用. 注册监听使用 registerScriptHandler 函数.

1). 注册节点数量回调

1
2
3
view:registerScriptHandler(function()
return 10 -- 假如有10个节点
end, cc.NUMBER_OF_CELLS_IN_TABLEVIEW)

返回节点数量.

2). 注册获取节点尺寸的回调

若是每个节点一致, 则可以返回一个固定尺寸; 若不一致, 可以根据 idx 去取出对应节点的尺寸.

1
2
3
4
5
6
7
8
9
10
view:registerScriptHandler(function(table, idx)
-- 默认尺寸
local size = self.defaultSize
local cell = view:cellAtIndex(idx)
if cell then
-- cell.view 属性是在线面创建 cell 时赋值的.
size = cell.view:getBoundingBox()
end
return size.height, size.width
end, cc.TABLECELL_SIZE_FOR_INDEX)

注意, 这里如果是纵向滚动的话,返回顺序是高,宽; 横向滚动的话则返回宽,高.

3). 获取对应位置节点的回调

这个回调的名称和上面那个很像, 有时候会分不清楚.

1
2
3
4
5
6
7
8
9
10
11
12
13
view:registerScriptHandler(function(table, idx)
local cell = table:dequeueCell()
if nil == cell then
cell = cc.TableViewCell:new()
cell.view = self.class.new()
:pos(self.class.designSize.width/2, self.class.designSize.height/2)
:addTo(cell)
end
-- 如果需要刷新的话, 可能需要自己去处理, 如不需要, 就可以不用下面的这个调用
cell.view:refresh()

return cell
end, cc.TABLECELL_SIZE_AT_INDEX)

3. 获取/新增/修改/删除节点

这些操作分别对应 cellAtIndex/updateCellAtIndex/insertCellAtIndex/removeCellAtIndex . 这些接口很好理解, 但就多数情况而言, 他们可能需要和 reloadData 配合使用.

好了, 以上就是基本用法了. 在使用时还有一些需要注意的细节:

  1. 不能使用 setNodeEventEnable, 因为和 Node 的 registerScriptHandler 有冲突.
  2. id 从 0 开始, 而大家在 lua 这边准备的数据多是以 1 开始, 这样可能需要 +1 和 -1, 需要细心一些.

二. 一些异常的解决方案

1. lua 出错导致 player 崩溃

TableView 回调 tableCellAtIndex 在lua这边的实现一旦出错, 就会在 c++ 那边收到一个 NULL 的 cell, 因为没有判空, 下面对 cell 的操作就会导致 Plaer 崩溃. 对应修改如下:

1
2
3
4
5
6
7
8
     cell = _dataSource->tableCellAtIndex(this, idx);
- this->_setIndexForCell(idx, cell);
- this->_addCellIfNecessary(cell);
+ if(cell)
+ {
+ this->_setIndexForCell(idx, cell);
+ this->_addCellIfNecessary(cell);
+ }

这时候, 不会崩溃了, 但是也看不到 lua 那边出的什么错误, 经过一番追踪, 定位到了 LuaStack::executeFunction 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        if (error)
{
if (traceCallback == 0)
{
CCLOG("[LUA ERROR] %s", lua_tostring(_state, - 1)); /* L: ... error */
lua_pop(_state, 1); // remove error message from stack
}
else /* L: ... G error */
{
+ CCLOG("[LUA ERROR] %s", lua_tostring(_state, - 2)); /* L: ... error */
lua_pop(_state, 2); // remove __G__TRACKBACK__ and error message from stack
}
return 0;
}

虽然出错了, 但是 traceCallback 的值并不等于 0, 所以没有进入输出错误的逻辑, 具体用意并不明白. 我的添加了带 + 号的哪一行, 输出了下就可以看到 lua 的错误了.

2. 滚动后导致节点上按钮触摸失效

大家使用时可能会遇到这样的问题, 节点上有一个利用 quick 触摸机制实现的按钮, 在 TableView 滚动后触摸事件都会失效, 按钮无法被点击.

这个实际上是 quick 触摸机制的一个bug, 复现是很容易的. 大家创建一个按钮并调用 retain, 然后将这个按钮添加到父节点上, 在某个时候将按钮从父节点上移除 (removeFromParent) 并再次添加(addChild)上到父节点. 这时候按钮还在, 但是触摸事件已经没有了.

究其原因是我们一般会在对象的 ctor 函数中 setTouchEnable, 然后 quick 在收到 cleanup 事件后移除了对象触摸事件, 具体逻辑大家可以看 Node:EventDispatcher 函数. 而一个节点从父节点上移除时恰好会发送 cleanup 事件.

对应到 TableView 中来, TableViewCell 为了做到复用在 dequeueCell 时会调用 retain 函数, 并且在移出屏幕时会被从 Container 上移除掉的. 这样 TableViewCell 的所有子节点都会收到对应的 cleanup 事件.

这个问题的解决方案恰好是我之前写的一篇文章, 为 quick-cocos2d-x 添加析构事件, 在这篇文章中, 已经修改为收到 destroy 事件后才去移除节点的触摸事件, 非常完美的解决了这个问题.

Update: 2016-04-24

3. 遮挡上层按钮触摸事件

经过这几天的使用, 我发现了一个十分严重的问题. 如下图所示, TableView 中的某个按钮拖出 ViewRect 范围后会不可见, 但如果其位置恰好在 TableView 外的另一个按钮范围内, 就会优先收到点击事件. 这就会引发十分奇怪的现象, TableView 外的按钮看得见点不着, TableView 内的按钮看不见却可以响应到, 十分影响体验.

因为有着以往 cocos 2.x 的悲惨经历, 我非常武断的认为这肯定是 TableView 的 bug, 开始着手阅读 TableView 的代码实现, 却一直未果. 然而团队中另外一位成员 @小齐同学 的意外发现, 让这个问题的谜底在无意中就被揭开了.

我们在项目中大量使用了 Quick-cocos2d-x 提供的一个控件 UIScrollView, 它是一个用 ClippingRegionNode 纯 lua 实现的 CCScrollView, 一直以来工作的十分良好. 但是在某一天 测试中, @齐少 意外的发现, 某个界面的 UIScrollView 出现了和 TableView 一模一样的问题, 导致上层按钮无法点击. 经过排查, 发现是因为 UIScrollView 的创建顺序被延后的原因, 如果一个按钮先于 UIScrollView 添加到父节点, 就会被 UIScrollView 中的按钮所屏蔽, 后添加则不会.

当 @齐少 告诉我这个结论后, 我立刻意识到, TableView 遇到的问题肯定也是这个原因, 查看代码后果然如此, TableView 是最后被创建的. 解决方案也十分简单, 将 TableView 外部按钮放到 TableView 之后去创建就可以啦.


恩, 以上就是我在使用 TableView 时遇到的所有问题了, 虽然解决了这些问题, 但是在使用上还是十分的繁琐. 若是在一两个界面上使用可能还可以接受, 但若是想大规模推广就很有些困难了, 代码会变得十分冗长.

在此基础上, 我们又对 TableView 进行了一次封装, 数据驱动, 使用时不用过分关注界面的逻辑, 中心更多的落在了数据的组织上, 真正的做到了 “开箱即用” , 等我们内部进行推广并稳定后可以再和大家分享下心得.