溫故
上回書說到, 對Cocos2d-lua的ScrollView進行了修改優化。
主要做了 —— 重用item。
僅僅是重用item, 是遠遠不夠的;還要對它進行進一步的功能擴展。
概覽
這次的擴展包括:
- 支持橫向和豎向
- 支持多行多列
- item的適配
- item數量不夠時的居中
- 刷新數據
- 跳轉到指定item
- 飛入動畫
多方向
之前的版本僅僅是縱向而已, 當然要支持橫向的滑動了。
橫向滑動其實與縱向不同。
縱向
由於ScrollView錨點在(0, 0), 要針對這個做一些處理。
否則, 顯示的是如下的樣子:
...
5
4
3
2
1
從下往上排列, 而且滑動是從下往上滑。
顯然, 這並不符合常規操作。
正常應該是, 從上往下滑, 且:
1
2
3
4
5
...
所以, 需要對它的座標進行小處理。
這裏有兩個座標需要被處理:
- item(要求錨點爲(0, 0))
它正常座標是從(0, 0)開始, 然後隨着索引增加變爲: (0, itemSize.height * index)
修改後的座標應該是從(0, innerSize.height - itemSize.height)開始, 隨着索引增加變爲:(0, innerSize.height - itemSize.height * index) - inner
正常開始的座標爲(0, 0), 顯示的是最底部的信息, 隨着滑動y座標減少。
修改後座標爲(0, scrollviewSize.height - innerSize.height), 顯示最頂部的信息, 隨着滑動y座標增加。
橫向
橫向就沒有那麼多問題了, 很符合常規的動作。
1 2 3 4 5 ...
它的兩個座標就不需要處理:
- item(要求錨點爲(0, 0))
座標從(0, 0)開始, 隨着索引增加變爲: (itemSize.width * index, 0)
- inner
座標從(0, 0)開始, 隨着滑動x座標增加
主要代碼
local ScrollViewDirection = {
DIR_VERTICAL = 1,
DIR_HORIZONTAL = 2,
DIR_BOTH = 3,
}
-- ScrollView 的大小
self.tContentSize
-- Item 的大小
self.tItemContentSize
-- item總數
self.iTotalItemNum
-- 重用的item的集合
self.tItemView
if ScrollViewDirection.DIR_HORIZONTAL == self:getDirection() then
self.tContentSize.width = self.tItemContentSize.width * self.iTotalItemNum
self:getInnerContainer():setContentSize(self.tContentSize)
self.fLastContentPos = self:getContentSize().width - self.tContentSize.width
local count = math.min(self.iTotalItemNum, math.ceil(self:getContentSize().width / self.tItemContentSize.width) + 1)
for i = 1, count do
local view = CSBReaderLoad(name)
view:init(...)
if view.setIndex then
view:setIndex(i)
end
-- 將父節點轉爲widget類型(原因可見 上一篇文章)
local widget = quick.packageNodeToWidget(view.pLayer:getChildByName("LayerTouch"))
view.pLayer = widget
-- widget隨父節點透明度變化,默認是false
widget:setCascadeOpacityEnabled(true)
self:addChild(view.pLayer)
view.pLayer:setVisible(i > 0 and i <= self.iTotalItemNum)
view.pLayer:setPosition(self.tItemContentSize.width * (i - 1), 0)
table.insert(self.tItemView, view)
end
self:jumpToLeft()
self:getInnerContainer():setPositionX(self.fLastContentPos)
self:setTouchEnabled(self.tContentSize.width > self:getContentSize().width)
elseif ScrollViewDirection.DIR_VERTICAL == self:getDirection() then
self.tContentSize.height = self.tItemContentSize.height * self.iTotalItemNum
self:getInnerContainer():setContentSize(self.tContentSize)
self.fLastContentPos = self:getContentSize().height - self.tContentSize.height
local count = math.min(self.iTotalItemNum, math.ceil(self:getContentSize().height / self.tItemContentSize.height) + 1)
for i = 1, count do
local view = CSBReaderLoad(name)
view:init(...)
if view.setIndex then
view:setIndex(i)
end
-- 將父節點轉爲widget類型(原因可見 上一篇文章)
local widget = quick.packageNodeToWidget(view.pLayer:getChildByName("LayerTouch"))
view.pLayer = widget
-- widget隨父節點透明度變化,默認是false
widget:setCascadeOpacityEnabled(true)
self:addChild(view.pLayer)
view.pLayer:setVisible(i > 0 and i <= self.iTotalItemNum)
view.pLayer:setPositionY(self.tItemContentSize.height * (self.iTotalItemNum - i))
table.insert(self.tItemView, view)
end
self:jumpToTop()
self:getInnerContainer():setPositionY(self.fLastContentPos)
self:setTouchEnabled(self.tContentSize.height > self:getContentSize().height)
end
適配item
根據ScrollView顯示區域大小及方向, 適當調整item大小。
更充分重用item, 適應多尺寸item。
如果是縱向的ScrollView, 根據width的值, 來決定放縮值。
如果是橫向的ScrollView, 根據height的值, 來決定放縮值。
然後根據放縮值再修改一下item size的值。
修改的東西(以縱向滑動ScrollView爲例)
- ScrollView inner 大小
local scale = ScrollViewSize.width / (ItemSize.width * multiNum)
- 需要繪製item的總個數
local totalRow = cond(totalItemNum % multiNum == 0,
totalItemNum / multiNum,
math.ceil(totalItemNum / multiNum))
- item的位置
self.iCount = math.min(totalRow, math.ceil(ScrollViewSize.height / ItemSize.height) + 1)
for i = 1, self.iCount do
for j = 1, self.iMultiNum do
...
item:setPosition(ItemSize.width * (j - 1), ItemSize.height * (totalRow - i))
end
end
多行多列
重用item, 這麼棒的東西, 肯定要多用用呀。
支持多行多列,是根據ScrollView的滾動方向,再根據傳入的行/列值進行設置。
需要重新計算一些數值。(下面均以縱向滑動的ScrollView爲例)
init
- 放縮值
scale = innerSize.width / (itemSize.width * multiNum)
- inner size
-- 根據總共需要的行數來計算高度
totalRow = (totalItemNum % multiNum == 0) and (totalItemNum / multiNum) or (math.ceil(totalItemNum / multiNum))
innerSize.height = totalRow * itemSize.height
- item position
-- 獲得需要重用的行數
showRow = math.min(totalRow, math.ceil(viewSize.height / itemSize.height) + 1)
for i = 1, showRow do
for j = 1, multiNum do
...
view:setPosition(itemSize.width * (j - 1), itemSize.height * (totalRow - i))
end
end
item數量不夠時的居中
主要是有個需求,希望item沒有填滿view的時候,所有的item居中顯示。
其實,item還是按照原來的方式放置,只需要移動inner的位置即可。
--[[
描述:
ScrollView內Item是否居中顯示
參數:
isCenter - boolean
是否居中顯示
返回:
無
--]]
function ScrollView:setShowCenter(isCenter)
local viewSize = self:getContentSize()
if ScrollViewDirection.DIR_HORIZONTAL == self:getDirection() then
local dertaValue = viewSize.width - self.tContentSize.width
if isCenter then
if dertaValue > 0 then
self:getInnerContainer():setPositionX(dertaValue/2)
end
else
self:getInnerContainer():setPositionX(dertaValue)
end
elseif ScrollViewDirection.DIR_VERTICAL == self:getDirection() then
local dertaValue = viewSize.height - self.tContentSize.height
if isCenter then
if dertaValue > 0 then
self:getInnerContainer():setPositionY(dertaValue/2)
end
else
self:getInnerContainer():setPositionY(dertaValue)
end
end
end
刷新數據
創建完ScrollView,除非item變動自己的位置,否則是不會刷新數據的。
所以需要一個手動刷新的方法。
這裏充分利用了lua的變長參數,在配合人爲默認規定。ie
--[[
描述:
刷新ScrollView中指定索引的item
參數:
... - 傳入一堆int
item的索引, -1代表全部刷新
返回:
無
--]]
function ScrollView:refreshItems(...)
local args = {...}
if #args > 0 then
if args[1] == -1 then
local items = self:getAllItemView()
for k, v in pairs(items) do
v:setIndex(v.iIndex)
end
else
-- 先做一個映射表,便於查找是否需要更新
local tempTable = {}
for k, v in pairs(args) do
tempTable[v] = 1
end
local items = self:getAllItemView()
for k, v in pairs(items) do
if v.iIndex and tempTable[v.iIndex] == 1 then
v:setIndex(v.iIndex)
end
end
end
end
end
這裏我用了一個映射表。
否則需要嵌套兩層循環,複雜度 m * n
做一個映射,只需要 n + m
用空間來換取時間
跳轉到指定item
這個功能ListView是支持的,覺得ScrollView也有必要支持一下。
方法是先計算出inner需要移動多少距離,從而知道了index需要變化多少。
主要步驟:(也是以垂直滑動方向爲例)
1. 計算所需跳轉的index在最上方位置是第幾行
2. 計算inner需要滑動多少距離
3. 計算從當前到目標,index需要變動多少
4. 按照移動後的index,重新佈局item
主要代碼:
-- -- 步驟1
local line = (index % self.iMultiNum == 0) and
(index / self.iMultiNum) or
(math.ceil(index / self.iMultiNum))
-- -- 步驟2
local posY = self:getContentSize().height - self.tContentSize.height + self.tItemContentSize.height * (line - 1)
-- 要考慮到滑動到底部,無法繼續向上滑的情況
posY = (posY > 0) and 0 or posY
-- -- 步驟3
local changeIndex = math.ceil((posY - self:getInnerContainer():getPositionY()) / self.tItemContentSize.height)
-- inner跳到指定位置
self:jumpToDestination(cc.p(0, posY))
-- -- 步驟4
self:updateViewByChangeIndex(changeIndex * self.iMultiNum)
根據index,重新佈局item
--[[
描述:
根據index,重新佈局item
參數:
changeIndex: int
改變的index值
返回:
無
]]
function ScrollView:updateViewByChangeIndex(changeIndex)
local totalBlock = (self.iTotalItemNum % self.iMultiNum == 0) and
(self.iTotalItemNum / self.iMultiNum) or
(math.ceil(self.iTotalItemNum / self.iMultiNum))
local items = self:getAllItemView()
for k, v in pairs(items) do
local idx = v.iIndex + changeIndex
v:setIndex(idx)
v.pLayer:setVisible(idx > 0 and idx <= self.iTotalItemNum)
local i = (idx % self.iMultiNum == 0) and (idx / self.iMultiNum) or (math.ceil(idx / self.iMultiNum))
local j = self.iMultiNum - (idx % self.iMultiNum)
v.pLayer:setPosition(self.tItemContentSize.width * (j - 1), self.tItemContentSize.height * (totalBlock - i))
end
end
跳轉的item在ScrollView中的位置
需要跳轉到的item在可視區域的 上、中、下 顯示
首先,一定要讓使用者傳入出現的位置枚舉,
然後在計算inner移動的位置上加上偏移量。
如果要在中間顯示,需要減去(向下移動) ScrollViewSize.height/2 , 因爲初始的位置是按照item在最上面計算的,減去一半高度後,還需要再加上item本身高度的一半 ItemSize.height/2。
如果在底部顯示,則需要減去(向下移動) ScrollViewSize.height , 同理,需要再加回來一個item的高度 ItemSize.height。
最後,依然要判定滑動到底部,無法滑動的情況。
SCROLLVIEW_ALIGNMENT = {
FIRST = 1,
MID = 2,
LAST = 3,
}
local posY = self:getContentSize().height - self.tContentSize.height + self.tItemContentSize.height * (line - 1)
if alignment == SCROLLVIEW_ALIGNMENT.MID then
posY = posY - self:getContentSize().height / 2 + self.tItemContentSize.height / 2
elseif alignment == SCROLLVIEW_ALIGNMENT.LAST then
posY = posY - self:getContentSize().height + self.tItemContentSize.height
end
posY = (posY > 0) and 0 or posY
飛入動畫
額外再加一個飛入動畫的支持吧。
就是從外部飛入到ScrollView的效果。
方法也很簡單,就是在開始的時候,讓所有的item在ScrollView外部;再一個個飛入到自己本應在的位置。
依舊是以垂直向爲例。
-- 遍歷所有item
for k, v in pairs(self.tItemView) do
-- 記錄它本來所在的位置
local aimPos = cc.p(v.pLayer:getPositionX(), v.pLayer:getPositionY())
-- 把它放在區域外
v.pLayer:setPositionY(-self:getContentSize().height - self.tItemContentSize.height)
v.pLayer:runAction(
act.seq(
-- 一個個飛入
act.delay((k - 1) * [delay_time]),
act.movto([move_time], aimPos)
)
)
end
當然,也要支持多方向ScrollView,並且要支持從前端飛入還是從後端飛入。
這些都是通過改動初始位置及回彈值來實現。
--[[
描述:
ScrollView內item的從外部飛入動畫, 有回彈效果
參數:
fromBack: boolean
對於垂直方向, true代表自下而上; 對於水平方向, true代表自右向左
返回:
無
]]
function ScrollView:playFlyInAction(fromBack)
fromBack = fromBack == nil and true
self.tItemView = self.tItemView or {}
local moveTime = 0.2
local delayTime = 0.1
if self:getDirection() == ScrollViewDirection.DIR_HORIZONTAL then
local initPos = fromBack and (self:getContentSize().width + self.tItemContentSize.width) or (-self:getContentSize().width - self.tItemContentSize.width)
for k, v in pairs(self.tItemView) do
local aimPos = cc.p(v.pLayer:getPositionX(), v.pLayer:getPositionY())
v.pLayer:setPositionX(initPos)
v.pLayer:runAction(
act.seq(
act.delay((k - 1) * delayTime),
act.movto(moveTime, aimPos)
)
)
end
elseif self:getDirection() == ScrollViewDirection.DIR_VERTICAL then
local initPos = fromBack and (-self:getContentSize().height - self.tItemContentSize.height) or (self:getContentSize().height + self.tItemContentSize.height)
for k, v in pairs(self.tItemView) do
local aimPos = cc.p(v.pLayer:getPositionX(), v.pLayer:getPositionY())
v.pLayer:setPositionY(initPos)
v.pLayer:runAction(
act.seq(
act.delay((k - 1) * delayTime),
act.movto(moveTime, aimPos)
)
)
end
end
end
總結
公司用ScrollView主要是用來替代ListView,雖然主要是用ScrollView的重用item的特性。
但是還是要平滑的過渡過來,要支持ListView常用的一些接口。讓這個組件更完善更好用。
當然功能擴展還沒有停止,之後也會陸陸續續的更新。