2014 年 3 月 4 日
教程:高级 TableView 技巧
在本教程中,我们将更深入地研究表格视图(或“列表视图”),它基于移动设备的范例,即信息以一系列单列行的形式呈现给用户。在 iOS 中,像邮件、时钟和联系人这样的应用程序是明显的表格视图,但它们也被用于无数其他应用程序的菜单等。
在 Corona 中,widget.newTableView() 是一个在 OpenGL 中呈现的小部件,它旨在模拟原生表格视图的一些更常见的功能。在底层,Corona 表格视图构建在 widget.newScrollView() 之上,它处理向上/向下滚动、类似弹簧的行为和基于动量的滚动。
设置
在本教程示例中,我们将创建一个表格视图,该视图跨越屏幕的整个宽度,位于上方的标题栏下方,并向下延伸到底部的 widget.newTabView() 控制器。这是基本代码
1 2 3 4 5 6 7 8 9 10 |
local navBarHeight = 60 local tabBarHeight = 50 local myList = widget.newTableView { top = navBarHeight, width = display.contentWidth, height = display.contentHeight - navBarHeight - tabBarHeight, onRowRender = onRowRender, onRowTouch = onRowTouch, listener = scrollListener } |
这将创建一个指定大小的“空”表格视图。现在,我们必须用行填充它。这是使用 tableView:insertRow() 方法完成的,该方法会在视图中插入一个空白的空行。为了在本示例中填充小部件,我们将使用一个包含名称和电话号码的项表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
local myData = {} myData[1] = { name="Fred", phone="555-555-1234" } myData[2] = { name="Barney", phone="555-555-1235" } myData[3] = { name="Wilma", phone="555-555-1236" } myData[4] = { name="Betty", phone="555-555-1237" } myData[5] = { name="Pebbles", phone="555-555-1238" } myData[6] = { name="BamBam", phone="555-555-1239" } myData[7] = { name="Dino", phone="555-555-1240" } for i = 1, #myData do myList:insertRow{ rowHeight = 60, isCategory = false, rowColor = { 1, 1, 1 }, lineColor = { 0.90, 0.90, 0.90 } } end |
即使使用此设置,此时也没有任何视觉渲染到表格视图中。在幕后,表格视图小部件会跟踪哪些行在屏幕上,并在需要时仅绘制这些行。这是通过我们提供并定义为表格视图构造函数中 onRowRender
参数的函数来完成的。当表格视图调用此函数时,我们会获得对相应行的句柄。
在 Corona 中,每个行实际上都是一个单独的 display.newGroup()。因此,在行渲染函数中,我们可以创建必要的自定义显示对象,并使用标准的 group:insert(object) 方法将每个对象插入到表格视图行中。
行渲染函数如何知道在每行中渲染什么?让我们检查一下行渲染函数的典型示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
local function onRowRender( event ) --设置通过事件表传递的本地化变量 local row = event.row local id = row.index row.bg = display.newRect( 0, 0, display.contentWidth, 60 ) row.bg.anchorX = 0 row.bg.anchorY = 0 row.bg:setFillColor( 1, 1, 1 ) row:insert( row.bg ) row.nameText = display.newText( myData[id].name, 12, 0, native.systemFontBold, 18 ) row.nameText.anchorX = 0 row.nameText.anchorY = 0.5 row.nameText:setFillColor( 0 ) row.nameText.y = 20 row.nameText.x = 42 row.phoneText = display.newText( myData[id].phone, 12, 0, native.systemFont, 18 ) row.phoneText.anchorX = 0 row.phoneText.anchorY = 0.5 row.phoneText:setFillColor( 0.5 ) row.phoneText.y = 40 row.phoneText.x = 42 row.rightArrow = display.newImageRect( "rightarrow.png", 15 , 40, 40 ) row.rightArrow.x = display.contentWidth - 20 row.rightArrow.y = row.height / 2 row:insert( row.nameText ) row:insert( row.phoneText ) row:insert( row.rightArrow ) return true end |
这里的“魔法”在于我们如何使用行的 ID 号来映射到 myData
数据集。然而,虽然这对于简单情况效果很好,但对于包含“类别”行或用于间隔目的的空行的更高级的表格视图则不起作用。
引入可传递参数
此功能已经存在一段时间,但尚未过多讨论。本质上,它提供了一种编程方式来关联行和特定数据,而不依赖于 ID 号。让我们使用相同的示例,但改为传入参数
1 2 3 4 5 6 7 8 9 10 11 12 |
for i = 1, #myData do myList:insertRow{ rowHeight = 60, isCategory = false, rowColor = { 1, 1, 1 }, lineColor = { 0.90, 0.90, 0.90 }, params = { name = myData[i].name, phone = myData[i].phone } } end |
现在,我们可以使用 params
表插入包含数据的行。 没有数据的行可以不带参数插入(例如,类别行)。 然后,在行渲染函数中,我们可以简单地测试它是否是类别行,并相应地渲染它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
local function onRowRender( event ) --设置通过事件表传递的本地化变量 local row = event.row local id = row.index local params = event.row.params row.bg = display.newRect( 0, 0, display.contentWidth, 60 ) row.bg.anchorX = 0 row.bg.anchorY = 0 row.bg:setFillColor( 1, 1, 1 ) row:insert( row.bg ) if ( event.row.params ) then row.nameText = display.newText( params.name, 12, 0, native.systemFontBold, 18 ) row.nameText.anchorX = 0 row.nameText.anchorY = 0.5 row.nameText:setFillColor( 0 ) row.nameText.y = 20 row.nameText.x = 42 row.phoneText = display.newText( params.phone, 12, 0, native.systemFont, 18 ) row.phoneText.anchorX = 0 row.phoneText.anchorY = 0.5 row.phoneText:setFillColor( 0.5 ) row.phoneText.y = 40 row.phoneText.x = 42 row.rightArrow = display.newImageRect( "rightarrow.png", 15 , 40, 40 ) row.rightArrow.x = display.contentWidth - 20 row.rightArrow.y = row.height / 2 row:insert( row.nameText ) row:insert( row.phoneText ) row:insert( row.rightArrow ) end return true end |
现在行渲染函数不需要知道关于我们数据结构的任何信息。这有助于支持模型-视图-控制器 (MVC) 的概念。通过这种方法,我们可以专注于我们的视图,并知道要获取哪些数据,而无需了解模型是如何跟踪数据的。在大多数情况下,这是一种非常好的填充表格视图行的方法,它应该可以更容易地可视化数据和相关的行。
在状态栏点击时重新加载表格视图
这是一个经常被请求的功能,它很容易用少量代码实现。数据的拉取方式很大程度上取决于应用程序的设计,您有责任创建一个重载函数,该函数将作为点击处理程序的一部分被调用。在大多数情况下,常见的功能将是:
- 检索更新的数据(例如,新的推文或 RSS 源)。
- 刷新(清除)现有表格视图的数据。
- 将新数据重新插入表格视图。
要像步骤#2 中提到的那样刷新/清空行,我们可以调用这个函数
1 |
myList:deleteAllRows() |
现在,要构建“状态栏点击”功能,我们可以在状态栏所在的位置创建一个透明矩形,并在其上添加一个 "tap"
事件处理程序。这个点击函数,顾名思义,将调用自定义的重新加载函数。就这么简单!
1 2 3 4 |
local reloadBar = display.newRect( display.contentCenterX, display.topStatusBarContentHeight*0.5, display.contentWidth, display.topStatusBarContentHeight ) reloadBar.isVisible = false reloadBar.isHitTestable = true reloadBar:addEventListener( "tap", reloadTable ) |
这里有一个重要的方面需要注意:由于矩形对象是不可见的 (.isVisible = false
),我们必须将 .isHitTestable
设置为 true,以便它可以对点击/触摸事件做出反应。
实现“弹簧重载”
表格视图“弹簧重载”是一种向下拉动列表使其重新加载/刷新的技术。这不像状态栏点击方法那样容易实现,但让我们来逐步了解一下。
作为弹簧重载的用户界面和用户体验 (UI/UX) 的一部分,需要考虑一些先决条件。当用户向下拉动表格视图时,它会显示应用程序的“背景”。在某些情况下,您可能希望在表格视图后面放置一些东西,例如一个纯灰色块。此外,大多数弹簧重载系统都有一个动画图形,当用户下拉足够远时会显示该图形,提示他们松开触摸。在 iOS 中,这通常是一个微调器,您可以考虑通过 Corona spinner 小部件来模仿它。但最终,如何处理应用程序的 UI/UX 由您决定。
在实现弹簧重载之前,让我们进一步研究表格视图事件系统。这是我们再次使用的表格视图构造函数示例:
1 2 3 4 5 6 7 8 |
local myList = widget.newTableView { top = navBarHeight, width = display.contentWidth, height = display.contentHeight - navBarHeight - tabBarHeight, onRowRender = onRowRender, onRowTouch = onRowTouch, listener = scrollListener } |
正如我们目前所见,onRowRender
函数处理实际表格行的渲染和显示。在此之下,onRowTouch
函数处理单个行上的 "tap"
和 "touch"
事件(有关用法示例,请参阅 widget.newTableView() 的文档)。分配给 listener
属性的最后一个函数是监听表格上的滚动相关事件的函数——这些事件在弹簧重载中特别受关注。使用这个监听器,我们将能够检测表格视图何时开始移动,何时仍在动量下移动,以及何时停止移动。当表格视图达到其滚动限制时,我们还会得到一个 event.phase = nil
,正是这个事件阶段表明我们应该重新加载表格视图。让我们看看示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
local springStart = 0 local needToReload = false local function scrollListener( event ) if ( event.phase == "began" ) then springStart = event.target.parent.parent:getContentPosition() needToReload = false elseif ( event.phase == "moved" ) then if ( event.target.parent.parent:getContentPosition() > springStart + 60 ) then needToReload = true end elseif ( event.limitReached == true and event.phase == nil and event.direction == "down" and needToReload == true ) then --print( "正在重新加载表格!" ) needToReload = false reloadTable() end return true end |
请注意这些条件检查是如何用于弹簧重新加载中的不同流程的
- 在
"began"
阶段,我们存储表格视图的当前内容位置。这样,当用户下拉列表时,如果用户只是稍微下拉,就不会触发意外的重新加载。在此阶段,我们还将标志变量needToReload
设置为false
。 如果您希望开始一个动画来指示他们正在开始弹簧重新加载,这将是一个启动它的好时机。 - 在
"moved"
阶段,如果表格视图的位置变化量比起始位置多出设定量,在本例中为 60 像素,我们将needToReload
标志设置为true
。 由于用户此时可能仍在拖动表格视图,因此我们不会立即触发重新加载。 如果您有一个图形或动画向用户显示现在是松开手或他们已经下拉足够的时机,您也应该在这里更改图形/动画。 - 在
"ended"
阶段,您可以停止在"began"
或"moved"
阶段启动的任何动画。 - 在最后的条件检查中,我们不希望仅在移动结束时才重新加载。 如果这样做,表格视图会在表格视图上的滚动停止时每次都重新加载。相反,我们将检查一系列条件,从
event.limitReached
事件等于true
开始。 此事件没有阶段(nil
),因此我们还检查是否缺少event.phase
。 接下来,我们检查移动方向是否为"down"
(通常,重新加载不会在向上滚动后发生),并且我们还确认needToReload
标志为true
(即用户已将视图下拉了足够的距离)。 如果满足所有这些条件,我们将调用reloadTable()
函数以启动表格视图重新加载。
总之
希望本教程向您展示了如何向表格视图小部件添加一些创造性和有用的功能。 凭借使用参数将数据传递给行渲染函数的能力,再加上这些易于实现的重新加载功能,您现在可以增强表格视图的功能。
Carlos
发布于 04 月 04 日 17:25感谢 Rob 又一篇有用且精彩的教程!
JCH_APPLE
发布于 03 月 05 日 00:11+ 1 赞赏本教程的卓越品质。
列表经常使用远程图像。在这种情况下,必须在加载图像后调用 onRowRender。
有没有一种简单的方法可以显示带有微调器的图像占位符,并在加载一个图像后更新行(而不是等待图像加载)。
Rob Miracle
发布于 03 月 05 日 16:21我已经在表格视图中完成了很多加载远程图像的操作。 这很棘手,但您可以设置一个占位符,然后调用 display.loadRemoteImage(),并在其回调中删除占位符并在其位置绘制图像。 将所有代码放在 onRowRender() 函数中以进行范围界定会很有帮助。
JCH_APPLE
发布于 03 月 05 日 23:37谢谢 Rob,我会尝试这种方法。 直到今天,我才加载图像,并且在加载完成后插入行。
它可以工作,但可能会导致行没有按照正确的顺序显示(如果产品 2 的图像比产品 1 小,则加载速度更快,并且会在产品 1 之前显示)。
您的想法将避免这种行为。
Rob Miracle
发布于 03 月 06 日 16:47我的做法是将下载与行本身绑定,而不是与它们插入的顺序绑定。 由于所有工作都在 onRowRender 中进行,因此您将占位符存储在行中
row.icon = dipslay.newImageRect(“placeholder.png”, 60, 60)
row:insert(icon)
因此,对象不仅与该行一起保存,而且还被插入到显示组中。 然后调用 display.loadRemoteImage()。 在侦听器中,如果您获取了图像,则删除 row.icon 并将该图像保存到 row.icon 并将其插入到组中。
M
发布于 03 月 06 日 11:03感谢您的撰写。 配置文件是否仍然需要设置为 320x480? 上次我在使用 640x960 配置的应用程序上尝试表格视图时,它不起作用。
Rob Miracle
发布于 03 月 06 日 16:50我不确定表格视图在 640x960 上无法工作的任何原因,只是滚动条不会放大。 但是还有其他小部件可以更好地将您的应用程序保持在 320x480。
罗伯
马克
发布于 03 月 26 日 19:15感谢 Rob 的教程。
我可以在哪里下载源代码?
此致,马克
Rob Miracle
发布于 03 月 27 日 16:32您可以从我们的 github 存储库中获取 Business App 示例,并将以上内容添加到其中。 这就是我用来测试这些功能的。 我现在只是没有处于可推送状态。
戴夫·巴克斯特
发布于 04 月 19 日 02:55只是将其添加到我的应用程序中,并且底部的第二个 onRowRender 代码块有问题,它被截断了,并且有一个词
在其中。
顺便说一下,效果很好,没有实现弹簧效果,我在按钮上刷新,但是正在使用参数并在 XML 文档中读取并显示它,没有问题。
戴夫
Rob Miracle
发布于 04 月 19 日 08:35已修复。 感谢您发现了它。
阿卜杜勒阿齐兹
发布于 05 月 11 日 22:00感谢 Rob 的精彩教程。
在弹簧重新加载中,通常在某些应用程序(例如 Twitter)中,当用户下拉列表时,刷新会开始,但我注意到当用户下拉列表时,内容仍然停留在它到达的最后一个位置。 当刷新结束时,内容会返回到初始高度。
您知道如何实现这一目标吗?
谢谢
阿卜杜勒
Rob Miracle
发布于 05 月 12 日 08:55您可以将刷新请求放在计时器内,以便主函数立即返回,然后在计时器函数完成时,它将更新。 计时器不必很长,比如几毫秒。 想法是让您不必等待它完成。
罗伯
艾萨克
发布于 12 月 03 日 22:19您好,Rob,我正在研究 Abdulaziz 所问的问题。 那么,您实际上将请求计时器放在哪里,以便在您重新加载并从服务器获取响应时,应用程序将显示动画或内容保持在底部。
普拉文
发布于 05 月 30 日 22:16感谢您的另一篇精彩教程。
我充实了代码并对其进行了一些调整,添加了下拉/刷新动画。 我使用 row-1 来保存刷新动画。
https://github.com/pspk/tableview
我还将其添加到代码交换中。
Pathmazing Dev
发布于 06 月 25 日 23:49您好,
有没有办法禁用表格视图蒙版?
Rob Miracle
发布于 06 月 26 日 15:47许多人希望拥有适合多种不同设备的表格视图,而无需使用数十个蒙版文件并试图找出要使用哪个蒙版文件。 为此,我们必须使用容器,并且它们会进行自我蒙版。 您始终可以从我们的 github.com 存储库下载开源版本,并将其从容器更改回组,并提供您自己的蒙版系统。
罗伯
罗迪
发布于 07 月 18 日 07:56您好,Rob
是否可以在 Composer 场景中的状态栏顶部添加一个不可见的按钮?
罗迪
Rob Miracle
发布于 07 月 18 日 14:33实际上不能。 状态栏会吸收对其的任何触摸。
罗伯
罗迪
发布于 07 月 19 日 07:41谢谢 Rob,非常感谢您的及时回复。
因此,从您上面的文章来看… “现在,为了构建“状态栏点击”功能,我们可以创建一个状态栏所在位置的透明矩形,并在其上添加一个“点击”事件处理程序。” 该透明矩形是否必须在 main.lua 中创建?
干杯
罗迪
Rob Miracle
发布于 07 月 19 日 07:44它在模拟器中有效,并且如果您关闭了设备状态栏。 虽然我们无法击败 iOS 状态栏的存在,但如果隐藏状态栏,您仍然可以提供该功能(即点击导航栏)。
罗伯
罗迪
发布于 07 月 20 日 04:18谢谢,Rob! 我会玩一下,看看什么效果最好。
非常感谢您的回复! 🙂
罗迪
马克
发布于 08 月 19 日 19:55有没有办法在 onTableRowTouch 中获取事件的 x、y 位置? 我想将用户不小心移动一两个像素的按压/释放事件仍然视为点击。 否则,您必须非常精确地触摸才能使 event.phase 成为点击。
Rob Miracle
发布于 08 月 20 日 17:09据我所知,如果没有从 github 获取开源版本并进行更新以实现此目的,则无法做到这一点。 这些小部件是纯 Lua,我们已经提供了代码,供想要拥有此类自定义功能的人使用。
罗伯
维韦克
发布于 08 月 31 日 22:14罗伯,
是否有更简单的方法来取消 `loadRemoteImage` 方法发出的请求?特别是,我试图在调用 `scene:hide()` 时阻止监听器显示已下载的图像。
– 我尝试使用一个布尔值,在调用 `scene:hide()` 时将其设置为 false,但图像仍然显示在默认的 x、y 位置(屏幕左上角)。
– 我尝试在监听器函数中设置 `network.cancel(event.requestId)`,但它给我一个段错误 11,并且 Corona 模拟器崩溃了。
我在想,如果可以返回 `display.loadRemoteImage` 内部 `network.download` 调用返回的句柄,岂不是会方便得多。这样就可以将这些句柄保存在一个表中,并在需要时取消。
维韦克
Rob Miracle
发布于 18:37,9 月 1 日`display.loadRemoteImage` 只是 `network.download` 的一个便捷方法。但是,它不处理取消下载。如果您需要能够取消下载,则应使用 `network.download()`,然后使用 `display.newImage()` 加载图像。
罗伯
Alberto
发布于 06:33,10 月 9 日嗨!
我正在尝试“引入可传递参数”部分。
当我到达代码
“myList:insertRow{
rowHeight = 60,
isCategory = false ” …
我收到错误“bad argument #-2 to ‘insert’ (proxy expected got nil) “。
我正在 MacOSX 10.8 上使用最新的 CoronaSDK。
我只是复制示例,但我放了自己的数据。
有什么想法吗?谢谢
Rob Miracle
发布于 20:20,10 月 9 日请在论坛中提问,并在 `[code]` 和 `[/code]` 括号内发布更多代码。
Kevin Thompson
发布于 20:40,12 月 16 日代码运行良好……但是当触摸表格视图中未填充行的区域时,我遇到了问题。如果触摸该区域,它会返回一个错误:尝试调用方法 'getContentPosition' (a nil value)。
有什么办法可以解决这个问题吗?
Kevin Thompson
发布于 20:41,12 月 16 日抱歉,我指的是下拉刷新代码。
Rob Miracle
发布于 16:44,12 月 17 日您可以在论坛中提问,并将您正在使用的代码放在 `[code]` 和 `[/code]` 标签内吗?
论坛评论对代码的格式化效果不佳。
罗伯
Kevin Thompson
发布于 18:19,12 月 17 日[code]
local widget = require(‘widget’)
local springStart = 0
local needToReload = false
local function scrollListener( event )
if ( event.phase == “began” ) then
springStart = event.target.parent.parent:getContentPosition()
needToReload = false
elseif ( event.phase == “moved” ) then
if springStart ~= nil then
if ( event.target.parent.parent:getContentPosition() > springStart + 60 ) then
needToReload = true
end
end
elseif ( event.limitReached == true and event.phase == nil and event.direction == “down” and needToReload == true ) then
needToReload = false
reloadTable = true
end
return true
end
tableView = widget.newTableView
{
x = _W*.6,
y = _H*.5,
height = 300,
width = 200,
onRowRender = onRowRender,
onRowTouch = onRowTouch,
listener = scrollListener,
}
[/code]
Rob Miracle
发布于 18:21,12 月 17 日我的意思是,去:http://forums.coronalabs.com在那里开始一个新主题。
不要在这里发布代码。
Amir
发布于 06:46,12 月 18 日我正在使用一个导致另一个表格视图的表格视图。它在模拟器上运行良好,但在设备上却搞砸了。有人遇到过同样的问题吗?
Rob Miracle
发布于 17:10,12 月 18 日请在论坛中寻求帮助。
罗伯
Rui Pereira
发布于 04:15,1 月 4 日表格显示了,但没有行文本,只有空白行。
Rob Miracle
发布于 07:21,1 月 4 日请在论坛中提出这个问题。我们需要查看代码,而且代码在博客评论中效果不佳。您需要发布您的 `onRowRender()` 函数,创建表格视图的代码以及将行插入表格的代码。请在每个代码块周围使用 `[code]` 和 `[/code]` 标签,或者使用论坛帖子编辑器中格式栏中的 `<>` 按钮。
Krystian
发布于 14:03,4 月 4 日对于任何想要使用 top/left 值而不是 x/y 值的人。
如果您计划在分辨率比 `config.lua` 设置高得多的设备上使用表格视图,您最终会将滚动视图向上和向左移动 1 个或多个像素。这是由于小部件库中的一个错误,其中 top/left 值被计算为 x/y,并且在计算结束时有人决定对这些值进行 `math.floor` 操作😉
David
发布于 17:12,7 月 6 日我正在使用表格视图小部件,并且遇到了一个障碍。好吧,实际上是两个,但我将首先解决第一个,因为这将解决我的第二个问题。
我以宽度为 375 的纵向模式开始。(这个数字并不重要,因为它会因设备而异。)表格渲染,没问题。然后我旋转设备,但是表格宽度仍然是 375。我如何重新渲染表格视图,使其现在跨越横向宽度?
我为标签栏使用以下 `onResize` 方法,它像冠军一样工作。
`display.remove(tabBar)` — 清理旧的标签栏
`tabBar = nil` — 清理旧的标签栏
`generateTabBar = makeTabBar ()` — 为新尺寸创建新的标签栏
不幸的是,类似的代码似乎不适用于表格视图。
有什么想法吗?谢谢!
David
Rob Miracle
发布于 14:13,7 月 11 日当您检测到设备旋转时,销毁并重新创建表格
Sanghyun Lee
发布于 11:17,12 月 29 日是否可以创建如下所示的表格视图?
一般来说,表格视图看起来像这样
————————————–
行 1
————————————–
行 2
————————————–
行 3
————————————–
但我想要做的是如下所示。
—————————-
行 1 l 行 2
—————————-
行 3 l 行 4
—————————-
这有可能吗??