-前言-
已经好久没有写博客了。最近开始了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没多久,也在摸索,如果有什么问题欢迎指正,谢谢~