效果
-
每次滑动结束后,根据本次滑动的方向(左或右),让离中央比较近的元素停在中央。
-
每次滑动结束后,发出事件:告知当前停在中央的元素的index。
-
可以传入合适的(在范围内)index,让列表自动滑动,让index对应的元素停在中央。
使用方式
创建一个列表,整体层级和各节点和参数面板如下所示。
整体层级
各节点的参数面板
Root
ScrollView
ViewPort
Content
在合适的时机初始化UIScrollRectAdjustor(和ScrollRect挂在同一节点上)
初始化时需传入index的有效范围(index从0开始数)。
为了让左右两边的元素也能显示在中央,因此需要在左右两侧各再创建1个元素(根据元素大小和列表宽度,有时需创建多个),将其子节点隐藏,用于占位。
假设一共有x个元素,则实际有效范围应是:1~x-2
关注UIScrollRectAdjustor的事件MoveEndEvent,可在每次滑动结束后获知当前停在中央的元素的index。
调用UIScrollRectAdjustor.SetIndexToCenter(index),让列表自动滑动,让index对应的元素停在中央。注意index必须在有效范围内。
代码
UIScrollRectAdjustor.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
[RequireComponent(typeof(ScrollRect))]
public class UIScrollRectAdjustor : MonoBehaviour, IEndDragHandler, IBeginDragHandler
{
private ScrollRect _scrollRect;
private ScrollRect ScrollRect
{
get
{
if (_scrollRect == null)
{
_scrollRect = GetComponent<ScrollRect>();
}
return _scrollRect;
}
}
private RectTransform ContentRectTrans => ScrollRect.content;
private GridLayoutGroup _gridLayoutGroup;
private GridLayoutGroup GridLayoutGroup
{
get
{
if (_gridLayoutGroup == null)
{
_gridLayoutGroup = ContentRectTrans.GetComponent<GridLayoutGroup>();
}
return _gridLayoutGroup;
}
}
[SerializeField, Header("自动移动时的移动速度")]
private float _moveSpeed = 3000;
private IEnumerator _autoMoveCoroutine;
private Vector2Int _validIndexRange;
private float ContentPosX => ContentRectTrans.anchoredPosition.x;
private float[] _posXs;
private float MinPosX => _posXs[MinPosXIndex];
private float MaxPosX => _posXs[MaxPosXIndex];
private int MinPosXIndex => _validIndexRange.y;
private int MaxPosXIndex => _validIndexRange.x;
// index:当前哪一列在中央。
public event Action<int> MoveEndEvent;
private bool IsValidIndex(int index)
{
return index >= _validIndexRange.x && index <= _validIndexRange.y;
}
public void Init(Vector2Int validIndexRange, int columnCnt = 0)
{
columnCnt = columnCnt > 0 ? columnCnt : CalcCurColumnCnt();
CreatePosXs(columnCnt);
_validIndexRange = new Vector2Int(Mathf.Max(0, validIndexRange.x), Mathf.Min(_posXs.Length - 1, validIndexRange.y));
/*
for (int i = _validIndexRange.x; i <= _validIndexRange.y; i++)
{
Debug.Log(i + ":" + _posXs[i]);
}
*/
}
public void Destroy()
{
StopAutoMove();
_posXs = null;
_scrollRect = null;
_gridLayoutGroup = null;
}
public void OnBeginDrag(PointerEventData eventData)
{
StopAutoMove();
}
public void OnEndDrag(PointerEventData eventData)
{
float curX = ContentPosX;
float suitableX = GetSuitableX(curX, _scrollRect.velocity.x < 0, out int index);
MoveToTargetX(suitableX, () => MoveEndEvent?.Invoke(index));
}
private float GetSuitableX(float curX, bool isLeftMove, out int index)
{
float targetX = 0;
index = 0;
if (curX < MinPosX)
{
targetX = MinPosX;
index = MinPosXIndex;
}
else if (curX > MaxPosX)
{
targetX = MaxPosX;
index = MaxPosXIndex;
}
else
{
if (isLeftMove)
{
targetX = MinPosX;
index = MinPosXIndex;
for (int i = MaxPosXIndex; i <= MinPosXIndex; i++)
{
if (_posXs[i] <= curX)
{
targetX = _posXs[i];
index = i;
break;
}
}
}
else
{
targetX = MaxPosX;
index = MaxPosXIndex;
for (int i = MinPosXIndex; i >= MaxPosXIndex; i--)
{
if (_posXs[i] >= curX)
{
targetX = _posXs[i];
index = i;
break;
}
}
}
}
return targetX;
}
private void MoveToTargetX(float x, Action moveEndAction)
{
StartAutoMove(ContentPosX, x, moveEndAction);
}
private void StartAutoMove(float beginX, float endX, Action moveEndAction)
{
StopAutoMove();
_autoMoveCoroutine = AutoMoveCoroutine(beginX, endX, moveEndAction);
MonoSys.Instance.StartCoroutine(_autoMoveCoroutine);
}
private void StopAutoMove()
{
MonoSys.Instance.DestroyCoroutine(ref _autoMoveCoroutine);
}
private IEnumerator AutoMoveCoroutine(float beginX, float endX, Action moveEndAction)
{
float timer = 0f;
float moveTime = Mathf.Abs(beginX - endX) / _moveSpeed;
while (timer < moveTime)
{
ContentRectTrans.anchoredPosition = new Vector3(Mathf.Lerp(beginX, endX, timer / moveTime),
ContentRectTrans.anchoredPosition.y);
timer += Time.deltaTime;
yield return null;
}
ContentRectTrans.anchoredPosition = new Vector3(endX, ContentRectTrans.anchoredPosition.y);
ScrollRect.StopMovement();
moveEndAction?.Invoke();
}
// 让第index个子节点显示到中央
public void SetIndexToCenter(int columnIndex)
{
if (IsValidIndex(columnIndex))
{
var pos = _posXs[columnIndex];
MoveToTargetX(pos, () => MoveEndEvent?.Invoke(columnIndex));
}
}
private int CalcCurColumnCnt()
{
return (int)((ContentRectTrans.sizeDelta.x + GridLayoutGroup.spacing.x) / (GridLayoutGroup.cellSize.x + GridLayoutGroup.spacing.x));
}
// 计算相关数据:各列居中时对应的Content的LocalPosX
private void CreatePosXs(int cnt, bool isforceSet = true)
{
var width = isforceSet ? (GridLayoutGroup.cellSize.x * cnt + GridLayoutGroup.spacing.x * (cnt - 1)) : ContentRectTrans.sizeDelta.x;
float firstX = width / 2 - GridLayoutGroup.cellSize.x / 2;
float distance = GridLayoutGroup.cellSize.x + GridLayoutGroup.spacing.x;
_posXs = new float[cnt];
for (int i = 0; i < cnt; i++)
{
_posXs[i] = firstX - i * distance;
}
}
}