如何使用Unity制作PageView

1,331 阅读5分钟

微信公众号: [游戏内圈]
关注可了解更多的教程。问题或建议,请公众号留言;

模型处理

1:新建一个Scroll View组件。
2:移除Scrollbar Horizontal和Scrollbar Vertical。
3:移除本身ScrollRect。
4:重新添加ScrollRect,作用:确保所有属性清空,改变value时不会滑动。
5:调整ScrollView宽高,按需求调整。

C#文件处理

1:创建一个C#文件,名字随意,此处我取名为Page_View,继承MonoBehaviour, IBeginDragHandler, IEndDragHandler.

会报错,不用怕。

写入两个函数

/// <summary>
/// 开始滑动
/// </summary>
/// <param name="eventData"></param>
public void OnBeginDrag(PointerEventData eventData)
{
}

/// <summary>
/// 滑动结束
/// </summary>
/// <param name="eventData"></param>
public void OnEndDrag(PointerEventData eventData)
{

}

导入引用

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

2:得到Content节点以及需要设置属性

/是否循环
[Tooltip("是否可循环")] public bool isCirculation = true;

[Tooltip("所有Item的父节点")] public GameObject ContentGo;

[Tooltip("所有Item是否宽度一致")] public bool isItemUniform = true;

[Tooltip("移动速度")] public float _speed = 500;

[Tooltip("初始显示哪个Item,用来判断是否到了最边上,不能再移动")]
public int initShowIndex = 1;

[Tooltip("是否可以点击Item移动,相对显示位置Index移动")]
public bool isClickItemMove = true;

[Tooltip("设置每次移动的间隔,如果不设置,按第一个和最后一个的位置来算平均值,锚点需要一致")]
public float _interval = 0;

添加代码,并在unity中拖入Content节点。

3:添加所需要属性

public int limit = 50;

//当前显示的Index,初始为0
private int showIndex = 0;

//当showIndex为0时,content的位置
private float contentInitPositionX = 0;

//所有Item列表
private List<RectTransform> _transformList = new List<RectTransform>();

private List<float> _positionList = new List<float>();

//第一个Item
private RectTransform _initRect;

//最后一个Item
private RectTransform _endRect;

//当前点击事件Id
//按下时鼠标坐标X值
private float _initMouserX;

private Transform _contentTransform;

/// <summary>
/// scroll的最小值X和最大值X
/// </summary>
private Vector2 scrollInitEndX;

//改变ScollView位置
private Vector2 _changeScorllViewVector2 = new Vector2();

//循环时改变Item位置
private Vector2 _changeItemVector2 = new Vector2();

//需要移动的目标位置
private float endPositon = 0;

准备工作做完了,先不要管属性有什么用,开始写代码,一步一步来。

三:处理函数
1:初始化

// Start is called before the first frame update
void Awake()
{
    Init();
    RecordContentPosition();
    InitPanleView();
    if (isClickItemMove)
    {
        SetItemClick();
    }
}

private void Init()
{
    _positionList.Clear();


    _contentTransform = ContentGo.transform;
    var length = _contentTransform.childCount;
    if (length <= 0)
    {
        Debug.Log("Scroll子节点数量为空");
        return;
    }

    for (int i = 0; i < length; i++)
    {
        RectTransform tr = _contentTransform.GetChild(i).GetComponent<RectTransform>();
        if (i == 0) _initRect = tr;
        else if (i == length - 1) _endRect = tr;
        _transformList.Add(tr);
        _positionList.Add(tr.localPosition.x);
    }

    if (isItemUniform)
    {
        if (_interval == 0) _interval = (_positionList[_positionList.Count - 1] - _positionList[0]) / (_positionList.Count - 1);
    }
}

//记录Content初始位置
private void RecordContentPosition()
{
    showIndex = 0;
    endPositon = _contentTransform.localPosition.x;
    contentInitPositionX = _contentTransform.localPosition.x;
}

/// <summary>
/// 初始化可视化区域
/// </summary>
private void InitPanleView()
{
    var rectTransform = gameObject.GetComponent<RectTransform>();
    scrollInitEndX = GetInitEndXByRect(rectTransform);
}

/// <summary>
/// 设置Item点击事件
/// </summary>
private void SetItemClick()
{
    var length = _contentTransform.childCount;
    if (length <= 0)
    {
        return;
    }

    for (int i = 0; i < length; i++)
    {
        Button btn = _contentTransform.GetChild(i).GetComponent<Button>();
        if (!btn)
        {
            btn = _contentTransform.GetChild(i).gameObject.AddComponent<Button>();
        }

        btn.onClick.AddListener(ClickItem);
    }
}

/// <summary>
/// Item的点击事件
/// </summary>
private void ClickItem()
{
    if (EventSystem.current.IsPointerOverGameObject())
    {
        var clickObj = EventSystem.current.currentSelectedGameObject;
        var clickIndex = -1;
        var length = _transformList.Count;
        for (int i = _transformList.Count - 1; i >= 0; i--)
        {
            if (clickObj == _transformList[i].gameObject)
            {
                clickIndex = i;
            }
        }

        if (clickIndex != -1)
        {
            Debug.Log("点击了clickIndex:" + clickIndex);

            if (!this.isCirculation)
            {

                var realIndex = GetNowClickIndex();
                //如果不循环才可以将点击Index和现在显示的index对比,然后+showIndex,得到该移动的位置
                var toIndex = showIndex + (clickIndex - realIndex);
                ToMoveByIndex(toIndex);
            }
            else
            {
                //如果循环只能左右移动一格
                if (clickIndex > initShowIndex) ToLeft();
                else if (clickIndex < initShowIndex) ToRight();
            }

        }
    }
}

有两个方法会报错,我们再添加
后面代码一次性复制了。

/往左移动,判断第一个Item右侧是否超过显示框最左边,超过则将Item移动到末尾
private void MoveLeft()
{
    var itemRect = _transformList.First();
    var rectInitEndX = GetInitEndXByRect(itemRect);
    var rectTransform = gameObject.GetComponent<RectTransform>();
    scrollInitEndX = GetInitEndXByRect(rectTransform);
    if (rectInitEndX.y <= scrollInitEndX.x)
    {
        Debug.Log("该移动了");
        var endRect = _endRect;
        _changeItemVector2.x = endRect.localPosition.x + _interval;
        _changeItemVector2.y = itemRect.localPosition.y;
        itemRect.localPosition = _changeItemVector2;
        if (isCirculation)
        {
            _endRect = itemRect;
            _transformList.RemoveAt(0);
            _transformList.Add(itemRect);
            _initRect = _transformList[0];
        }
    }
}

//往右移动,判断最后一个Item左侧是否超过显示框最右边,超过则将Item移动到首位
private void MoveRight()
{
    var itemRect = _transformList.Last();
    var rectInitEndX = GetInitEndXByRect(itemRect);
    if (rectInitEndX.x >= scrollInitEndX.y)
    {
        var initRect = _initRect;
        _changeItemVector2.x = initRect.localPosition.x - _interval;
        _changeItemVector2.y = itemRect.localPosition.y;
        itemRect.localPosition = _changeItemVector2;
        if (isCirculation)
        {
            //将最后一个放到第一个来
            _initRect = itemRect;
            InsertItemList(itemRect);
            _endRect = _transformList[_transformList.Count - 1];
        }
    }
}

#region 滑动处理

//是否点击
private bool isTouch = false;

//当前点击(防止多点触碰)
private int pointerId = 0;

//是否已经拖拽,需要开始移动
private bool isDrag = false;

//移动方向
private int dir = 0;

/// <summary>
/// 开始滑动
/// </summary>
/// <param name="eventData"></param>
public void OnBeginDrag(PointerEventData eventData)
{
    if (isTouch) return;
    if (pointerId == 0)
    {
        pointerId = eventData.pointerId;
        isTouch = true;
        _initMouserX = eventData.position.x;
    }
}

/// <summary>
/// 滑动结束
/// </summary>
/// <param name="eventData"></param>
public void OnEndDrag(PointerEventData eventData)
{
    if (eventData.pointerId == pointerId)
    {
        isTouch = false;
        pointerId = 0;
        var endMouserX = eventData.position.x;
        if (endMouserX > _initMouserX)
        {
            if (endMouserX - _initMouserX >= this.limit)
            {
                Debug.Log("大于了50,向右移动");
                ToRight();
            }
        }
        else
        {
            if (_initMouserX - endMouserX >= this.limit)
            {
                print("大于了50,向左移动");
                ToLeft();
            }
        }
    }
}

// Update is called once per frame
void Update()
{
    if (isDrag)
    {
        if (isItemUniform)
        {
            var toX = _contentTransform.localPosition.x + Time.deltaTime * _speed * dir;
            _changeScorllViewVector2.x = toX;
            _changeScorllViewVector2.y = _contentTransform.localPosition.y;
            _contentTransform.localPosition = _changeScorllViewVector2;
            //如果循环,需要判断
            if (isCirculation)
            {
                if (dir == 1)
                {
                    this.MoveRight();
                }

                if (dir == -1)
                {
                    this.MoveLeft();
                }
            }

            //超过判断
            if ((dir == 1 && _contentTransform.localPosition.x >= endPositon) ||
                (dir == -1 && _contentTransform.localPosition.x <= endPositon))
            {
                isDrag = false;
                dir = 0;
                _changeScorllViewVector2.x = endPositon;
                _contentTransform.localPosition = _changeScorllViewVector2;
            }
        }
    }
}

#endregion

#region 逻辑

/// <summary>
/// 根据RectTransform得到最小值X和最大值X
/// </summary>
/// <param name="rectTransform"></param>
/// <returns></returns>
private Vector2 GetInitEndXByRect(RectTransform rectTransform)
{
    var v2 = new Vector2();
    var x = rectTransform.position.x;
    var width = (float)(rectTransform.rect.width * rectTransform.lossyScale.x);
    v2.x = x - rectTransform.pivot.x * width;
    v2.y = v2.x + width;

    return v2;
}

/// <summary>
/// 插入一个Item,放到_transformList首位
/// </summary>
/// <param name="itemRect"></param>
private void InsertItemList(RectTransform itemRect)
{
    var length = _transformList.Count;
    RectTransform temp = _transformList[length - 1];
    for (int i = length - 1; i >= 0; i--)
    {
        if (i >= 1)
        {
            _transformList[i] = _transformList[i - 1];
        }
        else _transformList[i] = temp;
    }
}
/// <summary>
/// 得到当前的Index(0-content.length范围)
/// </summary>
/// <returns></returns>
private int GetNowClickIndex()
{
    var realIndex = showIndex + initShowIndex;
    Debug.Log("当前真实的realIndex:" + realIndex);
    int a = 0;
    var length = _transformList.Count;
    while (realIndex < 0)
    {
        realIndex += length;
    }
    while (realIndex > length - 1)
    {
        realIndex -= length;
    }

    return realIndex;
}

#endregion
/// <summary>
/// 移动几个位置
/// </summary>
/// <param name="num"></param>
public void ToLeft(int num = 1)
{
    //初始位置减去已经显示的Index,判断是否超出边界
    if (!isCirculation && initShowIndex - showIndex >= _transformList.Count - 1) return;
    isDrag = true;
    dir = -1;
    showIndex += num;

    endPositon = contentInitPositionX - showIndex * _interval;
}
/// <summary>
/// 移动几个位置
/// </summary>
/// <param name="num"></param>
public void ToRight(int num = 1)
{
    //初始位置减去已经显示的Index,判断是否超出边界
    if (!isCirculation && initShowIndex - showIndex <= 0) return;
    isDrag = true;
    dir = 1;
    showIndex -= num;

    endPositon = contentInitPositionX - showIndex * _interval;
}

public void ToMoveByIndex(int toIndex)
{
    isDrag = true;
    dir = toIndex > showIndex ? -1 : 1;
    showIndex = toIndex;
    endPositon = contentInitPositionX - showIndex * _interval;
}

大致思路

得到第一个节点与最后一个节点,计算位置间隔,然后除以所有节点数,得到两个节点的间隔,每次移动一个间隔距离。然后拖拽结束,根据鼠标位置判断左移动还是右移动。移动时改变isDrag值,然后update里面会执行移动函数。
注意事项:节点的锚点设置一致,第一个item在左测,最后一个需要在最右侧,不然计算偏差。

缺点

移动时会每帧判断一次最边上Item是否超出边框,会导致比较大的性能损耗。优化方法是当移动完时,再判断是否需要将超出边框item移动到另一侧,只需要执行一次判断。但是需要在最边上多生成Item,不然循环时会造成空白。

功能还不是很完善,后面能继续做跑马灯功能,或者其他滑动组件,大家有兴趣可以自己尝试。

效果展示:

我把大学和工作中用的经典电子书库(Java相关、数据结构、操作系统、C++/C、网络经典、前端编程经典、程序员认知、职场发展)、面试找工作的资料汇总都打包放在这了,点击下方可以直达:

\

更多免费游戏资源:

免费大礼包​mp.weixin.qq.com/s/TYr_5iLTfHibj6DZ7o4SPg