在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;
    }
}

 

posted @ 2015-06-23 13:31  春花无实  阅读(2463)  评论(3编辑  收藏  举报