Unity開發UGUI模塊開發經驗之ScrollView的使用及優化

-前言-

已經好久沒有寫博客了。最近開始了Unity的開發工作,一開始都是做做UI寫寫邏輯,目前主要任務就是摸透Unity UI的模塊開發。本章就來了解下最近用得筆記多的ScrollView功能。

在Unity中ScrollView功能是單一的滾動區域,但是我們日常遊戲開發中,使用ScrollView所需要的功能更像是使用List一樣,View中是重複的prefabs組成的,根據數據不同而展示不同內容的item。其實如果不考慮性能及個性的優化的話,自己認爲Unity的ScrollView功能非常強大,並且搭配自動佈局就能很輕鬆的實現所需要的功能。

-正文-

-不考慮性能的實現方式-

首先在場景中新建一個ScrollView組件,Unity會自動爲我們生成如圖

Content是我們元素添加的根節點,我們可以通過GameObject實例化Prefabs添加到Content中。添加邏輯也沒什麼好說。說下兩個注意的點:

1.由於滾動內容是根據Content的高度自適應出來的,因此在我們往Content下添加子節點時,需要更新Content的高度,有個不需要我們計算的方式,是在Content上掛載ContentSizeFitter腳本

2.根據需求掛載自適應腳本,GridLayoutGroup、VerticalLayoutGroup、HorizontalLayoutGroup這三個選中其中一個適用實際開發的,設置好參數即可。

問題

用上面方式做有個很大的問題是:

1.渲染壓力:對於不顯示的Item Unity也會記性計算渲染,雖然我暫時不知道UnityUI渲染步驟,但是ScrollView是使用的Mask遮罩,Mask一般是會計算並提交渲染的,只是在頂點着色器中被裁剪

2.計算壓力:如果ScrollView是一個玩家揹包,揹包數據可能有1000條,如果實例在1、2幀中同時處理這麼多數據及UI賦值,是非常卡的,很影響遊戲體驗,如果特殊情況不做優化一般就需要添加進度條。

優化方式

對於ScrollView的優化方式不管什麼引擎、語言都是差不多的,就是根據當前Bar的值去計算一個Viewport視窗內展示item所需源數據區間。下面的代碼是基於x-lua框架寫的,並且只實現了Vertical滑動,意思都差不多,能夠領悟到就行。

實現這個ScrollView我們就區別於原有的名字,命名爲ListView,只是骨子裏還是ScrollView。

1.創建ListView數據

local list_db = {
    -- 佈局信息
    Layout = {
        Padding = {
            Left = 10,
            Right = 10,
            Top = 30,
            Bottom = 30
        },
        CellSize = {
            x = 170, y = 174
        },
        Spacing = {
            x = 10, y = 10
        }
    },
    -- prefabs路徑
    PrefabsPath = false,
    -- 邏輯類
    LogicClass = false
}

這個ListView需要如同Unity自帶的Layout佈局信息,方便在設置item的時候設置座標。

2.創建ListView

-- 創建
UIListView.OnCreate = function(self, list_model)
    base.OnCreate(self)

    self.unity_scroll_view = UIUtil.FindComponent(self.transform, typeof(CS.UnityEngine.UI.ScrollRect))
    if IsNull(self.unity_scroll_view) then
        Logger.LogError("Unity Scroll View is Null-->??")
    end
    self.content_trans = self.unity_scroll_view.content
    self.unity_scroll_bar = self.unity_scroll_view.verticalScrollbar
    -- 檢測item是否加載
    if not GameObjectPool:GetInstance():CheckHasCached(list_model.PrefabsPath) then
        GameObjectPool:GetInstance():CoPreLoadGameObjectAsync(list_model.PrefabsPath, 1)
    end
    self.logic_cls = list_model.LogicClass
    self.render_prefabs_path = list_model.PrefabsPath
    self.cache_prefabs = {}
    self.list_model = list_model
    __InitCalColAndRowNum(self)
end

這裏的list_model就是上面的list_db的實例

3.計算viewport內需要的行和列

-- 計算有多少列
local function __InitCalColAndRowNum(self)
    --計算列
    local layout = self.list_model.Layout
    local valid_width = self.content_trans.rect.width - layout.Padding.Left - layout.Padding.Right
    local valid_height = self.unity_scroll_view.viewport.rect.height - layout.Padding.Top - layout.Padding.Bottom
    -- 列
    self.col = Mathf.Floor(valid_width / (layout.CellSize.x + layout.Spacing.x))
    -- 最大實例化出來的行數,超過的動態算
    self.max_row = Mathf.Ceil((valid_height + layout.Spacing.y) / (layout.CellSize.y + layout.Spacing.y))
    self.valid_height = valid_height
end

因爲這裏的Vertical方向,因此列是固定的,max_row就是viewport中能容納的最大行,這個行列相乘就是最少需要的Prefabs個數

4.設置數據源datasource

列表是通過數據驅動的,有多少數據再算出需要多少Item,因此這裏是根據外部傳進來的datasource,這裏就不關係數據類型是什麼,但要求是一個數組。

-- 計算列表高度
local function __CalContentHeight(self)
    local layout = self.list_model.Layout
    local total_row = Mathf.Ceil(#self.data_source / self.col)
    local height = total_row * (layout.CellSize.y + layout.Spacing.y) + layout.Padding.Top + layout.Padding.Bottom - layout.Spacing.y
    self.content_trans.sizeDelta  = Vector2.New(0,height)
    self.total_row = total_row
end

-- 刷新list
local function __Refresh(self)
    local new_index_vec = __CalDataIndexIntervalByScrollbarValue(self)
    if self.index_vec == new_index_vec then
        return
    end
    self.index_vec = new_index_vec
    --__MoveAllToCache(self)
    if not self.using_prefabs then
        self.using_prefabs = {}
    end
    local layout = self.list_model.Layout
    local item_index = 1
    local allFromUsing = true
    for index = new_index_vec.x, new_index_vec.y do
        local data = self.data_source[index]
        local item
        if allFromUsing and #self.using_prefabs > item_index then
            item = self.using_prefabs[item_index]
            item_index = item_index + 1
            if self.render_handler then
                self.render_handler:RunWith(item,data)
            end
        elseif #self.cache_prefabs > 0 then
            allFromUsing = false
            item = table.remove(self.cache_prefabs, 1)
            item:SetActive(true)
            if self.render_handler then
                self.render_handler:RunWith(item, data)
            end
            table.insert(self.using_prefabs, item)
        else
            allFromUsing = false
            local go = GameObjectPool:GetInstance():GetLoadedGameObject(self.render_prefabs_path)
            if IsNull(go) then
                Logger.LogError("UIListView GetLoadedGameObject Fail-->>Path:" .. self.render_prefabs_path)
                return
            end
            local go_trans = go.transform
            go_trans:SetParent(self.content_trans)
            go_trans.anchorMin = Vector2.New(0,1)
            go_trans.anchorMax = Vector3.New(0,1)
            go_trans.localPosition = Vector3.zero
            go_trans.localScale = Vector3.one
            go_trans.name = self.logic_cls.__cname .. tostring(RenderItemCnt)
            RenderItemCnt = RenderItemCnt + 1
            item = self:AddComponent(self.logic_cls, go)
            item.transform.sizeDelta = Vector2.New(layout.CellSize.x,layout.CellSize.y)
            if self.render_handler then
                self.render_handler:RunWith(item, data)
            end
            item:SetActive(true)
            table.insert(self.using_prefabs, item)
        end
        item.transform.anchoredPosition = Vector2.New(__CalPositionByIndex(self,index))
    end
    if allFromUsing and #self.using_prefabs > item_index then
        for i = item_index, #self.using_prefabs do
            local tmp_item = self.using_prefabs[item_index]
            table.remove(self.using_prefabs,item_index)
            table.insert(self.cache_prefabs,tmp_item)
        end
    end
end

首先通過datasource的數組長度計算出需要設置的content的高度,功能與ContentSizeFitter類似,只是這裏是自己算出來的。接下來就刷新列表,首先需要根據當前scrollbar的value計算出datasource的起點index與終點index,計算方式如下

local function __CalDataIndexIntervalByScrollbarValue(self)
    local start_row = Mathf.Floor((1 - self.scroll_value) * self.total_row)
    local end_row = start_row + self.max_row
    local start_index = (start_row - 3) * self.col + 1
    local end_index = end_row * self.col
    if start_index < 1 then
        start_index = 1
    end
    if end_index > #self.data_source then
        local diff = end_index - #self.data_source
        end_index = #self.data_source
        start_index = start_index - diff
        if start_index < 1 then
            start_index = 1
        end
    end
    return Vector2.New(start_index,end_index)
end

首先我們知道總共需要多少行,根據value就可以得出當前處於多少行,再加上viewport內容需要多少行,轉換爲index即可。

5.根據數據的index計算座標

-- 通過index計算座標
local function __CalPositionByIndex(self,index)
    local layout = self.list_model.Layout
    local col = (index - 1) % self.col
    local row = Mathf.Floor((index - 1) / self.col)
    local x = col * (layout.CellSize.x + layout.Spacing.x) + layout.Padding.Left + layout.CellSize.x / 2
    local y = row * (layout.CellSize.y + layout.Spacing.y) + layout.Padding.Top + layout.CellSize.y / 2
    y = y * -1
    return x,y
end

這裏就需要用到之前list_db中的佈局數據進行設置具體的座標

6.回調渲染

設置好後就可以通過之前設置的一個render函數,回調回ListView的持有方,告訴它我用什麼item用上了什麼數據,讓它來拿着這個數據和item對象來做些邏輯。

 

完~

自己也是才學Unity沒多久,也在摸索,如果有什麼問題歡迎指正,謝謝~

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