Unity ScrollView无限循环左右滑动翻页带阻尼效果

https://blog.csdn.net/Sam_ONE/article/details/60467911借鉴优化而来

上面的例子是上下滑动,而且滑动的过程中,子节点的顺序会打乱,这里改成左右滑动,并且加了个排序,使滑动过程中子节点保持正确的顺序。把脚本挂在ScrollView上面,子节点加上Item脚本(随意实现)基本上就可以了

using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class MainScrollView : MonoBehaviour, IEndDragHandler, IBeginDragHandler
{
    private LTDescr lt;//需要导入LeanTween插件
    //[组件信息]
    private ScrollRect scrollRect;
    private Transform ContentObj;    //父节点
    private Item[] itemList;                //子节点管理

    //[UI效果设置参数]
    private float m_inertiaTime = 0f;    //惯性作用时间 
    private float m_BackTime = 0.3f;     //回弹时间
    private float m_startDecelert = 0.05f;//初始惯性加速度

    //[Item设置参数]
    private float itemWidth;               //子节点宽度
    private float m_CurPosX;                    //Content相对X位置(0为参考)
    private int m_Index = 0;                    //X位置与子节点宽度的整倍数
    private float m_Surplus = 0;                //X位置与子节点宽度的整倍后剩余
    private float MaxPosX = 0;                  //根据Item数量、间距、item宽度计算得到的X轴最大的数值
    private Vector3 m_StartPos;                 //记录每次需要居中前的Content位置
    private float m_OldPosX;
    //[Item包含数据相关]
    private int RealAmount;             //真实的List<ResResut>Count
    private int IndexPlus;
    private int fistIndex = 0;
    private int lastIndex = 0;
    private int m_CurrentIndex = 0;
    private int m_PreviousIndex = 0;
    [SerializeField]
    private RectTransform content;

    void Awake()
    {
        if (!GetComponent<ScrollRect>())
        {
            gameObject.AddComponent<ScrollRect>();
            var scrollRect = GetComponent<ScrollRect>();
            scrollRect.vertical = false;
            scrollRect.movementType = ScrollRect.MovementType.Unrestricted;
            scrollRect.content = content;
        }
        scrollRect = GetComponent<ScrollRect>();
        ContentObj = scrollRect.content;
        itemWidth = GetComponent<RectTransform>().sizeDelta.x;
        if (ContentObj.GetComponent<GridLayoutGroup>())
            ContentObj.GetComponent<GridLayoutGroup>().enabled = false;
        ContentObj.transform.localPosition = new Vector3(0, ContentObj.localPosition.y);
        if (GetComponent<Image>())
            GetComponent<Image>().raycastTarget = true;
    }

    // Use this for initialization
    void Start()
    {
        //Content下的子物体排序,使子物体在拖动时保持顺序
        Item[] items = ContentObj.GetComponentsInChildren<Item>();
        int itemsLen = items.Length;
        Item[] itemsTmp = new Item[itemsLen];
        int index = (itemsLen - 1) / 2;
        for (int i = 0; i < itemsLen; i++)
        {
            itemsTmp[i] = items[(index - i) >= 0 ? index - i : itemsLen + index - i];
        }
        RefreshMusicList(itemsTmp);
    }

    /// <summary>
    /// Content的初始位置X赋值
    /// </summary>
    void ContentPointInit()
    {
        if (ContentObj.childCount % 2 == 0)//判断子对象的奇偶
        {
            ContentObj.localPosition = new Vector3(ContentObj.localPosition.x - (itemWidth / 2), ContentObj.localPosition.y, ContentObj.localPosition.z);
            m_OldPosX = -itemWidth / 2;
        }
    }

    // Update is called once per frame
    void Update()
    {
        m_PreviousIndex = m_CurrentIndex;
        CalculationContentChanege();
    }

    /// <summary>
    /// 初始更新列表内容
    /// </summary>
    /// <param name="Rr">对象</param>
    public void RefreshMusicList(Item[] Rr)
    {
        MaxPosX = Rr.Length / 2 * itemWidth;      //根据Item数量计算得到最顶上Item的Pos,用于往下一次推算之后的Pos
        if (Rr.Length % 2 == 0)
            MaxPosX -= itemWidth / 2;

        RealAmount = Rr.Length;
        gameObject.SetActive(true);

        itemList = Rr;
        for (int i = 0; i < itemList.Length; i++)
        {
            itemList[i].transform.localPosition = new Vector3(MaxPosX - i * itemWidth, 0, 0);
        }
        ContentPointInit();                     //更新列表初始位置
        fistIndex = 0;                          //记录首末序号
        lastIndex = RealAmount - 1;
    }


    /// <summary>
    /// Item定位
    /// </summary>
    public void LocateItem()
    {
        lt = LeanTween.value(0, 1, m_inertiaTime).setOnStart(() =>
        {
            scrollRect.decelerationRate = m_startDecelert;
        }).setOnUpdate((float f) =>
        {
            scrollRect.decelerationRate = Mathf.Lerp(m_startDecelert, 0, f);
        }).setOnComplete(() =>
        {
            lt = LeanTween.value(0, 1, m_BackTime).setOnStart(() =>
            {
                m_StartPos = ContentObj.localPosition;
                m_CurPosX = ContentObj.localPosition.x;
                m_Index = (int)(ContentObj.childCount % 2 == 0 ? (m_CurPosX + itemWidth / 2) / itemWidth : m_CurPosX / itemWidth);
                m_Surplus = m_CurPosX % itemWidth;
                if (m_CurPosX - m_OldPosX < 0)
                {
                    IndexPlus = m_CurPosX < 0 ? -1 : 0;
                }
                else
                {
                    if (ContentObj.childCount % 2 == 0) m_CurPosX += itemWidth / 2;
                    IndexPlus = m_CurPosX < 0 ? 0 : 1;
                }
                if (Mathf.Abs(m_Surplus) >= (itemWidth / 30))
                    m_Index += IndexPlus;
            }).setOnUpdate((float f) =>
            {
                float posX = m_Index * itemWidth;
                if (ContentObj.childCount % 2 == 0) posX = m_Index * itemWidth - (itemWidth / 2);
                ContentObj.localPosition = Vector3.Lerp(m_StartPos, new Vector3(posX, ContentObj.localPosition.y), f);
                m_OldPosX = ContentObj.localPosition.x;
            });
        });
    }

    void IEndDragHandler.OnEndDrag(PointerEventData eventData)
    {
        scrollRect.OnEndDrag(eventData);
        m_inertiaTime = Mathf.Clamp(Mathf.Clamp01(Math.Abs(eventData.delta.x * 0.008f)), 0, 0.1f);      //根据拖拽的速度限制惯性运行的时间
        LocateItem();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (lt != null)
            lt.reset();
    }

    void Circulation(int _dir)
    {
        if (_dir > 0)
        {
            itemList[fistIndex].transform.localPosition = itemList[lastIndex].transform.localPosition - new Vector3(itemWidth, 0, 0);
            lastIndex = fistIndex;
            fistIndex = (fistIndex + 1) % RealAmount;
        }
        else if (_dir < 0)
        {
            itemList[lastIndex].transform.localPosition = itemList[fistIndex].transform.localPosition + new Vector3(itemWidth, 0, 0);
            fistIndex = lastIndex;
            lastIndex = (lastIndex + RealAmount - 1) % RealAmount;
        }
    }

    private void CalculationContentChanege()
    {
        m_CurrentIndex = Mathf.FloorToInt(ContentObj.localPosition.x / itemWidth);  //向下取整获得变化的Index!!!!!!!!!!
        if (m_PreviousIndex != m_CurrentIndex)
        {
            for (int i = 0; i < Mathf.Abs(m_CurrentIndex - m_PreviousIndex); i++)
                Circulation(m_CurrentIndex - m_PreviousIndex);
        }
    }
}

----------------------------------------分割线----------------------------------------------------

上面的版本使用的场景是拖动页数比较多(基本超过5页)且子页宽度超过屏幕2/3的情况,在这样的情况下使用上面的代码没有问题,但是当子页只有只有2、3页且子页的比较小时就有很多问题了。主要问题一是可以把子页全部拖出显示区域造成显示区域空白,二是拖动的时候拖动距离需要超过1/2才会切换,造成一半显示区域是空白。经过几天的修改优化出了一个新版本,拖动的时候可以无缝衔接,同时暴力拖曳不会把所有子页都拖出显示区域,同时还加入了自动切换。

public class FocusScrollView : MonoBehaviour, IEndDragHandler, IBeginDragHandler
{
    private LTDescr lt, ltAuto;

    //[组件信息]
    [SerializeField]
    private ScrollRect scrollRect;
    [SerializeField]
    private Transform contentObj;       //父节点
    private ViewItem[] itemList;             //子节点管理

    //[UI效果设置参数]
    private float m_inertiaTime = 0f;    //惯性作用时间 
    private float m_BackTime = 0.2f;     //回弹时间
    private float m_startDecelert = 0.05f;//初始惯性加速度

    //[ViewItem设置参数]
    private float itemWidth;               //子节点宽度
    private float m_CurPosX;                    //Content相对X位置(0为参考)
    private int m_Index = 0;                    //X位置与子节点宽度的整倍数
    private float m_Surplus = 0;                //最小偏移量,滑动距离超过这个值就切换
    private float MaxPosX = 0;                  //根据ViewItem数量、间距、item宽度计算得到的X轴最大的数值
    private Vector3 m_StartPos;                 //记录每次需要居中前的Content位置
    private float m_OldPosX;
    private int realAmount;             //Count
    private int IndexPlus;
    //[计算相关数值]
    private int firstIndex = 0;
    private int lastIndex = 0;
    private float m_CurrentIndex = 0;
    private float m_PreviousIndex = 0;

    public Action<int> completeDragHandle;//滑动或者左右按钮切换结束,返回当前页码(从0开始)
    private bool isReadied = false;
    private int curIndex = 0;//当前页码

    private float lerpPos = 0;//页数为奇数时,设置偏移量=itemWidth使列表自动切换,手动切换时置为0
    private Vector3 itemWidthVec;
    private void InitGroup()
    {
        itemWidth = GetComponent<RectTransform>().sizeDelta.x;
        itemWidthVec = new Vector3(itemWidth, 0, 0);
        if (contentObj.GetComponent<GridLayoutGroup>())
            contentObj.GetComponent<GridLayoutGroup>().enabled = false;
        contentObj.transform.localPosition = new Vector3(0, 0);
    }

    private void Start()
    {
        Refresh();//外部调用的接口,页面准备好了调用此方法即可以开始切换
    }

    // Use this for initialization
    public void Refresh()
    {
        InitGroup();
        ViewItem[] items = contentObj.GetComponentsInChildren<ViewItem>(true);
        int itemsLen = items.Length;
        ViewItem[] itemsTmp = new ViewItem[itemsLen];
        int index = itemsLen / 2 + 1;//重新排序,将[len/2 + 1, len]移到数组前面
        for(int i = 0; i < itemsLen; i++)
        {
            itemsTmp[i] = items[i + index < itemsLen ? i + index : index + i - itemsLen];
        }
        RefreshMusicList(itemsTmp);
        if(realAmount == 1) return;
        isReadied = true;
        scrollRect.enabled = true;
        AutoRepeatMove();
    }

    private void AutoRepeatMove()
    {
        lerpPos = itemWidth;
        CancelInvoke("LocateItemAuto");
        InvokeRepeating("LocateItemAuto", 3, 5);
    }

    /// <summary>
    /// Content的初始位置X赋值
    /// </summary>
    void ContentPointInit()
    {
        if (contentObj.childCount % 2 == 0)//判断子对象的奇偶
        {
            contentObj.localPosition = new Vector3(contentObj.localPosition.x - (itemWidth / 2), 0);
            m_OldPosX = -itemWidth / 2;
        }
    }

    // Update
    void Update()
    {
        if (!isReadied || !this || !contentObj) return;
        m_PreviousIndex = m_CurrentIndex;
        CalculationContentChanege(contentObj.localPosition.x);
    }

    /// <summary>
    /// 初始更新列表内容
    /// </summary>
    /// <param name="Rr">对象</param>
    private void RefreshMusicList(ViewItem[] Rr)
    {
        realAmount = Rr.Length;
        MaxPosX = realAmount / 2 * itemWidth;      //根据Item数量计算得到最顶上Item的Pos,用于往下一次推算之后的Pos
        if (realAmount % 2 == 0)
            MaxPosX -= itemWidth / 2;


        itemList = Rr;
        for (int i = 0; i < realAmount; i++)
        {
            itemList[i].transform.localPosition = new Vector3(MaxPosX - i * itemWidth, 0, 0);
            itemList[i].gameObject.SetActive(true);
        }
        ContentPointInit();                     //更新列表初始位置
        firstIndex = 0;                          //记录首末序号
        lastIndex = realAmount - 1;
    }

    /// <summary>
    /// ViewItem定位
    /// </summary>
    private void LocateItem()
    {
        lt = LeanTween.value(0, 1, m_inertiaTime)
        .setOnStart(InitOnStart)
        .setOnUpdate(InitOnUpdate)
        .setOnComplete(InitOnComplete);
    }

    private void InitOnStart()
    {
        scrollRect.decelerationRate = m_startDecelert;
    }

    private void InitOnUpdate(float f)
    {
        scrollRect.decelerationRate = Mathf.Lerp(m_startDecelert, 0, f);
    }

    private void InitOnComplete()
    {
        lt = LeanTween.value(0, 1, m_BackTime)
        .setOnStart(OnStart)
        .setOnUpdate(OnUpdate)
        .setOnComplete(OnComplete);
    }

    private void LocateItemAuto()
    {
        ltAuto = LeanTween.value(0, 1, m_inertiaTime)
        .setOnStart(InitOnStart)
        .setOnUpdate(InitOnUpdate)
        .setOnComplete(InitOnCompleteAuto);
    }

    private void InitOnCompleteAuto()
    {
        ltAuto = LeanTween.value(0, 1, m_BackTime)
        .setOnStart(OnStart)
        .setOnUpdate(OnUpdateAuto)
        .setOnComplete(OnComplete);
    }

    int previous_m_Index = 0;
    private void OnUpdateAuto(float f)
    {
        if(!this || !contentObj) return;
        //包含偶数子项时,快速向左滑动后切换到自动滑动,列表可能不会动,
        // 而是一直重复显示当前页,原因未明(m_Index未变)
        if(m_Index == previous_m_Index && contentObj.childCount % 2 == 0)
        {
           previous_m_Index = m_Index++;
        }
        posX = m_Index * itemWidth + lerpPos;
        UpdatePos(f, posX);
    }

    private void OnStart()
    {
        if(!this || !contentObj) return;
        m_StartPos = contentObj.localPosition;
        m_CurPosX = contentObj.localPosition.x;
        m_Index = (int)(contentObj.childCount % 2 == 0 ? (m_CurPosX + itemWidth / 2) / itemWidth : m_CurPosX / itemWidth);
        m_Surplus = m_CurPosX % itemWidth;
        if (m_CurPosX - m_OldPosX < 0)
        {
            IndexPlus = m_CurPosX < 0 ? -1 : 0;
        }
        else
        {
            if (contentObj.childCount % 2 == 0) m_CurPosX += itemWidth / 2;
            IndexPlus = m_CurPosX < 0 ? 0 : 1;
        }
        if (Mathf.Abs(m_Surplus) >= (itemWidth / 30))
            m_Index += IndexPlus;
            
    }

    float posX;
    private void OnUpdate(float f)
    {
        previous_m_Index = m_Index;
        posX = m_Index * itemWidth;
        UpdatePos(f, posX);
    }
    
    private void OnComplete()
    {
        if(!this || !contentObj) return;
        int index = realAmount / 2 + 1;
        for(int i = 0; i < realAmount; i++)
        {
            //父容器与卡片相对运动,所以最终显示在中间的卡片其坐标与父容器坐标和为0
            if(contentObj.localPosition.x + itemList[i].transform.localPosition.x == 0)
            {
                //因为数组的[len/2 + 1, len]项被移到了前面,所以curIndex需要重新计算
                if(realAmount - index > i)
                {
                    curIndex = i + index;
                }else
                {
                    curIndex = i + index - realAmount;
                }
                break;
            }
        }
        completeDragHandle?.Invoke(curIndex);
    }

    private void UpdatePos(float f, float p)
    {
        if(!this || !contentObj) return;
        if (contentObj.childCount % 2 == 0) p = m_Index * itemWidth - (itemWidth / 2);
        contentObj.localPosition = Vector3.Lerp(m_StartPos, new Vector3(p, 0), f);
        m_OldPosX = contentObj.localPosition.x;
    }
    
    void IEndDragHandler.OnEndDrag(PointerEventData eventData)
    {
        scrollRect.enabled = true;
        isStartMove = false;
        scrollRect.OnEndDrag(eventData);
        ChildScrollEndDrag(eventData);
        AutoRepeatMove();
    }

    bool isStartMove = false;
    float oldPosX = 0;
    public void ChildScrollEndDrag(PointerEventData eventData)
    {
        m_inertiaTime = Mathf.Clamp(Mathf.Clamp01(Math.Abs(eventData.delta.x * 0.008f)), 0, 0.1f);      //根据拖拽的速度限制惯性运行的时间
        LocateItem();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        lerpPos = 0;
        CancelInvoke("LocateItemAuto");
        isStartMove = true;
        oldPosX = contentObj.localPosition.x;
    }

    void Circulation(float _dir)
    {
        if(itemList == null) return;
        if (_dir > 0)
        {
            itemList[firstIndex].transform.localPosition = itemList[lastIndex].transform.localPosition - itemWidthVec;
            lastIndex = firstIndex;
            firstIndex = (firstIndex + 1) % realAmount;
        }
        else if (_dir < 0)
        {
            itemList[lastIndex].transform.localPosition = itemList[firstIndex].transform.localPosition + itemWidthVec;
            firstIndex = lastIndex;
            lastIndex = (lastIndex + realAmount - 1) % realAmount;
        }
    }

    private void CalculationContentChanege(float pos)
    {
        //拖动的时候,防止暴力拖曳把整个列表拖出显示区域
        if(isStartMove && Mathf.Abs(pos - oldPosX) > itemWidth) 
        {
            scrollRect.enabled = false;
            return;
        }
        m_CurrentIndex = Mathf.FloorToInt((pos + itemWidth / 2) / itemWidth);  //向下取整获得变化的Index!!!!!!!!!!

        if (m_PreviousIndex != m_CurrentIndex)
        {
            for (int i = 0; i < Mathf.Abs(m_CurrentIndex - m_PreviousIndex); i++)
                Circulation(m_CurrentIndex - m_PreviousIndex);
        }
    }

    //重置状态
    public void Reset() 
    {
        CancelInvoke("LocateItemAuto");
        isReadied = false;
        scrollRect.enabled = false;
        m_CurrentIndex = 0;
        m_PreviousIndex = 0;
    }
}

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
实现无限循环ScrollView,可以使用 Unity 的 UI 布局组件和代码结合的方式来实现。 首先,在 ScrollView 中添加一个 Content 布局组件,用于控制 ScrollView 中的子控件排列。 然后,在代码中动态生成需要显示的子控件,并将其添加到 Content 中。为了实现无限循环,需要在首尾各添加一个相同的子控件,这样在滑动到末尾时,可以无缝地切换到开头,实现循环。 以下是一个简单的示例代码,可以放在 ScrollView 的父物体上: ```csharp using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class InfiniteScrollView : MonoBehaviour { public GameObject itemPrefab; public int itemCount = 10; private List<GameObject> itemList = new List<GameObject>(); private RectTransform contentRectTransform; private float itemHeight; private int currentItemIndex = 0; void Start() { // 获取 Content 的 RectTransform 组件 contentRectTransform = GetComponentInChildren<ScrollRect>().content; // 获取子控件的高度 itemHeight = itemPrefab.GetComponent<RectTransform>().rect.height; // 生成子控件 for (int i = 0; i < itemCount + 2; i++) { GameObject item = Instantiate(itemPrefab, contentRectTransform); item.transform.localPosition = new Vector3(0, -itemHeight * i, 0); itemList.Add(item); } // 更新 Content 的高度 contentRectTransform.sizeDelta = new Vector2(contentRectTransform.sizeDelta.x, itemHeight * (itemCount + 2)); // 更新子控件的内容 UpdateItemContent(); } void Update() { // 判断是否需要切换子控件 if (Input.GetKeyDown(KeyCode.UpArrow)) { currentItemIndex++; if (currentItemIndex > itemCount + 1) { currentItemIndex = 1; } UpdateItemContent(); } else if (Input.GetKeyDown(KeyCode.DownArrow)) { currentItemIndex--; if (currentItemIndex < 1) { currentItemIndex = itemCount + 1; } UpdateItemContent(); } } void UpdateItemContent() { // 更新子控件的内容 for (int i = 0; i < itemList.Count; i++) { int index = currentItemIndex - (itemList.Count - i - 1); if (index <= 0) { index += itemCount; } else if (index > itemCount) { index -= itemCount; } itemList[i].GetComponentInChildren<Text>().text = "Item " + index; } // 更新 Content 的位置 contentRectTransform.anchoredPosition = new Vector2(contentRectTransform.anchoredPosition.x, itemHeight * (currentItemIndex - 1)); } } ``` 这个示例代码实现了一个简单的无限循环 ScrollView,可以根据实际需求进行修改。其中,itemPrefab 是用于生成子控件的预制体,itemCount 是需要显示的子控件个数。在 UpdateItemContent 函数中,根据当前的 currentItemIndex 来更新子控件的内容,并将 Content 移动到对应的位置。在 Update 函数中,通过监听上下箭头键来模拟手指滑动,实现切换子控件的效果

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值