解决Unity无限滚动复用列表的问题

目录
  • 无限滚动复用列表
  • 前言
  • 设计思路
  • 关键基类
    • 1.ScrollData
    • 2.ScrollView
    • 3.ScrollItem
  • 测试类
    • 1.添加20组数据
    • 2.回到顶部
    • 3.回到底部
  • 坑点
    • 1.ScrollView回滚设置延迟;
    • 2.锚点设置;
    • 3.数据需要网络请求,自适应会失效;

无限滚动复用列表

Demo展示

前言

游戏中有非常多的下拉滚动菜单,比如成就列表,任务列表,以及背包仓库之类;如果列表内容非常丰富,会占用大量内存,这篇无限滚动复用ScrollView就是解决这种问题;还可以用来做朋友圈,聊天等;

一般情况,ScrollView中每个Item的大小是一直的,使用ContentSizeFillter组件足够解决大部分问题;

如果每个Item大小不一致,问题就复杂起来,需要做滚动位置判断,我这里做了大小适应;

设计思路

1.将数据部分和滚动逻辑部分分离开,数据设计成泛型类;

2.在ScrollView组件上添加ScrollView脚本,控制Item的添加和删除,分为头部和尾部;

3.在每个Item上添加ScrollItem脚本,重写更新数据方法,同时监听自身是否为头部或者尾部;

4.如果为头部或者尾部,且超界通过委托调用ScrollView脚本中的添加或删除Item方法;

关键基类

1.ScrollData

负责整个列表的数据管理,分为总数据和现实数据两个链表,增删查改方法;泛型类方便复用;

这里使用LinkedList方便查找并返回头尾节点;

全部代码:

public class ScrollData<T>
{
    public List<T> allDatas;
    public LinkedList<T> curDatas;

    public ScrollData()
    {
        allDatas = new List<T>();
        curDatas = new LinkedList<T>();
        //加载数据;
    }
    //获取头数据
    public T GetHeadData()
        if(allDatas.Count == 0)
            return default(T);
        if (curDatas.Count == 0)
        {
            T head = allDatas[0];
            curDatas.AddFirst(head);
            return head;
        }
        T t = curDatas.First.Value;
        int index = allDatas.IndexOf(t);
        if (index != 0)
            T head = allDatas[index - 1];
        return default(T);
    //移出头数据
    public bool RemoveHeadData()
        if (curDatas.Count == 0 || curDatas.Count == 1)
            return false;
        curDatas.RemoveFirst();
        return true;
    //获取尾部数据
    public T GetEndData()
        if (allDatas.Count == 0)
            T end = allDatas[0];
            curDatas.AddLast(end);
            return end;
        T t = curDatas.Last.Value;

        if (index != allDatas.Count - 1)
            T end = allDatas[index + 1];

    //移出尾部数据
    public bool RemoveEndData()
        curDatas.RemoveLast();
    //添加数据,通过数组
    public void AddData(T[] t)
        allDatas.AddRange(t);
    //添加数据,通过链表
    public void AddData(List<T> t)
        allDatas.AddRange(t.ToArray());
    //添加单条数据
    public void AddData(T t)
        allDatas.Insert(0,t);
        curDatas.AddFirst(t);
    //情况当前显示节点
    public void ClearCurData()
        curDatas.Clear();
    //获取当前显示链表的第一个数据在总数据中的下标
    public int GetFirstIndex()
        return allDatas.IndexOf(t);
}

2.ScrollView

关键字段:

scrollItemGo	//每个Item的预制体
content			//scrollRect下的Content
spacing			//每个Item的间隔
isStart			//是否第一次加载

方法:

GetChildItem;

1.获取一个Item的预制体,先从content的子物体中寻找active为false的物体,如果没有则根据scrollItemGo克隆一个;

2.创建新Item时,获取ScrollItem组件,赋值其中的参数(四个委托),并初始化;

OnAddHead;OnRemoveHead;OnAddEnd;OnRemoveEnd;

委托方法:

1.调用ScrollData中GetHeadData方法,获得头数据;

2.找到content中第一个节点;

3.调用GetChildItem方法获得item的实例;

4.SetAsFirstSibling,将实例设置为首节点,同时调用RefreshData,刷新数据;

5.根据item 的宽度做自适应(item大小相同,只选挂载ContentSizeFitter);

全部代码:

public class ScrollView : MonoBehaviour
{
    public GameObject scrollItemGo;
    private RectTransform content;

    [SerializeField]
    private float spacing;
    private bool isStart = true;
    void Start()
    {
        content = this.GetComponent<ScrollRect>().content;
        spacing = 15;
        OnAddHead();
    }
    private GameObject GetChildItem()
        //查找是否有未回收的子节点
        for (int i = 0; i < content.childCount; ++i)
        {
            GameObject tempGo = content.GetChild(i).gameObject;
            if (!tempGo.activeSelf)
            {
                tempGo.SetActive(true);
                return tempGo;
            }
        }
        //无创建新的
        GameObject childItem = GameObject.Instantiate<GameObject>(scrollItemGo,content.transform);
        ScrollViewItem scrollItem = childItem.GetComponent<ScrollViewItem>();
        if (scrollItem == null)
            scrollItem = childItem.AddComponent<ScrollViewItem>();

        scrollItem.onAddHead += OnAddHead;
        scrollItem.onRemoveHead += OnRemoveHead;
        scrollItem.onAddEnd += OnAddEnd;
        scrollItem.onRemoveEnd += OnRemoveEnd;
        scrollItem.Init();
        childItem.GetComponent<RectTransform>().anchorMin = new Vector2(0.5f, 1);
        childItem.GetComponent<RectTransform>().anchorMax = new Vector2(0.5f, 1);
        childItem.GetComponent<RectTransform>().pivot = new Vector2(0, 1);
        childItem.transform.localScale = Vector3.one;
        childItem.transform.localPosition = Vector3.zero;
        //-----设置宽高——加载数据
        return childItem;
    private void OnAddHead()
        Data data = this.GetComponent<Test>().scrollData.GetHeadData();
        if (data != null)
            Transform first = FindFirst();
            //----first 不为 数据头---在data中做了
            GameObject obj = GetChildItem();
            obj.GetComponent<ScrollViewItem>().RefreshData(data);
            obj.transform.SetAsFirstSibling();
            RectTransform objRect = obj.GetComponent<RectTransform>();
            float height = objRect.sizeDelta.y;

            if (first != null)
                obj.transform.localPosition = first.localPosition + new Vector3(0, height + spacing, 0);
            if (isStart)
                content.sizeDelta += new Vector2(0, height + spacing);
                isStart = false;
    private void OnRemoveHead()
        var scrollData = this.GetComponent<Test>().scrollData;
        if (scrollData.RemoveHeadData())
            Transform tf = FindFirst();
            if (tf != null)
                tf.gameObject.SetActive(false);
    private void OnAddEnd()
        Data data = this.GetComponent<Test>().scrollData.GetEndData();
            Transform end = FindEnd();
            //----end 不为 数据尾在data中做了
            obj.transform.SetAsLastSibling();
            float height = end.GetComponent<RectTransform>().sizeDelta.y;
            if (end != null)
                obj.transform.localPosition = end.localPosition - new Vector3(0, height + spacing, 0);
            //是否增加content高度
            if (IsAddContentH(obj.transform))
                float h = obj.GetComponent<RectTransform>().sizeDelta.y;
                content.sizeDelta += new Vector2(0, h + spacing);
    private void OnRemoveEnd()
        if (scrollData.RemoveEndData())
            Transform tf = FindEnd();
    private Transform FindFirst()
            if (content.GetChild(i).gameObject.activeSelf)
                return content.GetChild(i);
        return null;
    private Transform FindEnd()
        for (int i = content.childCount - 1; i >= 0; --i)
    private bool IsAddContentH(Transform tf)
        Vector3[] rectC = new Vector3[4];
        Vector3[] contentC = new Vector3[4];
        tf.GetComponent<RectTransform>().GetWorldCorners(rectC);
        content.GetWorldCorners(contentC);
        if (rectC[0].y < contentC[0].y)
            return true;
        return false;
}

3.ScrollItem

关键字段:四个委托

public Action onAddHead;
public Action onRemoveHead;
public Action onAddEnd;
public Action onRemoveEnd;

关键方法:

OnRecyclingItem;

1.判断自身是否为头尾节点;

2.判断自身是否超界,超界需要隐藏自身;

3.判断自身与边界距离,是否添加节点;

关键API:

RectTransform.GetWorldCorners(Vector3[4])

获取UI对象四个顶点的世界坐标,下标对应的位置;

全部代码:

public class ScrollViewItem : MonoBehaviour
{
    private RectTransform viewRect;
    private RectTransform rect;
    [SerializeField]
    private float viewStart;
    [SerializeField]
    private float viewEnd;
    [SerializeField]
    private Vector3[] rectCorners;
    public Action onAddHead;
    public Action onRemoveHead;
    public Action onAddEnd;
    public Action onRemoveEnd;
    public Text nameT;
    public Text inputT;
    void Start()
    {
        Init();
    }
    public void Init()
    {
        viewRect = transform.parent.parent.GetComponent<RectTransform>();
        rect = this.GetComponent<RectTransform>();
        rectCorners = new Vector3[4];
        viewRect.GetWorldCorners(rectCorners);
        viewStart = rectCorners[1].y;
        viewEnd = rectCorners[0].y;
    }
    void Update()
    {
        OnRecyclingItem();
    }
    //超界变false;
    private void OnRecyclingItem()
    {
        rect = this.GetComponent<RectTransform>();
        rectCorners = new Vector3[4];
        rect.GetWorldCorners(rectCorners);
        if (IsFirst())
        {
            if (rectCorners[0].y > viewStart)
            {
                //隐藏头节点
                if (onRemoveHead != null)
                    onRemoveHead();
            }
            if (rectCorners[1].y < viewStart)
            {
                //添加头节点-头节点不为数据起始点
                if (onAddHead != null)
                    onAddHead();
            }
        }
        if (IsLast())
        {
            if (rectCorners[0].y > viewEnd)
            {
                //添加尾节点-尾节点不为数据末尾
                if (onAddEnd != null)
                    onAddEnd();
            }
            if (rectCorners[1].y < viewEnd)
            {
                //隐藏尾节点
                if (onRemoveEnd != null)
                    onRemoveEnd();
            }
        }
    }
    private bool IsFirst()
    {
        for (int i = 0; i < transform.parent.childCount; ++i)
        {
            Transform tf = transform.parent.GetChild(i);
            if (tf.gameObject.activeSelf)
            {
                if (tf == this.transform)
                {
                    return true;
                }
                break;
            }
        }
        return false;
    }
    private bool IsLast()
    {
        for (int i = transform.parent.childCount-1; i >= 0 ; i--)
        {
            Transform tf = transform.parent.GetChild(i);
            if (tf.gameObject.activeSelf)
            {
                if (tf == this.transform)
                {
                    return true;
                }
                break;
            }
        }
        return false;
    }
    public bool IsInView()
    {
        rect = this.GetComponent<RectTransform>();
        rect.GetWorldCorners(rectCorners);
        if (rectCorners[1].y > viewEnd || rectCorners[0].y < viewStart)
            return false;
        return true;
    }
    public void RefreshData(Data da)
    {
        nameT.text = da.name;
        inputT.text = da.text;

        Vector2 oldSize = rect.sizeDelta;
        rect.sizeDelta = new Vector2(oldSize.x, 200 + da.h);
    }
}

测试类

初始化数据,随机4中宽度的item;

void InitData()
{
    int[] hArr = new int[4];
    hArr[0] = 0;
    hArr[1] = 190;
    hArr[2] = 190 * 2;
    hArr[3] = 190 * 3;
    for (int i = 0; i < 30; ++i)
    {
        Data da = new Data();
        da.name = "小紫苏" + i.ToString();
        da.text = "000000" + i.ToString();
        int index = UnityEngine.Random.Range(0, 3);
        da.h = hArr[index];
        scrollData.allDatas.Add(da);
    }
}

添加三个按钮,及相应的响应方法;

1.添加20组数据

private void AddData()
{
    int[] hArr = new int[4];
    hArr[0] = 0;
    hArr[1] = 190;
    hArr[2] = 190 * 2;
    hArr[3] = 190 * 3;
    Data[] newData = new Data[20];
    for (int i = 0; i < 20; ++i)
    {
        Data da = new Data();
        da.name = "小紫苏" + i.ToString();
        da.text = "000000" + i.ToString();
        int index = UnityEngine.Random.Range(0, 3);
        da.h = hArr[index];
        newData[i] = da;
    }
    scrollDat

回到顶部或底部需要有过程,因此需要在update中运行,也可以用插值;

2.回到顶部

private void OnGoHead()
{
    if (isGoHead)
        isGoHead = false;
    else
        isGoHead = true;
}
private void OnGoLast()
{
    if (isGoLast)
        isGoLast = false;
    else
        isGoLast = true;
}

3.回到底部

private void GoHead()
{
    if (!isGoHead)
        return;

    float curPos = scroll.verticalNormalizedPosition;
    if (curPos != 1)
    {
        curPos += 0.01f;
        if (curPos >= 1)
        {
            curPos = 1;
            isGoHead = false;
        }
        scroll.verticalNormalizedPosition = curPos;
    }
}
private void GoLast()
{
    if (!isGoLast)
        return;
    float curPos = scroll.verticalNormalizedPosition;
    if (curPos != 0)
    {
        curPos -= 0.01f;
        if (curPos <= 0)
        {
            curPos = 0;
            isGoLast = false;
        }
        scroll.verticalNormalizedPosition = curPos;
    }
}

坑点

1.ScrollView回滚设置延迟;

回滚判断是通过verticalNormalizedPosition的API,更改这个值后需要间隔一帧才会修改,因为可能导致判断两次;

解决方法,延迟调用1s——Invoke;

2.锚点设置;

锚点的设置以及UI的自适应会直接影响项目回滚的方向和位置;

大部分位置出错都是因为锚点设置错误;

3.数据需要网络请求,自适应会失效;

网络数据一般都是异步,所以判断会做多次,因此数据上要求提前计算好item的宽度;

项目工程我上传到Gitee,可自行下载学习;https://gitee.com/small-perilla/scroll-view

以上是我对滚动复用组件的总结,如果有更好的意见,欢迎给作者评论留言;

到此这篇关于Unity无限滚动复用列表的文章就介绍到这了,更多相关Unity无限滚动列表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Unity3d实现无限循环滚动背景

    在游戏项目中我们常常看到商城的广告牌,几张广告图片循环滚动,类似跑马灯,现在我将讨论一种实现方法,并提供一个管理类,大家可以直接使用. 实现原理:背景图片循环滚动的原理很简单:两张图片向一个方向移动,当达某张图片到临界区域时将图片放在后面,依次循环. 在实际项目中,广告牌显示的图片数量不确定,例如某个假期活动会上很多新品,此时我们需要动态的创建显示的图片(一般在配置表读取数据),如果需要显示分类标签还得动态生成分类标签. 综上所述,一个完整的广告牌组件应该具有以下功能: - 无限循环的滚动背景图

  • Unity UI组件ScrollRect实现无限滚动条

    在游戏开发中 经常遇到滚动显示的数据 特别是商店商品 排行榜 .......等 数据很多,每一条数据去加载一个UI来显示显然对内存浪费很大,这种情况处理一般就是用几个显示条可滚动循环显示无限数据条.本篇介绍实现过程和大体思路以及可重用的滑动脚本InfinityGridLayoutGroup和MarketLayoutGroup数据管理刷新脚本.MarketElement类要看具体项目中具体数据结构来设计:仅供参考. 一 .总体流程 建一个循环滑动脚本 InfinityGridLayoutGroup

  • 解决Unity无限滚动复用列表的问题

    目录 无限滚动复用列表 前言 设计思路 关键基类 1.ScrollData 2.ScrollView 3.ScrollItem 测试类 1.添加20组数据 2.回到顶部 3.回到底部 坑点 1.ScrollView回滚设置延迟: 2.锚点设置: 3.数据需要网络请求,自适应会失效: 无限滚动复用列表 Demo展示 前言 游戏中有非常多的下拉滚动菜单,比如成就列表,任务列表,以及背包仓库之类:如果列表内容非常丰富,会占用大量内存,这篇无限滚动复用ScrollView就是解决这种问题:还可以用来做朋

  • Vue.js 无限滚动列表性能优化方案

    问题 大家都知道,Web 页面修改 DOM 是开销较大的操作,相比其他操作要慢很多.这是为什么呢?因为每次 DOM 修改,浏览器往往需要重新计算元素布局,再重新渲染.也就是所谓的重排(reflow)和重绘(repaint).尤其是在页面包含大量元素和复杂布局的情况下,性能会受到影响.那对用户有什么实际的影响呢? 一个常见的场景是大数据量的列表渲染.通常表现为可无限滚动的无序列表或者表格,当数据很多时,页面会出现明显的滚动卡顿,严重影响了用户体验.怎么解决呢? 解决方案 既然问题的根源是 DOM

  • 微信小程序实现无限滚动列表

    本文实例为大家分享了微信小程序实现无限滚动列表的具体代码,供大家参考,具体内容如下 效果图1.0 实现方式是利用小程序原声组件swiper,方向设置为纵向 :vertical='true'设置同时显示的滑块数量:display-multiple-items='4'设置自动轮播:autoplay:'true'. 话不所说,直接上代码: <!-- 底部排名 --> <view class='contentBottom'> <view class='BottomFirst'>

  • js实现列表向上无限滚动

    本文实例为大家分享了js实现列表向上无限滚动的具体代码,供大家参考,具体内容如下 先来一张效果图 html <div class="transdata1"> <ul class="tody-table-header2"> <li>商品</li> <li>数量(kg)</li> <li>单价(元)</li> <li>金额(元)</li> </u

  • 使用Element的InfiniteScroll 无限滚动组件报错的解决

    一.问题描述 在使用Element的InfiniteScroll 无限滚动时候出现以下错误: TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node' InfiniteScroll的更多用法element官网 二.解决办法 给需要使用 InfiniteScroll 的元素或者它的父级元素加上 overflow:auto; 属性即可. <template> <

  • 原生+React实现懒加载(无限滚动)列表方式

    目录 应用场景 效果预览 思路剖析 原生代码实现 迁移到React 总结 应用场景 懒加载列表或叫做无限滚动列表,也是一种性能优化的方式,其可疑不必一次性请求所有数据,可以看做是分页的另一种实现形式,较多适用于移动端提升用户体验,新闻.资讯浏览等. 效果预览 思路剖析 设置临界元素,当临界元素进入可视范围时请求并追加新数据. 根据可视窗口和滚动元素组建的关系确定数据加载时机. container.clientHeight - wrapper.scrollTop <= wrapper.client

  • 简单方法实现Vue 无限滚动组件示例

    目录 1. 前言 2. 整体思路 开始 3. 钩子函数 3.1 获取偏移初始位置的像素值 3.2 获取开始滚动和结束滚动的钩子函数 4. 完整代码 1. 前言 对于列表类型的大量数据,前端展示往往采用 分页 和 无限滚动 的方式来展示,对于用户来说,鼠标滚轮和触控屏使滚动行为要比点击更快更容易. element-plus 组件库提供了简单的 vue 指令,就可以轻易的实现 但是 element-plus 只支持无限向下滚动,不支持无限向上滚动,同时也没缺少丰富的 钩子函数,我们无法在这个基础上更

  • vue使用mint-ui实现下拉刷新和无限滚动的示例代码

    在开发web-app中,总会遇到v-for出来的li会有很多,当数据达几百上千条的时候,一起加载出来会造成用户体验很差的效果. 这时候我们可以使用无限滚动和下拉刷新来实现控制显示的数量,当刷新到底部的边界的时候会触发无限滚动的事件,再次加载一定数量的条目. 还是拿在项目中的功能来举栗子介绍. 有个列表,几千条数据,做分页查询,限制每次显示查询20条,每次拉到最后20条边缘的时候,触发无限滚动,这时候会出现加载图标,继续加载后续20条数据,加载到最后的时候会提示数据"加载完毕". 项目的

  • IOS中无限滚动Scrollview效果

    本文实例讲了IOS无限滚动效果,分享给大家供大家参考,具体内容如下 滑动到当前位置时候才去请求,本地有内容则直接显示(以来SDWebImage,UIView+Ext) HZScrollView.h #import <UIKit/UIKit.h> typedef void(^HZReturnBlock)(NSInteger index,CGFloat offset); typedef NS_ENUM(NSUInteger, HZScrollViewPageControllPosition) {

  • Android代码实现AdapterViews和RecyclerView无限滚动

    应用的一个共同的特点就是当用户欢动时自动加载更多的内容,这是通过用户滑动触发一定的阈值时发送数据请求实现的. 相同的是:信息实现滑动的效果需要定义在列表中最后一个可见项,和某些类型的阈值以便于开始在最后一项到达之前开始抓取数据,实现无限的滚动. 实现无限滚动的现象的重要之处就在于在用户滑动到最低端之前就行数据的获取,所以需要加上一个阈值来帮助实现获取数据的预期. 使用ListView和GridView实现 每个AdapterView 例如ListView 和GridView 当用户开始进行滚动操

随机推荐