Cocos2d-lua ScrollView優化2

溫故

上回書說到, 對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常用的一些接口。讓這個組件更完善更好用。
當然功能擴展還沒有停止,之後也會陸陸續續的更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章