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