基于Unity的扩展ScrollView开发(自动居中,缩放,明暗变化),用于商城系统,皮肤系统等UI

本博客参照于已有的大神博客的代码进行扩展实现。

话不多说,先上测试结果和源码。

下面这是打包运行时的情况,自动将离中心最近的Item平滑拉到中心,并放大,变亮,支持点击某一个Item直接居中。

下面是源码,对Inspector面板进行过重绘,用于bool型属性控制其他属性的显示与隐藏,以及List<>属性的展示。

/******************************************************
* 滚动条扩展脚本 
* Author: liaoweiyu
* function:自动居中最靠近中心的Item
           支持居中Item缩放
           支持非居中Item变暗
           支持点击Item居中
           带有居中Item改变时回调委托:_centerChanged
******************************************************/


using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
using System;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif
public enum ScrollDir_UGUI
{
    Horizontal,
    Vertical
}
public enum ImageType_UGUI
{
    Image
}
/// <summary>
/// 用于显示在Inspector的类
/// </summary>
[Serializable]
public class ScrollViewImage
{
    public ImageType_UGUI type;
    public Image image;
}
#if UNITY_EDITOR
/// <summary>
/// 重绘Inspector类
/// </summary>
[CustomEditor(typeof(CenterOnChild_UGUI))]
public class OverDrawInspector : Editor
{
    private SerializedObject serial;//序列化
    private ReorderableList list;//数组

    private SerializedProperty TransformSpeed, ScaleSpeed, OpacitySpeed, CenterChildScale, AddMask, MaskOpacity;
    /// <summary>
    /// 初始化
    /// </summary>
    void OnEnable()
    {
        serial = new SerializedObject(target);
        list = new ReorderableList(serial, serial.FindProperty("ChildMasks"), true, true, true, true);
        TransformSpeed = serial.FindProperty("TransformSpeed");
        ScaleSpeed = serial.FindProperty("ScaleSpeed");
        OpacitySpeed = serial.FindProperty("OpacitySpeed");
        CenterChildScale = serial.FindProperty("CenterChildScale");
        AddMask = serial.FindProperty("AddMask");
        MaskOpacity = serial.FindProperty("MaskOpacity");

        //绘制List标题
        list.drawHeaderCallback = (Rect rect) =>
        {
            EditorGUI.LabelField(rect, "MaskList");
        };
        //绘制List
        list.drawElementCallback =
        (Rect rect, int index, bool isActive, bool isFocused) =>
        {       
            var element = list.serializedProperty.GetArrayElementAtIndex(index);
            rect.y += 2;
            //绘制一个单元
            EditorGUI.PropertyField(new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
            element.FindPropertyRelative("type"), GUIContent.none);
            EditorGUI.PropertyField(new Rect(rect.x + 60, rect.y, rect.width - 60 - 30, EditorGUIUtility.singleLineHeight),
            element.FindPropertyRelative("image"), GUIContent.none);
        };
    }
    /// <summary>
    /// 重绘
    /// </summary>
    public override void OnInspectorGUI()
    {
        serial.Update();
        EditorGUILayout.PropertyField(TransformSpeed);
        EditorGUILayout.PropertyField(ScaleSpeed);
        EditorGUILayout.PropertyField(CenterChildScale);
        EditorGUILayout.PropertyField(AddMask);
        if (AddMask.boolValue == true)
        {
            EditorGUILayout.PropertyField(OpacitySpeed);
            EditorGUILayout.PropertyField(MaskOpacity);
            list.DoLayoutList();
        }
        serial.ApplyModifiedProperties();//应用
    }

}
#endif

/// <summary>
/// 逻辑类
/// </summary>
public class CenterOnChild_UGUI : MonoBehaviour, IEndDragHandler, IDragHandler, IBeginDragHandler
{
    public ScrollDir_UGUI Dir = ScrollDir_UGUI.Horizontal;
    /// <summary>
    /// 未居中组件遮罩透明度
    /// </summary>
    [SerializeField, Range(0, 1)]
    public float MaskOpacity = 0.4f;
    /// <summary>
    /// 是否加入遮罩
    /// </summary>
    [Header("---开启遮罩(将每个子组件的遮罩手动拖入)")]
    public bool AddMask = false;
    //是否正在居中
    private bool _isCentering = false;
    //是否正在缩放
    private bool _isScaling = false;
    //是否需要变换透明度
    private bool _needChangingCapacity = false;
    //是否完成透明度还原
    //private bool _needReturnCapacity = false;

    /// <summary>
    /// 居中过程移动速度,超出范围可能会出bug
    /// </summary>
    [SerializeField, Range(5, 25)]
    public float TransformSpeed = 20f;
    /// <summary>
    /// 居中过程缩放,小于15不流畅,大于30没有明显变化
    /// </summary>
    [SerializeField, Range(15, 30)]
    public float ScaleSpeed = 20f;
    /// <summary>
    /// 居中过程明暗变化,小于5等待时间过长,大于40没有明显变化
    /// </summary>
    [SerializeField, Range(5, 40)]
    public float OpacitySpeed = 10f;
    /// <summary>
    /// 居中组件缩放
    /// </summary>
    public Vector3 CenterChildScale = new Vector3(1.3f, 1.3f, 1.3f);
    /// <summary>
    /// 组件的遮罩
    /// </summary>
    [SerializeField]
    public List<ScrollViewImage> ChildMasks = new List<ScrollViewImage>();
    //插值运算临时变量
    private Vector3 Temp_EnlargeScale;//放大
    private Vector3 Temp_ReduceScale;//缩小
    private float Temp_BrightenScale;//变亮
    private float Temp_DarkenScale;//变暗

    private ScrollRect _scrollView;
    //grid
    private Transform _content;
    //子节点的坐标
    private List<float> _childrenPos = new List<float>();
    //当前需要运动到的目标坐标
    private float _targetPos;

    /// <summary>
    /// 当前中心child索引
    /// </summary>
    private int _curCenterChildIndex = 0;
    /// <summary>
    /// 上一次中心child索引
    /// </summary>
    private int _lastCenterChildIndex;
    /// <summary>
    /// 是否改变了中心组件做引
    /// </summary>
    private bool _isChangeCenter = false;
    /// <summary>
    /// 中心组件改变委托
    /// 用于触发改变中心组件后切换对应数据
    /// </summary>
    public delegate void CenterChangedCallBack(int _index);
    public CenterChangedCallBack _centerChanged;

    /// <summary>
    /// 当前中心ChildItem
    /// </summary>
    public GameObject CurCenterChildItem
    {
        get
        {
            GameObject centerChild = null;
            if (_content != null && _curCenterChildIndex >= 0 && _curCenterChildIndex < _content.childCount)
            {
                centerChild = _content.GetChild(_curCenterChildIndex).gameObject;
            }
            return centerChild;
        }
    }

    void Awake()
    {
        //初始化
        _curCenterChildIndex = 0;
        _lastCenterChildIndex = _curCenterChildIndex;
        Temp_EnlargeScale = CenterChildScale;
        Temp_ReduceScale = CenterChildScale;
        Temp_BrightenScale = MaskOpacity;
        Temp_DarkenScale = MaskOpacity;

        _scrollView = GetComponent<ScrollRect>();
        if (_scrollView == null)
        {
            Debug.LogError("错误:ScrollRect为空");
            return;
        }
        _content = _scrollView.content;

        LayoutGroup layoutGroup = null;
        layoutGroup = _content.GetComponent<LayoutGroup>();

        if (layoutGroup == null)
        {
            Debug.LogError("错误:LayoutGroup Component为空");
        }
        _scrollView.movementType = ScrollRect.MovementType.Unrestricted;
        float spacing = 0f;
        //根据dir计算坐标,Horizontal:存x,Vertical:存y
        switch (Dir)
        {
            case ScrollDir_UGUI.Horizontal:
                if (layoutGroup is HorizontalLayoutGroup)
                {
                    float childPosX = _scrollView.GetComponent<RectTransform>().rect.width * 0.5f - GetChildItemWidth(0) * 0.5f;
                    spacing = (layoutGroup as HorizontalLayoutGroup).spacing;
                    _childrenPos.Add(childPosX);
                    for (int i = 1; i < _content.childCount; i++)
                    {
                        childPosX -= GetChildItemWidth(i) * 0.5f + GetChildItemWidth(i - 1) * 0.5f + spacing;
                        _childrenPos.Add(childPosX);
                    }
                }
                else if (layoutGroup is GridLayoutGroup)
                {
                    GridLayoutGroup grid = layoutGroup as GridLayoutGroup;
                    float childPosX = _scrollView.GetComponent<RectTransform>().rect.width * 0.5f - grid.cellSize.x * 0.5f;
                    _childrenPos.Add(childPosX);
                    for (int i = 0; i < _content.childCount - 1; i++)
                    {
                        childPosX -= grid.cellSize.x + grid.spacing.x;
                        _childrenPos.Add(childPosX);
                    }
                }
                else
                {
                    Debug.LogError("错误:Horizontal ScrollView正在使用VerticalLayoutGroup");
                }
                break;
            case ScrollDir_UGUI.Vertical:
                if (layoutGroup is VerticalLayoutGroup)
                {
                    float childPosY = -_scrollView.GetComponent<RectTransform>().rect.height * 0.5f + GetChildItemHeight(0) * 0.5f;
                    spacing = (layoutGroup as VerticalLayoutGroup).spacing;
                    _childrenPos.Add(childPosY);
                    for (int i = 1; i < _content.childCount; i++)
                    {
                        childPosY += GetChildItemHeight(i) * 0.5f + GetChildItemHeight(i - 1) * 0.5f + spacing;
                        _childrenPos.Add(childPosY);
                    }
                }
                else if (layoutGroup is GridLayoutGroup)
                {
                    GridLayoutGroup grid = layoutGroup as GridLayoutGroup;
                    float childPosY = -_scrollView.GetComponent<RectTransform>().rect.height * 0.5f + grid.cellSize.y * 0.5f;
                    _childrenPos.Add(childPosY);
                    for (int i = 1; i < _content.childCount; i++)
                    {
                        childPosY += grid.cellSize.y + grid.spacing.y;
                        _childrenPos.Add(childPosY);
                    }
                }
                else
                {
                    Debug.LogError("错误:Vertical ScrollView正在使用HorizontalLayoutGroup");
                }
                break;
        }
        //初始化ScrollView起始位置
        SetCenterChild(0);
        _isChangeCenter = true;
    }

    private float GetChildItemWidth(int index)
    {
        return (_content.GetChild(index) as RectTransform).sizeDelta.x;
    }

    private float GetChildItemHeight(int index)
    {
        return (_content.GetChild(index) as RectTransform).sizeDelta.y;
    }

    void Update()
    {
        //中心组件改变时触发委托
        if (_isChangeCenter)
        {
            CenterChanged(_curCenterChildIndex);
            _isChangeCenter = false;
            _lastCenterChildIndex = _curCenterChildIndex;
        }
        if (AddMask)
        {
            //开启遮罩标志
            _needChangingCapacity = true;
        }
        else
        {
            //关闭遮罩
            _needChangingCapacity = false;
        }
        if (_isScaling)
        {
            //插值缩放
            for (int i = 0; i < _content.childCount; i++)
            {
                if (i == _curCenterChildIndex)
                {
                    //居中的放大
                    Temp_EnlargeScale = new Vector3(Mathf.Lerp(CurCenterChildItem.transform.localScale.x, CenterChildScale.x, ScaleSpeed * Time.deltaTime),
                                                    Mathf.Lerp(CurCenterChildItem.transform.localScale.y, CenterChildScale.x, ScaleSpeed * Time.deltaTime),
                                                    Mathf.Lerp(CurCenterChildItem.transform.localScale.z, CenterChildScale.x, ScaleSpeed * Time.deltaTime));
                    CurCenterChildItem.transform.localScale = Temp_EnlargeScale;
                }
                else
                {
                    Temp_ReduceScale = new Vector3(Mathf.Lerp(_content.GetChild(i).gameObject.transform.localScale.x, 1f, ScaleSpeed * Time.deltaTime),
                                                   Mathf.Lerp(_content.GetChild(i).gameObject.transform.localScale.y, 1f, ScaleSpeed * Time.deltaTime),
                                                   Mathf.Lerp(_content.GetChild(i).gameObject.transform.localScale.z, 1f, ScaleSpeed * Time.deltaTime));
                    _content.GetChild(i).gameObject.transform.localScale = Temp_ReduceScale;
                }
            }
            //修改透明度
            if (AddMask)
            {
                if (_needChangingCapacity)
                {
                    if (MaskCheck() == false)
                    {
                        Debug.LogError("错误:没有找到遮罩资源或者部分子组件缺少遮罩资源");
                        AddMask = false;
                    }
                    else
                    {
                        for (int i = 0; i < ChildMasks.Count; i++)
                        {
                            if (i == _curCenterChildIndex)
                            {
                                //居中的变亮
                                Temp_BrightenScale = Mathf.Lerp(ChildMasks[i].image.color.a, 0f, OpacitySpeed * Time.deltaTime);
                                SetChildOpacity(ChildMasks[i].image, Temp_BrightenScale);
                            }
                            else
                            {
                                Temp_DarkenScale = Mathf.Lerp(ChildMasks[i].image.color.a, MaskOpacity, OpacitySpeed * Time.deltaTime);
                                SetChildOpacity(ChildMasks[i].image, Temp_DarkenScale);
                            }
                        }
                    }
                    if (ChildMasks[_curCenterChildIndex].image.color.a <= 0.01f)
                    {
                        _needChangingCapacity = false;
                        //_needReturnCapacity = true;
                        _isScaling = false;
                    }
                }
            }
            else
            {
                //去掉遮罩
                for (int i = 0; i < ChildMasks.Count; i++)
                {
                    if (ChildMasks[i].image != null)
                    {
                        SetChildOpacity(ChildMasks[i].image, 0f);
                    }                  
                }
            }
        }
        
        if (_isCentering)
        {
            Vector3 v = _content.localPosition;
         
            //插值居中
            switch (Dir)
            {
                case ScrollDir_UGUI.Horizontal:
                    v.x = Mathf.Lerp(_content.localPosition.x, _targetPos, TransformSpeed * Time.deltaTime);
                    _content.localPosition = v;
                    if (Math.Abs(_content.localPosition.x - _targetPos) < 0.01f)
                    {
                        _isCentering = false;

                    }
                    break;
                case ScrollDir_UGUI.Vertical:
                    v.y = Mathf.Lerp(_content.localPosition.y, _targetPos, TransformSpeed * Time.deltaTime);
                    _content.localPosition = v;
                    if (Math.Abs(_content.localPosition.y - _targetPos) < 0.01f)
                    {
                        _isCentering = false;

                    }
                    break;
            }
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        _isScaling = true;
        switch (Dir)
        {
            case ScrollDir_UGUI.Horizontal:
                _targetPos = FindClosestChildPos(_content.localPosition.x, out _curCenterChildIndex);
                break;
            case ScrollDir_UGUI.Vertical:
                _targetPos = FindClosestChildPos(_content.localPosition.y, out _curCenterChildIndex);
                break;
        }
        _needChangingCapacity = true;
        _isChangeCenter = true;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        switch (Dir)
        {
            case ScrollDir_UGUI.Horizontal:
                _targetPos = FindClosestChildPos(_content.localPosition.x, out _curCenterChildIndex);
                break;
            case ScrollDir_UGUI.Vertical:
                _targetPos = FindClosestChildPos(_content.localPosition.y, out _curCenterChildIndex);
                break;
        }
        _isCentering = true;
        _needChangingCapacity = true;
        if (_lastCenterChildIndex != _curCenterChildIndex)
        {
            _isChangeCenter = true;
        }
        //_isScaling = false;
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        _isCentering = false;

        _curCenterChildIndex = -1;
    }
    /// <summary>
    /// 计算离中心最近的子组件
    /// </summary>
    /// <param name="currentPos"></param>
    /// <param name="curCenterChildIndex"></param>
    /// <returns></returns>
    private float FindClosestChildPos(float currentPos, out int curCenterChildIndex)
    {
        float closest = 0;
        float distance = Mathf.Infinity;
        curCenterChildIndex = -1;
        for (int i = 0; i < _childrenPos.Count; i++)
        {
            float p = _childrenPos[i];
            float d = Mathf.Abs(p - currentPos);
            if (d < distance)
            {
                distance = d;
                closest = p;
                curCenterChildIndex = i;
            }
        }
        return closest;
    }
    /// <summary>
    /// 检查遮罩是否丢失,True-未丢失,False-丢失
    /// </summary>
    /// <returns></returns>
    public bool MaskCheck()
    {
        if (ChildMasks == null || ChildMasks.Count == 0 || ChildMasks.Count != _content.childCount)
        {
            return false;
        }
        else
        {
            for (int i = 0; i < ChildMasks.Count; i++)
            {
                if (ChildMasks[i].image == null)
                {
                    return false;
                }               
            }
        }
        return true;
    }
    /// <summary>
    /// 设置组件的遮罩透明度
    /// </summary>
    public void SetChildOpacity(Image img, float opacity)
    {
        if (img != null)
        {
            img.color = new Color(1, 1, 1, opacity);
        }
    }
    /// <summary>
    /// 强制设置当前居中的子物体
    /// 用于点击事件
    /// </summary>
    /// <param name="_index"></param>
    public void SetCenterChild(int _index) 
    {      
        _curCenterChildIndex = _index;
        _isCentering = true;
        _needChangingCapacity = true;
        _isScaling = true;
        _targetPos = _childrenPos[_index];
        if (_curCenterChildIndex != _lastCenterChildIndex)
        {
            _isChangeCenter = true;
        }
    }
    /// <summary>
    /// 中心组件改变(委托)
    /// 用于根据中心组件变化触发对应数据变化
    /// </summary>
    public void CenterChanged(int _index)
    {
        if (_centerChanged != null)
        {
            _centerChanged(_index);          
        }

    }
}
动态滚动条:CenterOnChild_UGUI.cs
目前功能:
1、自动将离中心点最近的Item平滑居中。
2、居中Item平滑缩放。
3、居中Item平滑变亮,其余Item变暗。
4、点击Item居中。
5、附带居中Item改变时需要改变显示数据的委托

1和2可直接使用,3、4和5需要写一点调用代码。

功能1和2基本使用:

创建prefab,按照常规ScrollView结构。


注意Item摆放一定要左右靠边,gird节点的X坐标尽可能为0。(不为0可能有计算错误)


将脚本挂在ScrollView上。


同时在Grid节点上挂载一个HorizontalLayoutGroup(VerticalLayoutGroup)组件,用于自动设置间距。


此时可以直接运行使用1和2功能,平滑居中和平滑缩放。


以下功能3,4,5的使用需要有一定的Unity开发基础。

功能3的使用:

点击脚本的AddMask属性打钩,根据多少个子Item在MaskList创建多少个单元。(固定Item数量可以使用网上的无限循环ScrollView插件)

在下面的界面,参数可在运行时调整。


需要创建一个Mask物体把整个Item遮住。


然后将遮罩ImageExt拖入到脚本。(这里的ImageExt是我使用的扩展版Image,上面的源码是Image版本的,这个不用管)


即完成了对Item变亮变暗的引用。


功能4的使用:

将遮罩物体射线检测打开。


在Item的脚本中,关联点击委托。


写入委托,ItemIndex一定要设置一下,用来判断点击的哪一个,从0开始。


在遮罩物体上设置按钮事件。


然后进入主UI的脚本。(这里的主UI脚本也是我用的扩展型的,这些杂七杂八的参数不用管)


写下委托调用的方法。


最后在主UI的初始化时关联委托,m_content为ScrollView下面的grid。


完成了对功能4的引用。


功能5的使用:
在居中Item改变时触发委托事件。(在源码中)


只需要在外部关联委托函数即可完成改变中心Item切换显示数据。


以上是文字说明,具体测试工程,以及打包编译的exe进入下载链接使用参考。

http://download.csdn.net/download/n1ngyue/10249844


要实现 ScrollView 的无限滑动自动居中的功能,可以通过计算当前可见区域内的子项数量,然后根据剩余的空间进行自动居中。 以下是一个示例代码,可以将其挂载在 ScrollView 的 content 对象上: ```csharp using UnityEngine; using UnityEngine.UI; public class InfiniteScrollView : MonoBehaviour { public GameObject[] contentItems; public ScrollRect scrollRect; private RectTransform contentRectTransform; private int itemHeight; private int visibleItemCount; void Start() { contentRectTransform = GetComponent<RectTransform>(); itemHeight = (int)contentItems[0].GetComponent<RectTransform>().rect.height; visibleItemCount = Mathf.CeilToInt(scrollRect.GetComponent<RectTransform>().rect.height / itemHeight); } void Update() { float contentY = contentRectTransform.anchoredPosition.y; int firstVisibleIndex = Mathf.FloorToInt(-contentY / itemHeight); int lastVisibleIndex = Mathf.Min(firstVisibleIndex + visibleItemCount, contentItems.Length) - 1; int centerIndex = (firstVisibleIndex + lastVisibleIndex) / 2; float centerY = -centerIndex * itemHeight; float newY = Mathf.Lerp(contentY, centerY, Time.deltaTime * 5f); contentRectTransform.anchoredPosition = new Vector2(contentRectTransform.anchoredPosition.x, newY); if (contentY < -itemHeight * contentItems.Length + scrollRect.GetComponent<RectTransform>().rect.height) contentRectTransform.anchoredPosition = new Vector2(contentRectTransform.anchoredPosition.x, 0f); if (contentY > 0f) contentRectTransform.anchoredPosition = new Vector2(contentRectTransform.anchoredPosition.x, -itemHeight * contentItems.Length + scrollRect.GetComponent<RectTransform>().rect.height); } } ``` 以上代码基本上实现了无限滑动和自动居中的功能,可以根据实际需要调整一些参数或者添加额外的逻辑。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值