|
前言
LoopScrollRect 是基于 UGUI 的一个用来代替 ScrollRect 的插件,其特点是:不同于原生的 ScrollRect,LoopScrollRect 会根据 Viewport 的大小来加载子元素,比如说我们的总数据条目是 500 条,但 Viewport 只显示得下十来个 cell,这时候 LoopScrollRect 就不会傻傻地生成全部 500 个 cell,大大优化了性能。
按我暂时的浅薄理解来看,其实现的基本方式类似于:给 content 挂上一个 ContentSizeFitter 组件,这样 content 就会根据其底下挂了几个子物体来自适应改变自身的大小,而 LoopScrollRect 则会根据这个 content 的大小有没有超过 viewport 的大小,来决定要不要给这个 content 生成新的 Item 或者删除多余的 Item。
然后,这里不准备记 LoopScrollRect 的基本用法(官方有提供中文文档),主要记一下自己在用 LoopScrollRect 实现一些乱七八糟效果的时候瞎写的代码 (˙ー˙),做个备忘(仅仅是私底下写着玩的,估计有 Bug,只能说提供个实现思路 (⊙_⊙)
自动轮播功能
主要是应对这种需求:比如一个转盘抽奖活动里,我们要无限循环播放所有中奖信息,效果如下图:
这里的思路是利用 LoopScrollRect 自带的无尽模式(totalCount = -1)来实现。
我们先给 LoopScrollRect 加上以下控制项:
[Serializable]
struct AutoScrollProperties
{
public bool enable;
public Vector2 speed;
}
[SerializeField, Tooltip("自动轮播")]
AutoScrollProperties autoScrollProperties = new AutoScrollProperties { enable = false, speed = Vector2.one };
enable 表示是否启用自动轮播功能,speed 则是 ScrollRect 自动滚动的速度。
滚动逻辑的实现其实是直接复制原有的 OnScroll 函数,然后将函数输入从鼠标事件替换成了一个 Vector2 类型值:
public virtual void OnAutoScroll(Vector2 delta)
{
if (!IsActive())
return;
EnsureLayoutHasRebuilt();
UpdateBounds();
if (vertical && !horizontal)
{
if (Mathf.Abs(delta.x) > Mathf.Abs(delta.y))
delta.y = delta.x;
delta.x = 0;
}
if (horizontal && !vertical)
{
if (Mathf.Abs(delta.y) > Mathf.Abs(delta.x))
delta.x = delta.y;
delta.y = 0;
}
Vector2 position = m_Content.anchoredPosition;
position += delta * m_ScrollSensitivit;
if (m_MovementType == MovementType.Clamped)
position += CalculateOffset(position - m_Content.anchoredPosition);
SetContentAnchoredPosition(position);
UpdateBounds();
}
然后在 Update 里驱动 ScrollRect 进行滚动:
protected virtual void Update()
{
if (autoScrollProperties.enable)
{
OnAutoScroll(autoScrollProperties.speed);
}
}
到此 LoopScrollRect 这边就写完了ww,接下来是子元素 cell 这边的逻辑。
在 LoopScrollRect 的无尽模式里,提供给子元素 ScrollCellIndex 函数的输入索引值是无限增长的,而我们实际拥有的数据条目是有限的,比如我们现在只有 10 条中奖信息用于显示,这时需要在 ScrollCellIndex 里做以下处理:
void ScrollCellIndex(int idx)
{
int dataIndex = idx % 10;
string name = "Cell " + dataIndex.ToString();
if (text != null)
{
text.text = name;
}
if (image != null)
{
image.color = Rainbow(dataIndex / 50.0f);
}
gameObject.name = name;
}
令索引值对当前拥有的数据条目数量取余,如此就可实现无限轮播。
实际使用时,注意将 totalCount 设为 -1,同时通过 speed 来控制自动轮播的条目滚动速度:
拖拽 ScrollRect 结束时,让距离 Viewport 中心点最近的一个 Cell 自动吸附过去,实现平滑居中
想实现的效果如下图:
代码方面,照例先加上控制项:
[Serializable]
struct ScrollToCenterProperties
{
public bool enable;
public float speed;
}
[SerializeField, Tooltip("子元素自动居中")]
ScrollToCenterProperties scrollToCenterProperties = new ScrollToCenterProperties { enable = false, speed = 100f };
enable 表示是否启用居中功能,speed 则表示子元素吸附过去的速度。
实现方面我是参考了 LoopScrollRect 原有的 SrollToCell 函数:
private void ScrollToCenter()
{
if (totalCount == 0 || itemTypeEnd <= itemTypeStart || scrollToCenterProperties.speed <= 0)
{
return;
}
StopAllCoroutines();
StartCoroutine(ScrollToCenterCoroutine());
}
因为自动居中应该支持无尽模式,所以这里允许 totalCount < 0。
我们要先找到距离 Viewport 中心点最近的 cell 的 Index:
private int FindClosestIndexToCenter()
{
m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
int closestIndex = itemTypeStart;
float closestDistance = float.MaxValue;
for (int i = itemTypeStart; i < itemTypeEnd; i++)
{
int childIdx = i - itemTypeStart;
if (childIdx >= 0 && childIdx < content.childCount)
{
var itemBounds = GetBounds4Item(i);
float itemDistance = float.MaxValue;
if (directionSign == -1)
itemDistance = Mathf.Abs(m_ViewBounds.center.y - itemBounds.center.y);
else if (directionSign == 1)
itemDistance = Mathf.Abs(itemBounds.center.x - m_ViewBounds.center.x);
if (itemDistance < closestDistance)
{
closestIndex = i;
closestDistance = itemDistance;
}
}
}
return closestIndex;
}
然后再平滑居中:
IEnumerator ScrollToCenterCoroutine()
{
int centerIndex = FindClosestIndexToCenter();
bool needMoving = true;
while (needMoving)
{
yield return null;
if (!m_Dragging)
{
float move = 0;
if (centerIndex < itemTypeStart)
{
move = -Time.deltaTime * scrollToCenterProperties.speed;
}
else if (centerIndex >= itemTypeEnd)
{
move = Time.deltaTime * scrollToCenterProperties.speed;
}
else
{
m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
var m_ItemBounds = GetBounds4Item(centerIndex);
var offset = 0.0f;
if (directionSign == -1)
offset = m_ViewBounds.center.y - m_ItemBounds.center.y;
else if (directionSign == 1)
offset = m_ItemBounds.center.x - m_ViewBounds.center.x;
float maxMove = Time.deltaTime * scrollToCenterProperties.speed;
if (Mathf.Abs(offset) < maxMove)
{
needMoving = false;
move = offset;
}
else
move = Mathf.Sign(offset) * maxMove;
}
if (move != 0)
{
Vector2 offset = GetVector(move);
content.anchoredPosition += offset;
m_PrevPosition += offset;
m_ContentStartPosition += offset;
}
}
}
StopMovement();
UpdatePrevData();
}
这里的关键点是根据 Bounds.center 这个中心点来计算。
最后是何时触发平滑居中的逻辑,我是选择放在 OnEndDrag 函数里(即拖拽之后):
public virtual void OnEndDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
m_Dragging = false;
if (scrollToCenterProperties.enable)
{
ScrollToCenter();
}
}
注意配置时 Movement Type 必须选 Unrestricted,否则 ScrollRect 滚动到列表边缘时无法让靠边的那几个 cell 居中显示。
平滑居中的动画实现
上面我们实现了子元素自动吸附平滑居中的功能,而在手游里通常都会伴有这样的动画效果:
对于类似这种动画,我觉得上层的 ScrollRect 只需给各个 cell 提供其在整个 Viewport 里的相对位置信息,然后由 cell 自己决定它应该变成什么样,比如在上面的动画里,越接近 Viewport 中心点的 cell 其 Alpha 值越高。
所以我们可以先给 cell 加个接口,用于向 cell 提供其本身在 Viewport 里的相对位置信息:
public interface IScrollCell
{
void UpdateNormalizedPos(float normalizedPos);
}
public abstract class LoopScrollDataSource
{
public abstract void ProvideData(Transform transform, int idx);
public void UpdateCellNormalizedPos(Transform transform, float normalizedPos)
{
IScrollCell cell = transform.GetComponent<IScrollCell>();
if (cell != null)
{
cell.UpdateNormalizedPos(normalizedPos);
}
}
}
然后给 LoopScrollRect 加一个这样的函数,各个 cell 是用中心点参与位置计算的:
private void UpdateCells()
{
m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
for (int i = itemTypeStart; i < itemTypeEnd; i++)
{
int childIdx = i - itemTypeStart;
if (childIdx >= 0 && childIdx < content.childCount)
{
var itemBounds = GetBounds4Item(i);
var normalizedPos = 0f;
if (directionSign == -1)
{
float offset = reverseDirection ? (itemBounds.center.y - m_ViewBounds.min.y) : (m_ViewBounds.max.y - itemBounds.center.y);
normalizedPos = Mathf.Clamp(offset / m_ViewBounds.size.y, 0f, 1f);
}
else if (directionSign == 1)
{
float offset = reverseDirection ? (m_ViewBounds.max.x - itemBounds.center.x) : (itemBounds.center.x - m_ViewBounds.min.x);
normalizedPos = Mathf.Clamp(offset / m_ViewBounds.size.x, 0f, 1f);
}
dataSource.UpdateCellNormalizedPos(content.GetChild(childIdx), normalizedPos);
}
}
}
而函数调用我是选择放在了 UpdateBounds 函数最后:
private void UpdateBounds(bool updateItems = false)
{
m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
m_ContentBounds = GetBounds();
if (m_Content == null)
return;
// ============LoopScrollRect============
// Don&#39;t do this in Rebuild
if (Application.isPlaying && updateItems && UpdateItems(m_ViewBounds, m_ContentBounds))
{
Canvas.ForceUpdateCanvases();
m_ContentBounds = GetBounds();
}
// ============LoopScrollRect============
// Make sure content bounds are at least as large as view by adding padding if not.
// One might think at first that if the content is smaller than the view, scrolling should be allowed.
// However, that&#39;s not how scroll views normally work.
// Scrolling is *only* possible when content is *larger* than view.
// We use the pivot of the content rect to decide in which directions the content bounds should be expanded.
// E.g. if pivot is at top, bounds are expanded downwards.
// This also works nicely when ContentSizeFitter is used on the content.
Vector3 contentSize = m_ContentBounds.size;
Vector3 contentPos = m_ContentBounds.center;
Vector3 excess = m_ViewBounds.size - contentSize;
if (excess.x > 0)
{
contentPos.x -= excess.x * (m_Content.pivot.x - 0.5f);
contentSize.x = m_ViewBounds.size.x;
}
if (excess.y > 0)
{
contentPos.y -= excess.y * (m_Content.pivot.y - 0.5f);
contentSize.y = m_ViewBounds.size.y;
}
m_ContentBounds.size = contentSize;
m_ContentBounds.center = contentPos;
UpdateCells();
}
至此,cell 拿到了自己需要的相对位置信息,接下来就简单了,我们可以给 cell 加一个 Canvas Group 组件,再做一个这样的 Animation:
这个动画的要点在于:normalizedTime 等于 0 和 1 的时候,设置 Canvas Group 的 Alpha 为 0,而 normalizedTime 等于 0.5 的时候,设置 Canvas Group 的 Alpha 为 1。
我们将这个动画的速度设为 0(这样此动画就会卡在我们设置的对应帧,而不会自动往下播放):
接下来在 cell 脚本里如此实现 IScrollCell 接口:
public Animator animator;
private readonly int scrollTriggerHash = Animator.StringToHash(&#34;scroll&#34;);
public void UpdateNormalizedPos(float normalizedPos)
{
animator.Play(scrollTriggerHash, -1, normalizedPos);
animator.speed = 0;
}
其实,这种实现方式的本质就是将 cell 在 Viewport 里处于各个相对位置的样子存储在 Animation 里,然后各个 cell 直接播放对应一帧即可。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|