-前言-

已经好久没有写博客了。最近开始了Unity的开发工作,一开始都是做做UI写写逻辑,目前主要任务就是摸透Unity UI的模块开发。本章就来了解下最近用得笔记多的ScrollView功能。

在Unity中ScrollView功能是单一的滚动区域,但是我们日常游戏开发中,使用ScrollView所需要的功能更像是使用List一样,View中是重复的prefabs组成的,根据数据不同而展示不同内容的item。其实如果不考虑性能及个性的优化的话,自己认为Unity的ScrollView功能非常强大,并且搭配自动布局就能很轻松的实现所需要的功能。

-正文-

-不考虑性能的实现方式-

首先在场景中新建一个ScrollView组件,Unity会自动为我们生成如图

unity Scroll View自动隐藏Scrollbar unity scroll view优化_数据

Content是我们元素添加的根节点,我们可以通过GameObject实例化Prefabs添加到Content中。添加逻辑也没什么好说。说下两个注意的点:

1.由于滚动内容是根据Content的高度自适应出来的,因此在我们往Content下添加子节点时,需要更新Content的高度,有个不需要我们计算的方式,是在Content上挂载ContentSizeFitter脚本

unity Scroll View自动隐藏Scrollbar unity scroll view优化_Layout_02

2.根据需求挂载自适应脚本,GridLayoutGroup、VerticalLayoutGroup、HorizontalLayoutGroup这三个选中其中一个适用实际开发的,设置好参数即可。

unity Scroll View自动隐藏Scrollbar unity scroll view优化_ci_03

问题

用上面方式做有个很大的问题是:

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没多久,也在摸索,如果有什么问题欢迎指正,谢谢~