zoukankan      html  css  js  c++  java
  • 在NGUI中高效优化UIScrollView之UIWrapContent的简介以及使用

    前言:

    1.我使用的NGUI版本为 v3.7.5,不知道老版的NGUI是否有UIWrapContent 这个脚本。

    2.本文讲解主要以图片显示的例子为主,本文例子UIScrollView是水平方向,一页数量为6个cell,cell上显示的数字是其处于整个列表中的index,index 从0开始计数。

    一。使用UIWrapContent的原因以及大致原理

    做UI的时候经常会做一些列表来显示商品啦,任务什么的,而且当这些列表数量很多,一页显示不完的时候,又会使用UIScrollView,这样虽然实现了滑动显示界面,但是当列表数量过多或者每个列表元素过多时,就会在初始化列表和滑动列表时导致程序卡顿。其实细想一下,每个列表的显示基本一样,完全可以重用的,于是NGUI里有了这个UIWrapContent脚本。

    UIWrapContent的原理大致就是初始化的时候显示一页的元素,当滑动这一页的时候,位于首端或者尾端的元素会滑出显示区域,然后再把滑出显示区域的元素移动到本页的末端,同时改变其显示,这样就可以重复使用现有的几个元素来做显示。如下图所示,初始的时候只有一页6个cell,向左滑动列表,当第一个cell(0字的cell)滑出显示区域后,它会自动移动到页面的最末端,就是第二个箭头所指示的位置。向右滑动的时候同理。

    二。如何使用UIWrapContent

    上面讲解了UIWrapcontent的大致原理,现在讲如何使用。

    1.UI初始化

    1). 创建UIWrapContent对象

    首先在你需要显示的界面下创建UIScrollView对象,然后在UIScrollView下创建一个空GameObject,再在此GameObject上挂接UIWrapContent脚本。我在场景中使用右键时找不到UIWrapContent脚本,需要在空GameObject的Inspector界面使用 Add Component 选项来找到UIWrapContent来挂接。

    2). 添加并摆放好Cell

    接下来需要添加cell到UIWrapContent了 ,其实也可以用代码动态添加,不过还是推荐预先摆好,这样可以方便调整显示。如开始所讲的实现原理,我们需要至少添加一页的cell。(如果你界面中的cell不是太多的话,就不必使用UIWrapContent)

    注意点一:cell的位置需要从0开始,然后依次以UIWrapContent中 Item Width 设定的值来递增摆放。如上图所示,UIScrollView的方向是水平的,UIWrapContent中Item Width设置的值为100,那么cell的摆放位置就是从左到右Y坐标不变,X坐标依次为0,100,200,300,。。。最后摆放完之后,如果发现所有cell整体显示位置不合适的话就需要调整UIWrapContent挂接的GameObject了,只需要拖到GameObject到合适位置就行了。如上图示,黄色的UIWrapContent边界基本和UIScrollView的紫色边界比较接近即可。

    注意点二:通常情况下我们列表里的cell之间都会有间隔,滑动列表时会出现首端第一cell还未全部消失的时候,就会因UIWrapContent的算法作用突然移动的列表的末端,这样用户体验很不好。这时我们就需要在添加完满页cell后,再在末尾多添加一个cell。如上图示,一页会有6个cell,我在最末尾又添加了一个cell ,这个cell在UIScrollView外部,没有显示出来。这样的话,当滑动列表时首端cell就可以完全移出UIScrollView。不过这里有个问题,就是多摆放的这个cell会在UIWrapContent初始化的时候遗漏掉,当你滑动翻页的时候不会初始化这个cell。这个时候就需要调整UIScrollView的显示区域,让多出的这个cell距离UIScrollView的中心位置不超过UIScrollView显示区域的一半。具体情况会在下面代码部分解释。

     

    2.代码初始化

    经过上面的UI设置后,就可以绑定代码了。

    1). onInitializeItem
        /// <summary>
        /// Callback that will be called every time an item needs to have its content updated.
        /// The 'wrapIndex' is the index within the child list, and 'realIndex' is the index using position logic.
        /// </summary>
    
        public OnInitializeItem onInitializeItem;

    在UIWrapContent里有个onInitializeItem委托,为了方便下面的解释,先解释这个委托,解释这个委托前再说点别的。

    综上可知实际上我们的cell只有一页,但是实现的效果可能是几页或者十几页,每个cell都有自己对应的Data。通常来说,如果做列表显示,我们首先会用一个List或者数组来存放所有cell对应的Data,假如说会显示100个cell,就会有100条Data(当然如果每个cell之间的显示差别不大,即对应的Data差别不大时也不必如此)。有了这些Data后就可以把指定的Data传给指定的cell做显示。onInitializeItem就是用来做这个的。

    看UIWrapContent源码,onInitializeItem会传出三个值,第一个是GameObject,就是当前要跳转的cell(可能是从首端跳转到末端,也可能是末端跳转到首端),第二个是 int 值,可以称为wrapIndex,其实就是当前要跳转cell位于现有列表中的Index,拿本文列子来说,实际列表有7个cell,则这个wrapIndext的值就是在 0 ~ 6 之间(索引值是从0开始计数的),重点就是第三个 int 值,可以称之为realIndex。这个值对应的就是UIWrapContent实现的列表值,比如说我们实现的是100个cell的列表(实际上只用了7个cell),这个值的范围就是 0 ~ 99。

    当滑动列表,每次有cell跳转的时候,就会调用这个onInitializeItem,会把要跳转cell的GameObject和realIndex传到绑定方法,此时就可以根据realIndex从你的数据List或者数组中来取得对应的Data,再把这个Data传给这个GameObject做显示改变即可。比如本文绑定的方法,实现的功能就是显示cell当前处于列表中的realIndex。

     void OnUpdateItem(GameObject go, int index, int realIndex) 
        {
    
            //Debug.Log("index = " + index);
            Debug.Log("realIndex = " + realIndex);
            Testbox tb = go.GetComponent<Testbox>();
            tb.SetNumber(realIndex.ToString());
        }
    2). Range Limit

    在UIWrapContent的Inspector面板有这么个属性,当去看源码的时候对应的是minIndex和maxIndex这两个变量,我看了好几遍注释也不知所云,或许经过测试你会发现,当minIndex和maxIndex相等的时候,你用UIWrapContent制作的列表可以无限滑动,可以一直滑,一直滑,没有尽头,显然通常情况下我们不需要这个功能。后来测试多遍后才知道这两个值的作用。

    /// <summary>
        /// Minimum allowed index for items. If "min" is equal to "max" then there is no limit.
        /// For vertical scroll views indices increment with the Y position (towards top of the screen).
        /// </summary>
    
        public int minIndex = 0;
    
        /// <summary>
        /// Maximum allowed index for items. If "min" is equal to "max" then there is no limit.
        /// For vertical scroll views indices increment with the Y position (towards top of the screen).
        /// </summary>
    
        public int maxIndex = 0;

    UIWrapContent显示初始化时是从0开始的。这里会存在两种情况一个就是minIndex为负值(你没有看错,就是负值),一个就是minIndex为0而maxIndex大于0时。

    2.1.当minIndex为负值时,初始化的时候,为了保持UIWrapContent显示从0开始,拿本文例子来说,此时的情况就是当前页的左边还有内容。如下图示,此时设置的minIndex为负数,初始化结束后,0还是显示在首位,但是0的左边还是有元素的。

    当向右滑动时,0左边的值就会显示出来并且是负数。

    minIndex为负数时可以实现初始化跳转到指定页。比如说现在做的是关卡列表(一章就是一页),玩家已经打完第一章了,现在在打第二章了,那么玩家下次打开关卡界面的时候应该显示的是第二章(第二页),而不是第一章(第一页)。此时就可以用minIndex为负值来实现。
    2.2.当minIndex 为0,而maxIndex 为大于0的时候,就是正常初始化从0显示,0就是首位,0的左边没有cell。
    2.3.minIndex与maxIndex不等时,两个绝对值相加的数量就是要实现列表的数量。比如minIndex为 -10,maxIndex的值为 10,那么实现的列表总值为 20。

    综上可知,如果不做页面初始跳转的话,就可以指定minIndex为0,maxIndex 为 [列表总数 - 1]。如果做页面初始跳转的话就可以指定minIndex为跳转数的负值,[maxIndx为列表总数 + minIndex -1]。

    3). UIWrapContent初始化流程
    UIWrapContent中的代码不多,流程也比较简单。首先是 Start 方法,这个方法主要是初始化设置现有cell的位置,并且绑定OnMove方法,可以在滑动ScrollView时,会调用此方法,进而调用WrapContent()方法来做计算。
        /// Initialize everything and register a callback with the UIPanel to be notified when the clipping region moves.
        /// </summary>
    
        protected virtual void Start ()
        {
            SortBasedOnScrollMovement();
            WrapContent();
            if (mScroll != null) mScroll.GetComponent<UIPanel>().onClipMove = OnMove;
            mFirstTime = false;
        }

    先看SortBasedOnScrollMovement()方法。功能时为已添加的cell排序,排完后再调用ResetChildPositions方法

    /// <summary>
        /// Immediately reposition all children.
        /// </summary>
    
        [ContextMenu("Sort Based on Scroll Movement")]
        public  void SortBasedOnScrollMovement ()
        {
            if (!CacheScrollView()) return;
    
            // Cache all children and place them in order
            mChildren.Clear();
            for (int i = 0; i < mTrans.childCount; ++i)
                mChildren.Add(mTrans.GetChild(i));
    
            // Sort the list of children so that they are in order
            if (mHorizontal) mChildren.Sort(UIGrid.SortHorizontal);
            else mChildren.Sort(UIGrid.SortVertical);
            ResetChildPositions();
        }

    ResetChilPosition比较简单,同时也是我们可做扩展一个方法。方法功能就是根据 ScrollView为 Horizontal 或者 Vertical来为cell做水平或者垂直排列布局。如果不想这样直线排序就可以修改此方法,如下图示,改变cell的Y坐标后,可以实现指定位置排序,不必直线排序。

    /// <summary>
        /// Helper function that resets the position of all the children.
        /// </summary>
    
        void ResetChildPositions ()
        {
            for (int i = 0, imax = mChildren.Count; i < imax; ++i)
            {
                Transform t = mChildren[i];
                t.localPosition = mHorizontal ? new Vector3(i * itemSize, 0f, 0f) : new Vector3(0f, -i * itemSize, 0f);
            }
        }


    重点:WrapContent方法。在Start的时候会调用此方法,这个方法是整个UIWrapContent的核心部分。功能就是根据当前列表中cell移动后的位置与UIScrollView的中心位置做比较,如果超过了设定距离,则做cell的跳转处理。初始化的时候会发现方法一直会进下图红线标示的这段代码。

    在UpdateItem方法里,会把当前要跳转的cell相关值传给你绑定的方法。gameObject和index都是wrapContent中计算的,这里只计算了realIndex的值。至此UIWrapContent就初始化完毕了。

    /// <summary>
        /// Want to update the content of items as they are scrolled? Override this function.
        /// </summary>
    
        protected virtual void UpdateItem (Transform item, int index)
        {
            if (onInitializeItem != null)
            {
                int realIndex = (mScroll.movement == UIScrollView.Movement.Vertical) ?
                    Mathf.RoundToInt(item.localPosition.y / itemSize) :
                    Mathf.RoundToInt(item.localPosition.x / itemSize);
                onInitializeItem(item.gameObject, index, realIndex);
            }
        }

    上文中提到多放置一个cell不初始化的问题(如果遇见了的话)就可以在WrapContent中找到原因。多放一个cell的话,在循环最后一个cell时会进else if(distance - extents > 0.01f),没有进else if(mFirstTime) UpdateItem(t, i)。这是因为多出的那个cell距离超过了extents,此时就需要调整UIScrollView的size来规避一下。

    UIWrapContent初始化完毕后,剩下的任务就交给了 OnMove。滑动UIScrollView时,会不断调此方法,然后WrapContent再不断循环计算。

    /// <summary>
        /// Callback triggered by the UIPanel when its clipping region moves (for example when it's being scrolled).
        /// </summary>
    
        protected virtual void OnMove (UIPanel panel) { WrapContent(); }
    4). 具体使用UIWrapContent

    经过上文的解释,就可以明白地使用UIWrapContent了。通常情况下因为事先无法知道列表最终的显示数量,因而需要在程序运行时指定 minIndex 和 maxIndex。这样的话UIWrapContent脚本就不能在界面初始化的时候可用,所以需要在指定完minIndex和maxIndex才启用脚本。

    4.1. 摆放好UI后先禁用UIWrapContent脚本

    4.2.界面初始化的时候(比如在Start里面)为onInitializeItem绑定一个方法。

    4.3.获取数据后,根据你的数据量以及是否需要跳转,来指定minIndex和maxIndex的值,然后启用 UIWrapContent脚本。

    本文Demo演示代码如下:

    using UnityEngine;
    using System.Collections;
    
    public class WrapContent : MonoBehaviour
    {
        public GameObject _wrap;
        UIWrapContent _wrapScript;
        public GameObject _box;
    
        // Use this for initialization
        void Awake()
        {
            //获取脚本
            _wrapScript = _wrap.GetComponent<UIWrapContent>();
    
            //绑定方法
            _wrapScript.onInitializeItem = OnUpdateItem;
        }
    
    
        // Update is called once per frame
        void Update()
        {
            //Test
            if (Input.GetKeyDown(KeyCode.A))
            {
                _wrapScript.minIndex = -10;
                _wrapScript.maxIndex = 10;
    
                //启用脚本
                _wrapScript.enabled = true;
    
    
            }
        }
    
        void OnUpdateItem(GameObject go, int index, int realIndex)
        {
            //Debug.Log("index = " + index);
            Debug.Log("realIndex = " + realIndex);
            Testbox tb = go.GetComponent<Testbox>();
            tb.SetNumber(realIndex.ToString());
        }
    }
    
    
    using UnityEngine;
    using System.Collections;
    
    public class Testbox : MonoBehaviour
    {
    
        public UILabel number;
    
        public void SetNumber(string text) 
        {
            number.text = text;
        }
    }
  • 相关阅读:
    项目中docker swarm实践
    Spring的分模块开发的配置
    单点登录
    在服务器搭建git服务器
    Learn Git Lesson06
    kafka Poll轮询机制与消费者组的重平衡分区策略剖析
    gulp初体验
    vue-cli3中axios如何跨域请求以及axios封装
    vue-cli3中怎么配置vue.config.js文件
    svn的下载与安装,使用,包教包会!!!
  • 原文地址:https://www.cnblogs.com/lovexb/p/4595246.html
Copyright © 2011-2022 走看看