[Unity UGUI]ScrollRect效果大全

2023-07-10,,

UGUI各种优化效果

本文所实现的UGUI效果需求如下: 
支持缩放滑动效果 
支持动态缩放循环加载 
支持大数据固定Item复用加载 
支持不用Mask遮罩无限循环加载 
支持ObjectPool动态加载 
支持无限不规则子物体动态加载 
支持拖动并点击和拖拽 
支持拖动并拖拽 
支持ScrollRect拖动自动吸附功能(拖动是否超过一半自动进退)


前言

要实现以上效果,我从网上搜索得到部分解决方案链接,但不是完全满足想要的效果,就自己继续改造优化和添加想要的效果,本文最后会附带上完整Demo下载链接。

效果图

缩放滑动效果 

缩放循环展示卡牌效果

大量数据无卡顿动态加载,并且支持拖拽、点击和吸附功能

大量数据循固定Item复用 

无限无遮罩动态加载

不规则子物体动态循环加载

部分核心代码

有遮罩无卡顿加载 
思路:并没有使用UGUI的ScrollRect组件,摆放几张卡片,通过移动和缩放来实现

 using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI; public class EnhancelScrollView : MonoBehaviour
{
// 缩放曲线
public AnimationCurve scaleCurve;
// 位移曲线
public AnimationCurve positionCurve;
// 位移系数
public float posCurveFactor = 500.0f;
// y轴坐标固定值(所有的item的y坐标一致)
public float yPositionValue = 46.0f; // 添加到EnhanceScrollView的目标对象
public List<EnhanceItem> scrollViewItems;
// 目标对象Widget脚本,用于depth排序
private List<Image> imageTargets; // 当前处于中间的item
private EnhanceItem centerItem;
private EnhanceItem preCenterItem; // 当前出移动中,不能进行点击切换
private bool canChangeItem = true; // 计算差值系数
public float dFactor = 0.2f; // 点击目标移动的横向目标值
private float[] moveHorizontalValues;
// 对象之间的差值数组(根据差值系数算出)
private float[] dHorizontalValues; // 横向变量值
public float horizontalValue = 0.0f;
// 目标值
public float horizontalTargetValue = 0.1f; // 移动动画参数
private float originHorizontalValue = 0.1f;
public float duration = 0.2f;
private float currentDuration = 0.0f; private static EnhancelScrollView instance;
public static EnhancelScrollView GetInstance()
{
return instance;
} void Awake()
{
instance = this;
} void Start()
{
if((scrollViewItems.Count % ) == )
{
Debug.LogError("item count is invaild,please set odd count! just support odd count.");
} if(moveHorizontalValues == null)
moveHorizontalValues = new float[scrollViewItems.Count]; if(dHorizontalValues == null)
dHorizontalValues = new float[scrollViewItems.Count]; if (imageTargets == null)
imageTargets = new List<Image>(); int centerIndex = scrollViewItems.Count / ;
for (int i = ; i < scrollViewItems.Count;i++ )
{
scrollViewItems[i].scrollViewItemIndex = i;
Image tempImage = scrollViewItems[i].gameObject.GetComponent<Image>();
imageTargets.Add(tempImage); dHorizontalValues[i] = dFactor * (centerIndex - i); dHorizontalValues[centerIndex] = 0.0f;
moveHorizontalValues[i] = 0.5f - dHorizontalValues[i];
scrollViewItems[i].SetSelectColor(false);
} centerItem = scrollViewItems[centerIndex];
canChangeItem = true;
} public void UpdateEnhanceScrollView(float fValue)
{
for (int i = ; i < scrollViewItems.Count; i++)
{
EnhanceItem itemScript = scrollViewItems[i];
float xValue = GetXPosValue(fValue, dHorizontalValues[itemScript.scrollViewItemIndex]);
float scaleValue = GetScaleValue(fValue, dHorizontalValues[itemScript.scrollViewItemIndex]);
itemScript.UpdateScrollViewItems(xValue, yPositionValue, scaleValue);
}
} void Update()
{
currentDuration += Time.deltaTime;
if (currentDuration > duration)
{
// 更新完毕设置选中item的对象即可
currentDuration = duration;
if (centerItem != null)
centerItem.SetSelectColor(true);
if (preCenterItem != null)
preCenterItem.SetSelectColor(false);
canChangeItem = true;
} SortDepth();
float percent = currentDuration / duration;
horizontalValue = Mathf.Lerp(originHorizontalValue, horizontalTargetValue, percent);
UpdateEnhanceScrollView(horizontalValue);
} /// <summary>
/// 缩放曲线模拟当前缩放值
/// </summary>
private float GetScaleValue(float sliderValue, float added)
{
float scaleValue = scaleCurve.Evaluate(sliderValue + added);
return scaleValue;
} /// <summary>
/// 位置曲线模拟当前x轴位置
/// </summary>
private float GetXPosValue(float sliderValue, float added)
{
float evaluateValue = positionCurve.Evaluate(sliderValue + added) * posCurveFactor;
return evaluateValue;
} public void SortDepth()
{
imageTargets.Sort(new CompareDepthMethod());
for (int i = ; i < imageTargets.Count; i++)
imageTargets[i].transform.SetSiblingIndex(i);
} /// <summary>
/// 用于层级对比接口
/// </summary>
public class CompareDepthMethod : IComparer<Image>
{
public int Compare(Image left, Image right)
{
if (left.transform.localScale.x > right.transform.localScale.x)
return ;
else if (left.transform.localScale.x < right.transform.localScale.x)
return -;
else
return ;
}
} /// <summary>
/// 获得当前要移动到中心的Item需要移动的factor间隔数
/// </summary>
private int GetMoveCurveFactorCount(float targetXPos)
{
int centerIndex = scrollViewItems.Count / ;
for (int i = ; i < scrollViewItems.Count;i++ )
{
float factor = (0.5f - dFactor * (centerIndex - i)); float tempPosX = positionCurve.Evaluate(factor) * posCurveFactor;
if (Mathf.Abs(targetXPos - tempPosX) < 0.01f)
return Mathf.Abs(i - centerIndex);
}
return -;
} /// <summary>
/// 设置横向轴参数,根据缩放曲线和位移曲线更新缩放和位置
/// </summary>
public void SetHorizontalTargetItemIndex(int itemIndex)
{
if (!canChangeItem)
return; EnhanceItem item = scrollViewItems[itemIndex];
if (centerItem == item)
return; canChangeItem = false;
preCenterItem = centerItem;
centerItem = item; // 判断点击的是左侧还是右侧计算ScrollView中心需要移动的value
float centerXValue = positionCurve.Evaluate(0.5f) * posCurveFactor;
bool isRight = false;
if (item.transform.localPosition.x > centerXValue)
isRight = true; // 差值,计算横向值
int moveIndexCount = GetMoveCurveFactorCount(item.transform.localPosition.x);
if (moveIndexCount == -)
{
moveIndexCount = ;
} float dvalue = 0.0f;
if (isRight)
dvalue = -dFactor * moveIndexCount;
else
dvalue = dFactor * moveIndexCount; // 更改target数值,平滑移动
horizontalTargetValue += dvalue;
currentDuration = 0.0f;
originHorizontalValue = horizontalValue;
} /// <summary>
/// 向右选择角色按钮
/// </summary>
public void OnBtnRightClick()
{
if (!canChangeItem)
return;
int targetIndex = centerItem.scrollViewItemIndex + ;
if (targetIndex > scrollViewItems.Count - )
targetIndex = ;
SetHorizontalTargetItemIndex(targetIndex);
} /// <summary>
/// 向左选择按钮
/// </summary>
public void OnBtnLeftClick()
{
if (!canChangeItem)
return;
int targetIndex = centerItem.scrollViewItemIndex - ;
if (targetIndex < )
targetIndex = scrollViewItems.Count - ;
SetHorizontalTargetItemIndex(targetIndex);
}
}
 using UnityEngine;
using System.Collections;
using UnityEngine.UI; public class EnhanceItem : MonoBehaviour { // 在ScrollViewitem中的索引
// 定位当前的位置和缩放
public int scrollViewItemIndex = ;
public bool inRightArea = false; private Vector3 targetPos = Vector3.one;
private Vector3 targetScale = Vector3.one; private Transform mTrs;
private Image mImage; void Awake()
{
mTrs = this.transform;
mImage = this.GetComponent<Image>();
} void Start()
{
this.gameObject.GetComponent<Button>().onClick.AddListener(delegate () { OnClickScrollViewItem(); });
} // 当点击Item,将该item移动到中间位置
private void OnClickScrollViewItem()
{
EnhancelScrollView.GetInstance().SetHorizontalTargetItemIndex(scrollViewItemIndex);
} /// <summary>
/// 更新该Item的缩放和位移
/// </summary>
public void UpdateScrollViewItems(float xValue, float yValue, float scaleValue)
{
targetPos.x = xValue;
targetPos.y = yValue;
targetScale.x = targetScale.y = scaleValue; mTrs.localPosition = targetPos;
mTrs.localScale = targetScale;
} public void SetSelectColor(bool isCenter)
{
if (mImage == null)
mImage = this.GetComponent<Image>(); if (isCenter)
mImage.color = Color.white;
else
mImage.color = Color.gray;
}
}

有遮罩无卡顿加载 
思路:协程加载,先加载屏幕显示的数量,然后返回一帧在继续加载,防止出现数量太大卡顿的现象。

 while (CardsList.Count > roleInfo.Count)
{
DestroyImmediate(CardsList[].gameObject);
CardsList.RemoveAt();
}
StartCoroutine(createRoleCards()); private IEnumerator createRoleCards()
{
List<CLocalCharInfo> charInfos = new List<CLocalCharInfo>();
charInfos.AddRange(roleInfo);
int index = ;
for (int i = ; i < charInfos.Count; i++)
{
_createRoleCard(charInfos[i], index++);
if (index % == )
yield return null;
}
} private void _createRoleCard(CLocalCharInfo roleInfo, int index)
{
CUIPlayedCharCardWidget charCardWidget = null;
if (CardsList.Count > index)
{
charCardWidget = CardsList[index];
}
else
{
var obj = Instantiate(Resources.Load<GameObject>("Prefab/RoleCard")) as GameObject;
if (obj == null)
{
UnityEngine.Debug.LogError("有误");
return;
}
obj.name = roleInfo.Name; charCardWidget = obj.GetComponent<CUIPlayedCharCardWidget>(); if (charCardWidget == null)
{
UnityEngine.Debug.LogError("有误");
return;
}
obj.transform.parent = Obj_ScrollViewContent.transform;
obj.transform.localScale = Vector3.one;
CardsList.Add(charCardWidget);
} CUIPlayedCharCardWidget.CUIContent uiContent = new CUIPlayedCharCardWidget.CUIContent();
uiContent.RoleInfo = roleInfo;
uiContent.ScrollRectObj = m_ScrollRect;
uiContent.FixGridRect = m_FixGrid;
charCardWidget.InitContent(uiContent);
}

支持ScrollRect拖拽或点击 
思路:在卡片的Image上添加一个继承了IBeginDragHandler,IGradHandler,IEndDragHandler的脚本,重写接口里面的Drag事件方法。

 using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; namespace Mga
{
[RequireComponent(typeof(Image))]
public class CPlayedCardOnDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public bool dragOnSurfaces = true;
public ScrollRect m_ScrollRect = null;
public CFixGridRect m_FixGridRect = null;
private GameObject m_DraggingCard;
private RectTransform m_DraggingPlane; public bool isVertical = false;
private bool isSelf = false; private System.Action m_OnBeginDragCallBack = null;
private System.Action m_OnEndDragCallBack = null; private System.Action m_OnBeginScroll = null;
private System.Action m_OnEndScroll = null;
public void Init(CLocalCharInfo roleInfo, System.Action beginCallBack = null, System.Action endCallBack = null, System.Action beginScroll = null, System.Action endScroll = null)
{
m_OnBeginDragCallBack = beginCallBack;
m_OnEndDragCallBack = endCallBack;
m_OnBeginScroll = beginScroll;
m_OnEndScroll = endScroll;
}
public void OnBeginDrag(PointerEventData eventData)
{
Vector2 touchDeltaPosition = Vector2.zero;
#if UNITY_EDITOR
float delta_x = Input.GetAxis("Mouse X");
float delta_y = Input.GetAxis("Mouse Y");
touchDeltaPosition = new Vector2(delta_x, delta_y); #elif UNITY_ANDROID || UNITY_IPHONE
touchDeltaPosition = Input.GetTouch().deltaPosition;
#endif
if (isVertical)
{
if (Mathf.Abs(touchDeltaPosition.x) > Mathf.Abs(touchDeltaPosition.y))
{
isSelf = true;
var canvas = FindInParents<Canvas>(gameObject);
if (canvas == null)
return;
m_DraggingCard = createCard();
m_DraggingCard.transform.SetAsLastSibling(); m_DraggingCard.AddComponent<CIgnoreRayCast>(); if (dragOnSurfaces)
m_DraggingPlane = transform as RectTransform;
else
m_DraggingPlane = canvas.transform as RectTransform; SetDraggedPosition(eventData);
if (m_OnBeginDragCallBack != null)
{
m_OnBeginDragCallBack();
}
}
else
{
isSelf = false;
if (m_ScrollRect != null)
m_ScrollRect.OnBeginDrag(eventData);
}
}
else
{
if (Mathf.Abs(touchDeltaPosition.x) < Mathf.Abs(touchDeltaPosition.y))
{
isSelf = true;
var canvas = FindInParents<Canvas>(gameObject);
if (canvas == null)
return;
m_DraggingCard = createCard();
m_DraggingCard.transform.SetAsLastSibling(); m_DraggingCard.AddComponent<CIgnoreRayCast>(); if (dragOnSurfaces)
m_DraggingPlane = transform as RectTransform;
else
m_DraggingPlane = canvas.transform as RectTransform; SetDraggedPosition(eventData);
if (m_OnBeginDragCallBack != null)
{
m_OnBeginDragCallBack();
}
}
else
{
isSelf = false;
if (m_ScrollRect != null)
m_ScrollRect.OnBeginDrag(eventData);
}
}
if (m_OnBeginScroll != null)
m_OnBeginScroll();
} public void OnDrag(PointerEventData data)
{
if (isSelf)
{
if (m_DraggingCard != null)
{
SetDraggedPosition(data);
}
}
else
{
if (m_ScrollRect != null)
m_ScrollRect.OnDrag(data);
}
} private void SetDraggedPosition(PointerEventData data)
{
if (dragOnSurfaces && data.pointerEnter != null && data.pointerEnter.transform as RectTransform != null)
m_DraggingPlane = data.pointerEnter.transform as RectTransform; var rt = m_DraggingCard.GetComponent<RectTransform>();
Vector3 globalMousePos;
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(m_DraggingPlane, data.position, data.pressEventCamera, out globalMousePos))
{
rt.position = globalMousePos;
rt.rotation = m_DraggingPlane.rotation;
}
} private GameObject createCard()
{
CUIPlayedCharCardWidget charCardWidget = null;
m_DraggingCard = Instantiate(Resources.Load<GameObject>("Prefab/RoleCard") as GameObject);
if (m_DraggingCard == null)
{
return null;
}
charCardWidget = m_DraggingCard.GetComponent<CUIPlayedCharCardWidget>(); if (charCardWidget == null)
{
return null;
} CUIPlayedCharCardWidget.CUIContent uiContent = new CUIPlayedCharCardWidget.CUIContent(); charCardWidget.InitContent(uiContent);
return m_DraggingCard;
} public void OnEndDrag(PointerEventData eventData)
{
if (isSelf)
{
if (m_DraggingCard != null)
{
Destroy(m_DraggingCard);
if (m_OnEndDragCallBack != null)
{
m_OnEndDragCallBack();
}
}
}
else
{
if (m_ScrollRect != null)
m_ScrollRect.OnEndDrag(eventData);
if (m_FixGridRect != null)
m_FixGridRect.OnEndDrag(eventData);
} } static public T FindInParents<T>(GameObject go) where T : Component
{
if (go == null) return null;
var comp = go.GetComponent<T>(); if (comp != null)
return comp; Transform t = go.transform.parent;
while (t != null && comp == null)
{
comp = t.gameObject.GetComponent<T>();
t = t.parent;
}
return comp;
}
}
}

如果想要实现拖拽到目标位置的检测,还要在目标位置放一个Image并且添加上继承了IDropHandler,IPointerEnterHandler,IPointerExitHanler的组件。

 using System.Reflection;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Collections;
namespace Mga
{
public class CPlayedCardOnDrop : MonoBehaviour, IDropHandler, IPointerEnterHandler, IPointerExitHandler
{
public Image containerImage;
public Image receivingImage;
private Color normalColor;
public Color highlightColor = Color.yellow;
private int drapAreaIndex = ; void Start()
{
drapAreaIndex = System.Convert.ToInt32(this.transform.parent.name);
} public void OnEnable()
{
if (containerImage != null)
normalColor = containerImage.color;
} public void OnDrop(PointerEventData data)
{
containerImage.color = normalColor; if (receivingImage == null)
return;
Sprite dropSprite = GetDropSprite(data);
if (dropSprite != null)
receivingImage.overrideSprite = dropSprite;
} public void OnPointerEnter(PointerEventData data)
{
if (containerImage == null)
return;
Sprite dropSprite = GetDropSprite(data);
if (dropSprite != null)
containerImage.color = highlightColor;
} public void OnPointerExit(PointerEventData data)
{
if (containerImage == null)
return;
containerImage.color = normalColor;
} private Sprite GetDropSprite(PointerEventData data)
{
var originalObj = data.pointerDrag;
if (originalObj == null)
return null; var srcImage = originalObj.GetComponent<Image>();
if (srcImage == null)
return null; return srcImage.sprite;
}
}
}

在ScrollRect物体上添加吸附功能组件,工程里面要使用DoTween插件

 using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; //TODO:当前只试应横向的ScrollRect,还需要扩展支持纵向
public class CFixGridRect : MonoBehaviour, IEndDragHandler
{
public GameObject content;
public ScrollRect scorllRect;
public float itemWidth;
private RectTransform contentRectTf; private float formalPosX = ;
private float currentPosX = ;
private float halfItemLength = ; void Start()
{
if (itemWidth <= )
UnityEngine.Debug.LogError("请设置Item的宽度");
halfItemLength = itemWidth / ;
this.contentRectTf = this.content.GetComponent<RectTransform>();
} public void OnEndDrag(PointerEventData eventData)
{
this.scorllRect.StopMovement();
Vector2 afterDragPagePos = this.content.transform.localPosition;
currentPosX = afterDragPagePos.x; //当前拖动的位置 负
if (scorllRect.horizontalNormalizedPosition < || scorllRect.horizontalNormalizedPosition > )
return;
int count = (int)(Mathf.Abs(currentPosX) / itemWidth);
var targetPos = -(float)(count * itemWidth); if (((float)(count * itemWidth + halfItemLength)) < Mathf.Abs(currentPosX))
{
targetPos = -(float)((count + ) * itemWidth);
}
formalPosX = targetPos;
this.contentRectTf.DOLocalMoveX(targetPos, .2f);
}
}

具体DemoGit下载链接

欢迎加入U3D开发交流群:159875734


优化支持横竖屏的ScrollRect吸附功能

 using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; namespace Mga
{
public enum DragDirection
{
Horizontal,
Vertical,
} public class CFixGridRectBase : MonoBehaviour, IEndDragHandler
{
public class CUIContent
{
public GameObject ScrollRectContent;
public ScrollRect m_ScorllRect;
public float ItemSize;
public float ItemSpaceLength; //间隙
public float Margin = ; //顶部边缘间隙
public DragDirection m_DragDirection = DragDirection.Vertical;
}
private RectTransform contentRectTf;
private float halfItemLength = ;
private CUIContent m_uiContent = null;
private bool m_bWidgetReady = false;
void Start()
{
m_bWidgetReady = true;
_initContent();
} public void InitContent(CUIContent uiContent)
{
m_uiContent = uiContent; if (m_bWidgetReady)
_initContent();
} private void _initContent()
{
if (m_uiContent == null) return; if (m_uiContent.ItemSize <= )
{
UnityEngine.Debug.LogError("请设置Item的宽度");
return;
}
halfItemLength = m_uiContent.ItemSize / ;
this.contentRectTf = m_uiContent.ScrollRectContent.GetComponent<RectTransform>();
} public void OnEndDrag(PointerEventData eventData)
{
m_uiContent.m_ScorllRect.StopMovement();
Vector2 afterDragPagePos = m_uiContent.ScrollRectContent.transform.localPosition;
var itemLength = m_uiContent.ItemSize + m_uiContent.ItemSpaceLength;
if (m_uiContent.m_DragDirection == DragDirection.Horizontal)
{
var currentPosX = afterDragPagePos.x; //当前拖动的位置 负
currentPosX -= m_uiContent.Margin;
int count = (int)(Mathf.Abs(currentPosX) / m_uiContent.ItemSize);
if (m_uiContent.m_ScorllRect.horizontalNormalizedPosition <= )
{
return;
}
else if (m_uiContent.m_ScorllRect.horizontalNormalizedPosition >= ) //总数-当前显示的数量
{
return;
} var targetPosX = -(float)(count * itemLength);
if (((float)(targetPosX + halfItemLength)) < Mathf.Abs(currentPosX))
{
count++;
targetPosX = -(float)(count * itemLength);
}
this.contentRectTf.DOLocalMoveX(targetPosX, .2f);
}
else
{
var currentPosY = afterDragPagePos.y; //当前拖动的位置 正
currentPosY -= m_uiContent.Margin;
int count = (int)(Mathf.Abs(currentPosY) / itemLength);
if (m_uiContent.m_ScorllRect.verticalNormalizedPosition <= )
{
return;
}
else if (m_uiContent.m_ScorllRect.verticalNormalizedPosition >= ) //总数-当前显示的数量
{
return;
} var targetPosY = (float)(count * itemLength);
if (((float)(targetPosY + halfItemLength)) < Mathf.Abs(currentPosY))
{
count++;
targetPosY = (float)(count * itemLength);
}
this.contentRectTf.DOLocalMoveY(targetPosY, .2f);
}
}
}
}
 

补充

如果代码创建AnimationCurve默认是曲线,如果想要直线效果,可以在面板里面设置,也可以代码设置,如果代码设置如下:

 var curve2 = new AnimationCurve();
var key1 = new Keyframe(, );
key1.outTangent = ;
var key2 = new Keyframe(, );
key2.inTangent = ;
curve2.AddKey(key1);
curve2.AddKey(key2);
curve2.postWrapMode = WrapMode.Loop;
curve2.preWrapMode = WrapMode.Loop;

这样的话就是直线了。

[Unity UGUI]ScrollRect效果大全的相关教程结束。

《[Unity UGUI]ScrollRect效果大全.doc》

下载本文的Word格式文档,以方便收藏与打印。