UI Scrollable List
From GiderosMobile
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
Here you will find various resources to help you create scrollable lists in Gideros Studio.
note:You may have to provide your own assets (fonts, gfx, …).
Scrollable List
Class
--====================================================================--
-- ListView widget for GiderosMobile
--====================================================================--
--
-- ListView.lua
-- Version 0.1 beta
-- Author: Lukas Gergel, http://pykaso.net
--
-- This library is free to use and modify. Use it for free!
--
-- TODO: add methods for programatic manipulating with ListView
--
-- USAGE
--
-- myList = ListView.new({
-- width=280,
-- height=390,
-- bgTexture = Texture.new("texture.png"),
-- bgColor = 0xffffff,
-- rowSnap = true, -- experimental feature
-- friction = 0.92 -- number lower then 1
-- data=data
-- })
ListView = Core.class(Shape)
function ListView:init(params)
-- properties
self.velocityPrevTime = 0
self.listHeight = 0
--data items (pre rendered sprites)
local data = params.data or {}
--configuration
self.cfg = {
--dimension
width = params.width or self.screenW,
height = params.height or self.screenH,
bgColor = params.bgColor or nil,
rowSnap = params.rowSnap or false,
friction = params.friction or 0.92,
callback = params.callback or function(row) return row end,
onScroll = params.onScroll or nil
}
local prevY, prevH, i, finalData = 0, 0, 0, {}
for i = 1, #data do
finalData[i] = {}
finalData[i].objectData = data[i]
finalData[i].height = data[i]:getHeight()
finalData[i].xInit = 0
finalData[i].yInit = prevY + prevH
prevY = finalData[i].yInit
prevH = finalData[i].height
self.listHeight = self.listHeight + finalData[i].height
end
if(self.cfg.bgColor ~= nil) then
self:setFillStyle(Shape.SOLID, self.cfg.bgColor, 1)
end
-- create defined shape
self:beginPath()
self:moveTo(0, 0)
self:lineTo(0, self.cfg.height)
self:lineTo(self.cfg.width, self.cfg.height)
self:lineTo(self.cfg.width, 0)
self:closePath()
self:endPath()
-- create defined shape
self.mask = Shape.new()
self.mask:beginPath()
self.mask:moveTo(0, 0)
self.mask:lineTo(0, self.cfg.height)
self.mask:lineTo(self.cfg.width, self.cfg.height)
self.mask:lineTo(self.cfg.width, 0)
self.mask:closePath()
self.mask:endPath()
self:addChild(self.mask)
self.listItems = Sprite.new()
self:addChild(self.listItems)
self.viewSize = self.cfg.height
self.itemData = finalData
finalData = nil
self:createRender()
end
function ListView:newListItem(id, data)
local thisItem = Sprite.new()
local callback = self.cfg.callback
local t = callback(data)
local tv = self
thisItem.id = id
thisItem:addChild(t)
thisItem:addEventListener(Event.TOUCHES_BEGIN, function(e)
if(self.mask:hitTestPoint(e.touch.x, e.touch.y)) then
tv:listItemTouch(e, "begin")
end
end)
thisItem:addEventListener(Event.TOUCHES_MOVE, function(e)
tv:listItemTouch(e,"move")
end)
thisItem:addEventListener(Event.TOUCHES_END, function(e)
tv:listItemTouch(e,"end")
end)
return thisItem
end
function ListView:listItemTouch(e, act)
local li = self.listItems
local top, height = self:getY(), self:getHeight()
if(act == "begin") then
delta, velocity, prevPos = 0, 0, 0
self.isFocus = true
startPos = e.touch.y
prevPos = e.touch.y
if self.tween then
self.tween:setPaused(true)
self.tween = nil
end
self:removeEventListener("enterFrame", self.scrollList, self)
self:addEventListener("enterFrame", self.trackVelocity, self)
elseif(self.isFocus) then
if(act == "move") then
local lastItem = li:getChildAt(li:getNumChildren())
delta = e.touch.y - prevPos
prevPos = e.touch.y
if (li:getChildAt(1).id == 1 and li:getY() > 0) or
(lastItem.id == #self.itemData and
(self.listHeight - lastItem:getY() - lastItem:getHeight()) <= 0) then
li:setY(math.floor(li:getY() + delta / 3))
else
li:setY(math.floor(li:getY() + delta))
end
self:updateRender()
end
if(act == "end") then
self:removeEventListener("enterFrame", self.trackVelocity, self)
self:addEventListener("enterFrame", self.scrollList, self)
self.isFocus = false
end
end
end
function ListView:scrollList(event)
if math.abs(velocity) < 0.3 then
velocity = 0
self:removeEventListener("enterFrame", self.scrollList, self)
-- experimental feature
if self.cfg.rowSnap then
local _me = self
local lastItem = self.listItems:getChildAt(self.listItems:getNumChildren())
local firstVisibleItem = self:getFirstVisibleRow()
if firstVisibleItem ~= nil and self.listItems:getY() < 0 and
self.listHeight - lastItem:getY() - lastItem:getHeight() > 0 then
local x, y = firstVisibleItem:getBounds(self)
local vx, vy = self.listItems:localToGlobal(self.listItems:getBounds(self))
local final = self.listItems:getY()
if firstVisibleItem:getHeight() + y >= firstVisibleItem:getHeight() / 2 then
final = self.listItems:getY() - y
else
final = self.listItems:getY() - (firstVisibleItem:getHeight() + y)
end
self.tween = GTween.new(self.listItems, 0.7, {y = final}, {delay = 0.01,
ease = easing.outQuartic, onChange = function() self:updateRender() end})
end
else
-- nothing here
end
end
local timePassed = event.deltaTime * 300
local li = self.listItems
-- Slow the list down
velocity = velocity * self.cfg.friction
li:setY(math.floor(self.listItems:getY() + velocity * timePassed))
self:updateRender()
local firstItem = li:getChildAt(1)
local lastItem = li:getChildAt(li:getNumChildren())
local lx, ly = self:localToGlobal(li:getBounds(self))
local x, y, w, h = self:getBounds(self)
local x2, y2 = self:localToGlobal(x, y)
--local lx, ly = lastItem:getBounds(self)
if firstItem.id == 1 and li:getY() > 0 then
velocity = 0
self:removeEventListener("enterFrame", self.scrollList, self)
self.tween = GTween.new(li, 0.4, {y = li.yInit}, {delay = 0.01, ease = easing.outQuartic})
end
if lastItem.id == #self.itemData and self.listHeight - lastItem:getY() - lastItem:getHeight() <= 0 then
if self.listHeight > self.viewSize and li:getY() < -self.listHeight + self.viewSize - lastItem:getHeight() * 0.5 then
velocity = 0
self:removeEventListener("enterFrame", self.scrollList, self)
self.tween = GTween.new(li, 0.4, {y = math.ceil(self.viewSize - self.listHeight)},
{delay = 0.01, ease = easing.outQuartic})
elseif self.listHeight < self.viewSize then
velocity = 0
self:removeEventListener("enterFrame", self.scrollList, self)
self.tween = GTween.new(li, 0.4, {y = li.yInit}, {delay = 0.01, ease = easing.outQuartic})
end
end
return true
end
function ListView:getFirstVisibleRow()
local index = 1
local posY = self:getY()
local item = nil
while posY <= self:getY() do
if(index < self.listItems:getNumChildren()) then
item = self.listItems:getChildAt(index)
local x1, y1 = self:localToGlobal(item:getBounds(self))
posY = y1 + item:getHeight()
index = index + 1
else
return nil
end
end
return item
end
function ListView:createRender()
local lastY = 0
local position = 1
while lastY < self.viewSize and position <= #self.itemData do
local rowObject = self:render(nil, position)
if (rowObject == nil) then break end
self.listItems:addChild(rowObject)
rowObject:setPosition(self.itemData[position].xInit, self.itemData[position].yInit)
lastY = rowObject:getY()
position = position + 1
end
self.listItems.yInit = self.listItems:getY()
end
function ListView:render(thisItem, id)
if thisItem == nil then
thisItem = self:newListItem(id, self.itemData[id].objectData)
else
thisItem:removeChildAt(thisItem:getNumChildren())
local callback = self.cfg.callback
local t = callback(self.itemData[id].objectData)
thisItem:addChild(t)
thisItem.id = id
end
return thisItem
end
function ListView:updateRender()
local firstItem = self.listItems:getChildAt(1)
local lastItem = self.listItems:getChildAt(self.listItems:getNumChildren())
local fx, fy = firstItem:getBounds(self)
local lx, ly = lastItem:getBounds(self)
local bottom = self.viewSize + self:getY()
local fillToTop = fy > 0
local fillToBottom = ly + lastItem:getHeight() < self.viewSize
if fillToBottom then
if (ly + lastItem:getHeight() < self.viewSize) and (lastItem.id ~= #self.itemData) then
local y = lastItem:getY() + lastItem:getHeight()
local recycledRow = firstItem
self:render(recycledRow, lastItem.id + 1)
self.listItems:addChild(recycledRow)
recycledRow:setY(y)
end
elseif fillToTop then
if fy > 0 and (firstItem.id ~= 1) then
local recycledRow = lastItem
local newItem = self:render(recycledRow, firstItem.id - 1)
local y = firstItem:getY() - newItem:getHeight()
self.listItems:addChildAt(recycledRow, 1)
recycledRow:setY(y)
end
end
if (self.cfg.onScroll ~= nil) then
self.cfg.onScroll(self, firstItem)
end
end
function ListView:trackVelocity(event)
local timePassed = msTimer() - self.velocityPrevTime
self.velocityPrevTime = self.velocityPrevTime + timePassed
if prevY then
velocity = (self.listItems:getY() - prevY) / timePassed
end
prevY = self.listItems:getY()
end
function msTimer()
return math.floor(os.timer() * 500)
end
return ListView
Example
-- fonts
local font = TTFont.new("fonts/Roboto-Medium-webfont.ttf", 24)
local fontSub = TTFont.new("fonts/Roboto-Medium-webfont.ttf", 18)
-- rows layout
function row(item)
local row = Sprite.new()
local itembmp = Bitmap.new(Texture.new(item.icon))
itembmp:setScale(0.75)
itembmp:setPosition(0, 0)
row:addChild(itembmp)
local itemtitle = TextField.new(font, item.title)
itemtitle:setPosition(itembmp:getX() + itembmp:getWidth() + 4, itembmp:getY() + itemtitle:getHeight())
row:addChild(itemtitle)
-- clickable button (arrow button)
local arrow = Texture.new("gfx/arrow_right.png")
local arrow_pressed = Texture.new("gfx/arrow_right_down.png")
local button = Button.new(Bitmap.new(arrow), Bitmap.new(arrow_pressed))
button:setScale(3, 2)
button:setPosition(application:getContentWidth() - button:getWidth(), itembmp:getY())
button:addEventListener("click", function()
mytitle:setText(item.title)
end)
row:addChild(button)
return row
end
-- datas
local data = {}
for i = 1, 10 do
data[i] = row(
{
icon = "gfx/maurice.png",
title = "Item ".. i,
}
)
end
-- scrollable list
local myList = ListView.new(
{
width = application:getContentWidth() - 32,
height = 8 * application:getContentHeight() / 10,
friction = 0.99,
bgColor = 0xaaaaff,
rowSnap = true, -- experimental feature
data = data
}
)
myList:setPosition(16, 0)
-- infos
mytitle = TextField.new(nil, "CLICK AN ARROW")
mytitle:setScale(3)
mytitle:setTextColor(0xff0000)
mytitle:setPosition(0, application:getContentHeight() - mytitle:getHeight())
-- stage
stage:addChild(myList)
stage:addChild(mytitle)