NGUI 滑动组件:UIScrollView

UIScrollView:滑动组件,核心方法是Press、Drag、CalculateConstrainHelper。
核心方法、属性:
UIScrollView:滑动列表
        property:
        smoothDragStart:按下时立即开始滑动还是添加过渡状态
        restrictWithinPanel:滑动结束时是否调用RestrictWithinBounds,让content尽可能多地位于panel内
        canMoveHorizontally/canMoveVertically:是否支持横向/纵向滑动,根据movement判断
        shouldMoveHorizontally/shouldMoveVertically:根据包围盒大小、panel裁剪区域判断是否可以滑动
        shouldMove:所有显示内容是否都不再裁剪范围内,根据包围盒bound和裁剪区域判断
        
        function:
        OnScrollBar:滚动条拉动事件
        RestrictWithinBounds:让content尽可能多地位于panel内,返回此操作所需的Vector2偏移量(位移Vector2,让content尽可能多地位于panel内)
        UpdateScrollbars:更新滚动条
        SetDragAmount:更新uiPanel的坐标、clipOffset、scrollBar
        ResetPosition:重置回原点
        UpdatePosition、OnScrollBar:调用SetDragAmount更新panel坐标
        MoveRelative:移动UIPanel的显示内容
        Press:把滑动后缓冲的动力归零、关闭滑动后的缓冲运动(滑动后是指手指离开屏幕)、记录按下点的世界坐标、创建一个平面;其实就是为滑动做准备.如果为false,会尽量把界面拉回裁剪区域内
        drag:计算滑动偏移、计算缓冲动力、移动
        LateUpdate:计算滑动结束后的一个缓冲效果,就是手指松开以后会继续滑动一段距离,比较平滑
movement:滑动方向
dragEffect:滑动效果,是否有惯性
constrainOnDrag:看代码是处理MomentumAndSpring效果下,滑动超过边界的情况
Vector3 constraint = CalculateConstrainHelper(canMoveHorizontally, canMoveVertically);
if (constrainOnDrag)
   RestrictWithinBoundsHelper(true, constraint);
 
public bool RestrictWithinBoundsHelper (bool instant, Vector3 constraint)
{
   if (constraint.sqrMagnitude > 0.1f)
   {
      if (!instant && dragEffect == DragEffect.MomentumAndSpring)
      {
         // Spring back into place
         Vector3 pos = mTrans.localPosition + constraint;
         pos.x = Mathf.Round(pos.x);
         pos.y = Mathf.Round(pos.y);
         SpringPanel.Begin(mPanel.gameObject, pos, 8f);
      }
      else
      {
         // Jump back into place
         MoveRelative(constraint);
 
         // Clear the momentum in the constrained direction
         if (Mathf.Abs(constraint.x) > 0.01f) mMomentum.x = 0;
         if (Mathf.Abs(constraint.y) > 0.01f) mMomentum.y = 0;
         if (Mathf.Abs(constraint.z) > 0.01f) mMomentum.z = 0;
         mScroll = 0f;
      }
      return true;
   }
   return false;
}
disableDragIfFits:当界面全部位于视图内(裁剪区域)时,true:无法拖动;false:可以拖动内容到边界外,不过会有阻力
smoothDragStart:按下时立即开始滑动还是从0速度开始,当开始拖动滑块的时候,如果勾上了,则有一个从 0 变成拖动速度的平滑现象,如果不勾,则开始拖动时就与拖动速度一样
restrictWithinPanel:滑动结束时是否调用RestrictWithinBounds,让content尽可能多地位于panel内
Momenturm Amount:滑动后,惯性滑行的距离
 
开始之前,我们先思考下,在unity里要实现拖动一张图片,最方便的实现方法是什么?
其实就3步:鼠标点触摸touch是一样的
1.在鼠标按下时记录按下点的坐标,
2.鼠标抬起时,把当前的鼠标位置坐标减去按下前的坐标,这就是滑动的偏移量,当然一般会在update轮询鼠标坐标,以便在拖动时更新拖动对象的坐标。NGUI的滑动事件是在UICamera的update方法里实现的,通过go.SendMessage方法分发出去,所以只需要在Drag处理逻辑就可以了。
3.这边还可能涉及到坐标的转换,点击坐标一般是屏幕坐标。
Ray ray = smoothDragStart ?
   UICamera.currentCamera.ScreenPointToRay(UICamera.currentTouch.pos - mDragStartOffset) :
   UICamera.currentCamera.ScreenPointToRay(UICamera.currentTouch.pos);
 
float dist = 0f;
 
if (mPlane.Raycast(ray, out dist))
{
   Vector3 currentPos = ray.GetPoint(dist);
   Vector3 offset = currentPos - mLastPos;
   mLastPos = currentPos;
}
NGUI的UIScrollView实现逻辑跟上面是一样:
1.通过UIDragScrollView监听UICamera发出的OnPress/OnDrag/OnScroll事件,然后调用UIScrollView对应的接口。
2.在Press/Drag里处理拖动事件。
3.UIScrollView在拖动结束的时候会把滑动内容尽可能的显示在裁剪区域内,这是可选的。比如下面的图1会被修改成图2显示,尽可能多的让图片和裁剪区域重合。
4.UIScrollView实现了惯性的效果,简单说就是手指抬起后,还会惯性滑动一段距离。
SpringPanel:移动panel,配合ScrollView使用,移动的是UIPanel的裁剪区域clipOffset。
        function:
        Update:unity的Update函数只在OnEnable的时候调用,在这边轮询AdvanceTowardsPosition方法实现坐标移动
        AdvanceTowardsPosition:根据strength、target对坐标差值,最后赋值给mPanel.clipOffset实现panel位移效果
        Begin:开始动画,实际是设置了组件的enabled = true
        end:结束动画,实际是设置了组件的enabled = false
 
UIWrapContent:基于UIScrollView实现循环滑动列表。
核心方法WrapContent。
原理实现:
1.NGUI的UIScrollView滑动时,实际上时修改的UIPanel的LocalPosition和clipOffset,拿在初始化的时候就可以确定每个item(data)的LocalPosition。
例如第i行j列,相对于父节点的坐标:
private Vector3 GetItemLocalPos(int i, int j)
{
    float posX = size.x * i;
    float posY = size.y * j;
    if (m_moveType == UIScrollView.Movement.Vertical)
    {
        posX = size.x * j;
        posY = size.y * i;
    }
 
    Vector3 localPos = new Vector3(posX, -posY, 0);
    return localPos;
}
初始化完成的大概是这样的,无非就是按顺序排序子对象
2.监听滑动事件,UIPanel的clipOffset发生变化时,会触发onClipMove事件,我们监听这个事件,然后在事件里实现交换逻辑。
3.交换逻辑。
1.WrapContent的交换逻辑:轮询每个对象,如果对象不在裁剪区域内,移动对象(最下面的移动到最上面,最左侧的移动到最右侧)。
Transform t = mChildren[i];
float distance = t.localPosition.x - center.x;
float extents = itemSize * mChildren.Count * 0.5f;
 
if (distance < -extents)
{
   Vector3 pos = t.localPosition;
   pos.x += ext2;
   distance = pos.x - center.x;
   int realIndex = Mathf.RoundToInt(pos.x / itemSize);
 
   if (minIndex == maxIndex || (minIndex <= realIndex && realIndex <= maxIndex))
   {
      t.localPosition = pos;
      UpdateItem(t, i);
   }
   else allWithinRange = false;
}
else if (distance > extents)
{
   Vector3 pos = t.localPosition;
   pos.x -= ext2;
   distance = pos.x - center.x;
   int realIndex = Mathf.RoundToInt(pos.x / itemSize);
 
   if (minIndex == maxIndex || (minIndex <= realIndex && realIndex <= maxIndex))
   {
      t.localPosition = pos;
      UpdateItem(t, i);
   }
   else allWithinRange = false;
}
2.优化逻辑:监听本次滑动的距离,滑动超过一格才处理,而且只处理最边缘一列(最下面、最上面、最左侧、最右侧)。
//滑动事件
private void OnPanelLoopClipMove(UIPanel panel) {
    Vector3 delata = m_panelInitPos - panel.transform.localPosition;
    float distance = -1;
 
    int index; //当前显示出来的第一个格子,在grid数据中的index
    distance = delata.y != 0 ? delata.y : delata.x;
    // 满的时候向上滑不管它(滑到头了)
    if (distance > 0 && m_moveType == UIScrollView.Movement.Vertical)
        return;
    if (distance < 0 && m_moveType == UIScrollView.Movement.Horizontal)
        return;
 
    CalDistance(distance, out distance, out index);
    // 拖拽不满一个单元格
    if (index == m_lastDataIndex)
        return;
 
    // 拉到底了
    if (index + m_fillCount >= m_dataArrayNum) {
        index = m_dataArrayNum - m_fillCount;
    }
    // 重刷
    int offset = Math.Abs(index - m_lastDataIndex);
    for (int i = 1; i <= offset; i++)
    {
        //上(左)移动到下(右)
        MoveGridItem(m_lastDataIndex < index);
    }
 
    m_lastDataIndex = index;
}
 
private void CalDistance(float distance, out float targetDis, out int index)
{
    FixBoxPostion();
    targetDis = Mathf.Abs(distance);
    index = Mathf.FloorToInt(targetDis / cellLength);
}
 
private void MoveGridItem(bool isTopToBottom) {
    // 判断是否是 上(左)移动到下(右)
    int curIndex;
    int itemIndex;
    int sign;
    if (isTopToBottom) {
        curIndex = m_maxIndex + 1;
        itemIndex = 0;
        sign = 1;
    } else {
        curIndex = m_minIndex - 1;
        itemIndex = m_items.Count - 1;
        sign = -1;
    }
 
    List<Transform> items;
    items = m_items[itemIndex];
 
    int targetIndex = itemIndex == 0 ? m_items.Count - 1 : 0;
 
    m_items.Remove(items);
    m_items.Insert(targetIndex, items);
 
    for (int i = 0; i < items.Count; i++) {
        int dataIndex = curIndex * maxPerLine + i;
        if (dataIndex < 0) {
            break;
        }
        if (dataIndex > m_loopMax - 1) {
            break;
        }
 
        if (m_listener != null)
        {
            int index = Array.IndexOf(m_itemsTran, items[i]);
            m_listener(index, dataIndex);
        }
 
        items[i].localPosition = m_itemPos[dataIndex];
    }
 
    m_minIndex += sign;
    m_maxIndex += sign;
}
4.分发事件通知obj。
m_listener(index, dataIndex); index是对象数组的下标,dataIndex是数据数组的下表
local item = self._friendItems[objIdx+1] --lua
local data = self._friendDatas[dataIdx+1]
5. UIWrapContent待扩展功能:
1.只支持单行或单列。
2.固定子节点,适配不方便。
3.轮询全部节点没必要。
原文地址:https://www.cnblogs.com/wang-jin-fu/p/13509096.html