[Unity优化] 如何优化UGUI的ScrollRect

介绍

每个元素知道自己的序号,可以根据需要修改自己的内容、大小等信息。

此外支持了ScrollBar,支持横向、纵向及正反向。



在关闭Mask后可以看到,只有当需要的时候才动态实例化元素,使用完后回收。


最原始版本的代码是@ivomarel的InfinityScroll。我改到后来,基本和原始版没啥相同的了。

原代码使用了sizeDelta作为大小,但是这个在锚点不重合情况下是不成立的

支持了GridLayout

在启动时检查锚点和轴心,方便使用

修复了原代码在往前拖拽会卡顿的问题

优化代码,提升性能

支持反向滑动

支持ScrollBar (在无尽模式下不起作用;如果元素大小不一致会出现滚动条瑕疵)

此外,我修改了Easy Object Pool作为池子,循环利用元素。

警告: 为了解决原始代码回拉卡顿的问题,我直接复制了一份UGUI中的ScrollRect代码,而没有继承。这是因为老的做法是在onDrag里停止并立即启动滚动,而我通过修改两个私有变量保证了滑动顺畅。所有我的代码都用==========LoopScrollRect==========这样的注释包起来,维护起来就像打patch了。

框架思路

和UGUI自带的ScrollRect有所不同,我拆分出了LoopHorizontalScrollRect和LoopVerticalScrollRect两个类,分别代表水平滚动条和水平滚动条。下面我们以LoopVerticalScrollRect为例,水平版本类似。

1. 判定cell大小

LoopScrollRect要解决的核心问题是:如何计算每个元素的大小。这里我使用了Content Size Fitter配合Layout Element来控制每个cell的长宽,因此对于GridLayout直接取高度,否则取Preferred Height。需要注意的是,除了元素本身的大小之外,我们还要将padding考虑进去。

protected override float GetSize(RectTransform item)

{

    float size = contentSpacing;

    if (m_GridLayout != null)

    {

        size += m_GridLayout.cellSize.y;

    }

    else

    {

        size += LayoutUtility.GetPreferredHeight(item);

    }

    return size;

}

这个其实也是最核心的一个地方:在能够准确计算格子大小的基础上,后续工作就好实现了。

2. 如何优雅的增删元素

对于每个ScrollRect,其实只需要考虑在头部和尾部是否需要增加或者删除元素。在这里以头部的各种情况为例进行解释,因为在正向滑动情况下,必须保证在修改元素之后整个ScrollRect内容显示一致不跳变;这些情况比尾部处理会麻烦一些。

NewItemAtStart函数实现了在头部增加一个(或一行,针对GridLayout)元素,并返回这些元素的高度;DeleteItemAtStart代表删除头部的一个元素。需要注意的是,在修改头部元素之后要及时修改content的anchoredPosition,这样才能保证整个内容区域不会因为多了或者少了一行而产生跳变。

protected float NewItemAtStart()

{

    float size = 0;

    for (int i = 0; i < contentConstraintCount; i++)

    {

        // Get Element from ObjectPool

    }

    if (!reverseDirection)

    {

        // Modify content.anchoredPosition

    }

    return size;

}

protected float DeleteItemAtStart()

{

    float size = 0;

    for (int i = 0; i < contentConstraintCount; i++)

    {

        // Return Element to ObjectPool

    }

    if (!reverseDirection)

    {

        // Modify content.anchoredPosition

    }

    return size;

}

3. 何时需要增删元素

这里需要有两个概念viewBounds和contentBounds:前者是指ScrollRect本身的大小,一般也对应Mask;后者是指ScrollRect里所有cell组成的内容部分的大小。在这个基础上就简单了:如果contentBounds的最上面比viewBounds的最上面要低,那么尝试在顶部增加元素;如果contentBounds的最上面比viewBounds的最上面高很多,那么尝试删除元素。

protected override bool UpdateItems(Bounds viewBounds, Bounds contentBounds)

{

    bool changed = false;

    // cases for NewItemAtEnd/DeleteItemAtEnd

    if (viewBounds.max.y > contentBounds.max.y - 1)

    {

        float size = NewItemAtStart();

        if (size > 0)

        {

            changed = true;

        }

    }

    else if (viewBounds.max.y < contentBounds.max.y - threshold)

    {

        float size = DeleteItemAtStart();

        if (size > 0)

        {

            changed = true;

        }

    }

    return changed;

}

4. 对象池交互

在新建cell和销毁cell的时候,使用对象池来避免内存碎片;同时这里使用了SendMessage来向每个cell发送必须的信息,保证数据的正确性。

private void SendMessageToNewObject(Transform go, int idx)

{

    go.SendMessage("ScrollCellIndex", idx);

}

private void ReturnObjectAndSendMessage(Transform go)

{

    go.SendMessage("ScrollCellReturn", SendMessageOptions.DontRequireReceiver);

    prefabPool.ReturnObjectToPool(go.gameObject);

}

private RectTransform InstantiateNextItem(int itemIdx)

{

    RectTransform nextItem = prefabPool.GetObjectFromPool(prefabPoolName).GetComponent<RectTransform>();

    nextItem.transform.SetParent(content, false);

    nextItem.gameObject.SetActive(true);

    SendMessageToNewObject(nextItem, itemIdx);

    return nextItem;

}

5. 滚动条相关

这块我其实是估算的,根据当前的长度和当前元素个数/总个数按照比例缩放,这个在所有cell大小一致的情况下是没有问题的;但是如果大小不一致我就无法得到精确结果,所以会产生一定抖动。我暂时没有更好办法,因为得到的信息就是不够用。

6. 其他细节

我主要遇到了两个坑:

增加或者删除元素之后,有时候需要强行调用Canvas.ForceUpdateCanvases()刷新下。

注意不要在Build Canvas过程中再次修改元素,从而再次触发Build Canvas。

使用示例

以竖直滚动条为例,介绍一下步骤。如果觉得麻烦的话,直接打开DemoScene复制粘贴就好。当然你也可以干掉EasyObjPool,自己控制生成和销毁。

1. 准备好Prefabs

每个物体上需要贴上Layout Element并指定preferred width/height。

贴上一个脚本接受void ScrollCellIndex (int idx) 消息,从而对每个位置的元素根据需要灵活修改。


2. 在Hierarchy里右键,选择UI/Loop Horizontal Scroll Rect或UI/Loop Vertical Scroll Rect即可。使用Component菜单里的也是一样的。

Init in Start: 启动时自动调用Refill cells初始化

Prefab Pool: EasyObjPool物体

Prefab Pool Name: 第二步中对应的Cell Prefab名字

Total Count: 总共能有多少物体,范围0 ~ TotalCount-1

Threshold: 两端预留出来的缓存量(像素数)

ReverseDirection: 如果是从下往上或者从右往左拖动,就打开这里

Clear Cells: 清除已有元素,恢复到未初始化状态

Refill Cells: 初始化并填充元素


如果是正向滑动,就设置pivot为1;否则设为0并打开ReverseDirection。我强烈建议你试试在播放状态下修改这些参数。

无尽模式

如果需要无限滚动模式,将totalCount设为负数即可。

其他参考

后来搜了下,发现网上也有人提到过UGUI ScrollRect 优化(http://blog.csdn.net/subsystemp/article/details/46912479),不过他的策略是监听ScrollRect的value,然后禁用范围外的cell。最后作者也提到改成动态加载策略。这种基于value的做法我不太确认在在滚动前动态添加新元素的时候是否会出现问题。

文末,再次感谢钱康来的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容