最近在研究滚动条的制作,主要研究的是两个github上的开源项目:LoopScrollRect和基于动画的滚动条FancyScrollView
但是因为基于动画的滚动条复用性不高,每次制作一个新的滚动条都需要重新构建动画,而且看demo中整体的滚动布局不是很清晰,最终选择LoopScrollRect
作为开发的基准,但是LoopScrollRect中的功能支持的不完全,还需要自己再进行一些开发工作。
1.滚动到末尾时的Content位置错误问题
Demo中提供了两个滚动到指定下标的接口:SrollToCell
和SrollToCellWithinTime
,分别表示以指定的速度或指定的时间滚动到对应格子。在对应的滚动条组件上也有对应接口调用的按钮:
但当滚动到最后一个格子且速度或时间小于0时,组件内部其实调用的就不再是滚动接口,而是ReFillCells
接口,但是这个接口不是从后向前重新ReFill,而是从前向后,所以有的时候调用该接口就会发现想跳转到最后一个格子,但是ReFill之后根本看不到最后一个格子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public void SrollToCell(int index, float speed) { if (totalCount >= 0 && (index < 0 || index >= totalCount)) { Debug.LogErrorFormat("invalid index {0}", index); return; } StopAllCoroutines(); if (speed <= 0) { //重新填充格子 RefillCells(index); return; } StartCoroutine(ScrollToCellCoroutine(index, speed)); } |
如果想解决这个问题,也许可以通过一些条件判断配合RefillCellsFromEnd
(从后向前重新填充格子)接口进行处理。
2.滚动到指定格子时该格子始终在列表最上方或最左方
举个栗子,比如我们现在要SrollToCell(14, 1f)
,滚动之后的结果就会变成这样:
目标格子会被滚动到最上方,但是如果我们想要目标滚到中间或者是滚到最下方应该怎么办呢?在格子滚动的实际逻辑中,在计算偏移量时使用的是ViewBounds和格子的Bounds的上边界差进行计算,所以最终格子会被移动到最上方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// IEnumerator ScrollToCellCoroutine(int index, float speed) 函数内的代码: //计算实际要进行滚动的偏移量 //这里计算的量为View的上边界与格子上边界的差,所以格子会被移动到最上方。 //如果这里我们使用m_ViewBounds.center.y - m_ItemBounds.center.y则会把格子移动到整个滚动条的中间。 if (direction == LoopScrollRectDirection.Vertical) offset = reverseDirection ? (m_ViewBounds.min.y - m_ItemBounds.min.y) : (m_ViewBounds.max.y - m_ItemBounds.max.y); else offset = reverseDirection ? (m_ItemBounds.max.x - m_ViewBounds.max.x) : (m_ItemBounds.min.x - m_ViewBounds.min.x); //获取偏移量,可以用这个函数与自定义的变量OffsetAligment控制格子移动的目标位置 float GetOffset(Bounds itemBounds) { if (direction == LoopScrollRectDirection.Vertical) { if (OffsetAligment == 0) return reverseDirection ? (m_ViewBounds.min.y - itemBounds.min.y) : (m_ViewBounds.max.y - itemBounds.max.y); else if (OffsetAligment == 1) return (m_ViewBounds.center.y - itemBounds.center.y); else return (m_ViewBounds.max.y - itemBounds.max.y); } else { if (OffsetAligment == 0) return reverseDirection ? (itemBounds.max.x - m_ViewBounds.max.x) : (itemBounds.min.x - m_ViewBounds.min.x); else if (OffsetAligment == 1) return itemBounds.center.x - m_ViewBounds.center.x; else return (itemBounds.min.x - m_ViewBounds.min.x); } } |
3.滚动结束时滚回到当前选择的页签
想做一个和FancyScroll类似的卡在某个格子的功能,但是LoopScrollRect并没有提供对应的功能。看了一下FancyScroll其实是在update滚动计算位置时,当速度小于某个阈值时就直接滚动到指定下标或者指定位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//function update velocity *= Mathf.Pow(decelerationRate, deltaTime); if (Mathf.Abs(velocity) < 0.001f) { velocity = 0f; } position += velocity * deltaTime; if (snap.Enable && Mathf.Abs(velocity) < snap.VelocityThreshold) { //滚到指定位置 ScrollTo(Mathf.RoundToInt(currentPosition), snap.Duration, snap.Easing); } |
于是我们也可以照虎画猫整一个类似的东西,但是在看LoopScrollRectBase代码时发现,这里的代码update和那边基本一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
//protected virtual void LateUpdate() 函数内的代码: m_Velocity[axis] *= Mathf.Pow(m_DecelerationRate, deltaTime); //这里使用了一个变量StartLockVelocity控制速度小于多少时认定为滚动结束,并开始锁定滚动条 if (Mathf.Abs(m_Velocity[axis]) < StartLockVelocity) m_Velocity[axis] = 0; position[axis] += m_Velocity[axis] * deltaTime; //添加一个锁定滚动条功能,可以滚回到指定我们选择的页签 LockScroll(axis); //添加一个锁定滚动条逻辑, //因为分为两个轴,所以要判断一下,如果不是当前滚动的轴就不管他。 //force是否强制锁定 void LockScroll(int axis, bool force = false) { //速度小于阈值时开始锁定到指定格子Item上 if (AutoLockMidCell && Mathf.Abs(m_Velocity[axis]) < StartLockVelocity && ((direction == LoopScrollRectDirection.Vertical && axis == 1) || (direction == LoopScrollRectDirection.Horizontal && axis == 0)|| force)) SrollToCellWithinTime(GetMiddleIndex(), LockSpeed); } //获取view中间的index下标,比较bounds的中心范围与view的中心范围,找到最小值 int GetMiddleIndex() { Bounds itemBounds; int minIndex = 0; var targetValue = direction == LoopScrollRectDirection.Vertical ? m_ViewBounds.center.y : m_ViewBounds.center.x; var result = float.MaxValue; for (int index = itemTypeStart; index < itemTypeEnd; index++) { itemBounds = GetBounds4Item(index); float itemValue = direction == LoopScrollRectDirection.Vertical ? itemBounds.center.y : itemBounds.center.x; float tempValue = Mathf.Abs(itemBounds.center.y - targetValue); if (result > tempValue) { result = tempValue; minIndex = index; } } return minIndex; } |
4.滚动停止功能导致锁定失效
由于LoopScrollRectBase
继承了IInitializePotentialDragHandler
接口,实现了对应的OnInitializePotentialDrag
函数,该函数接收开始拖动的点击事件,并在该回调函数中将滚动速度置为0,所以导致上述的锁定逻辑无法执行。然后就会出现滚动条没有被锁住而是卡在了中间的诡异情况。
在FancyScroll中没有实现IInitializePotentialDragHandler
接口而是实现IPointerDownHandler
和IPointerUpHandler
接口实现相同功能,可是我在LoopScrollRectBase中使用这两个接口时无法生效。暂时不知道是什么原因。
暂时没想到什么好的处理方案,于是只是简单的在OnInitializePotentialDrag
接口内调用了一次锁定滚动条的操作。
1 2 3 4 5 6 7 8 |
public virtual void OnInitializePotentialDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; //锁定滚动条,第二个参数表示强制锁定 LockScroll(0, true); m_Velocity = Vector2.zero; } |