UI Scrollable List

From GiderosMobile
Revision as of 18:05, 10 February 2020 by MoKaLux (talk | contribs)
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

Ui list view.png

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)


Template:Welcome!