-前言-
已經好久沒有寫博客了。最近開始了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沒多久,也在摸索,如果有什麼問題歡迎指正,謝謝~