Unity之圆环slider

一、参考文章

Unity_圆环滑动条(圆形、弧形滑动条)_unity弧形滑动条-CSDN博客

此滑动条拖动超过360后继续往前滑动值会从0开始,正常我们超过360度时不可在滑动。

二、 超过360度不可滑动问题解决

参考HTML文章制作: https://www.cnblogs.com/pangys/p/13201808.html

下载链接

修改后的脚本:

cs 复制代码
using OfficeOpenXml.FormulaParsing.Excel.Functions;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Math;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(RectTransform)), ExecuteInEditMode]
public class AnnularSlider : Selectable, IDragHandler, ICanvasElement
{
    [Serializable]
    public class DragEvent : UnityEvent
    {
    }

    [Serializable]
    public class DragValueEvent : UnityEvent<float>
    {
    }

    [SerializeField] private Image _fillImage;
    [SerializeField] private Image.Origin360 _fillOrigin;
    [SerializeField] private bool _clockwise;
    [SerializeField] private bool _wholeNumbers;
    [SerializeField] private float _minValue;
    [SerializeField] private float _maxValue = 1f;
    [SerializeField] private float _maxAngle = 360f;
    [SerializeField] private float _value;

    [SerializeField] private RectTransform _handleRect;
    [SerializeField] private float _radius = 10f;
    [SerializeField] private bool _towardCenter;

    [SerializeField] private DragValueEvent _onValueChanged = new DragValueEvent();
    [SerializeField] private DragEvent _onBeginDragged = new DragEvent();
    [SerializeField] private DragEvent _onDragging = new DragEvent();
    [SerializeField] private DragEvent _onEndDragged = new DragEvent();

    private bool _delayedUpdateVisuals;

    public Image FillImage
    {
        get { return _fillImage; }
        set
        {
            if (SetClass(ref _fillImage, value))
            {
                UpdateCachedReferences();
                UpdateVisuals();
            }
        }
    }

    public Image.Origin360 FillOrigin
    {
        get { return _fillOrigin; }
        set
        {
            if (SetStruct(ref _fillOrigin, value))
            {
                UpdateVisuals();
            }
        }
    }

    public bool Clockwise
    {
        get { return _clockwise; }
        set
        {
            if (SetStruct(ref _clockwise, value))
            {
                UpdateVisuals();
            }
        }
    }

    public bool WholeNumbers
    {
        get { return _wholeNumbers; }
        set
        {
            if (SetStruct(ref _wholeNumbers, value))
            {
                UpdateValue(_value);
                UpdateVisuals();
            }
        }
    }

    public float MinValue
    {
        get { return _minValue; }
        set
        {
            if (SetStruct(ref _minValue, value))
            {
                UpdateValue(_value);
                UpdateVisuals();
            }
        }
    }

    public float MaxValue
    {
        get { return _maxValue; }
        set
        {
            if (SetStruct(ref _maxValue, value))
            {
                UpdateValue(_value);
                UpdateVisuals();
            }
        }
    }

    public float MaxAngle
    {
        get { return _maxAngle; }
        set
        {
            if (SetStruct(ref _maxAngle, value))
            {
                UpdateVisuals();
            }
        }
    }

    public float Value
    {
        get
        {
            if (_wholeNumbers) return Mathf.Round(_value);
            return _value;
        }

        set { UpdateValue(value); }
    }

    public RectTransform HandleRect
    {
        get { return _handleRect; }
        set
        {
            if (SetClass(ref _handleRect, value))
            {
                UpdateVisuals();
            }
        }
    }

    public float Radius
    {
        get { return _radius; }
        set
        {
            if (SetStruct(ref _radius, value))
            {
                UpdateVisuals();
            }
        }
    }

    public bool TowardCenter
    {
        get { return _towardCenter; }
        set
        {
            if (SetStruct(ref _towardCenter, value))
            {
                UpdateVisuals();
            }
        }
    }

    public DragValueEvent OnValueChanged
    {
        get { return _onValueChanged; }
        set { _onValueChanged = value; }
    }

    public DragEvent OnBeginDragged
    {
        get { return _onBeginDragged; }
        set { _onBeginDragged = value; }
    }

    public DragEvent OnDragging
    {
        get { return _onDragging; }
        set { _onDragging = value; }
    }

    public DragEvent OnEndDragged
    {
        get { return _onEndDragged; }
        set { _onEndDragged = value; }
    }

    public float NormalizedValue
    {
        get
        {
            if (Mathf.Approximately(_minValue, _maxValue)) return 0;
            return Mathf.InverseLerp(_minValue, _maxValue, Value);
        }
        set { 
            Value = Mathf.Lerp(_minValue, _maxValue, value);
        }
    }

    protected override void OnEnable()
    {
        base.OnEnable();
        UpdateCachedReferences();
        UpdateValue(_value, false);
        UpdateVisuals();
    }

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        base.OnValidate();

        if (WholeNumbers)
        {
            _minValue = Mathf.Round(_minValue);
            _maxValue = Mathf.Round(_maxValue);
        }

        //Onvalidate is called before OnEnabled. We need to make sure not to touch any other objects before OnEnable is run.
        if (IsActive())
        {
            UpdateCachedReferences();
            UpdateValue(_value, false);
            _delayedUpdateVisuals = true;
        }


        //if (!UnityEditor.PrefabUtility.IsComponentAddedToPrefabInstance(this) && !Application.isPlaying)
        //    CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
    }
#endif

    protected virtual void Update()
    {
        if (_delayedUpdateVisuals)
        {
            _delayedUpdateVisuals = false;
            UpdateVisuals();
        }


    }

    public override void OnPointerDown(PointerEventData eventData)
    {
        if (MayEvent(eventData))
        {
            OnBeginDragged.Invoke();
        }
    }
    public double degValue;//存储
    public void OnDrag(PointerEventData eventData)
    {
        if (!MayEvent(eventData)) return;
        _onDragging.Invoke();

        Vector2 localPoint;//鼠标在ui中的位置
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(_fillImage.rectTransform, eventData.position, eventData.pressEventCamera, out localPoint))
        {

            var deg = XYToDeg(localPoint.x, localPoint.y);//获取角度(用弧度制π来表示)
            double min = 0, max = 0;//滑块起点位置区间

            //根据起点位置进行换算
            switch (_fillOrigin)
            {
                case Image.Origin360.Bottom:
                    //第四象限 起点为270°终点为360 即:[3π/2, 2π]
                    min = Math.PI * 1.5;
                    max = Math.PI * 2;

                    break;
                case Image.Origin360.Right:
                    //deg = deg;//在第一象限为起点不换算
                    min =max =0;
                    break;
                case Image.Origin360.Top:

                    //第二象限为起点 区间[π/2,π]=>[90,180]
                    min = Math.PI * 0.5;
                    max = Math.PI * 2;

                    break;
                case Image.Origin360.Left:

                    //第三象限为起点 区间[π,2π]=>[180,360]
                    min = Math.PI;
                    max = Math.PI * 2;

                    break;
                default:
                    break;
            }

            //起点位置差值换算
            if (deg >= min && deg <= max)
                deg -= min;
            else
                deg += (max - min);

            deg = Clockwise ? Math.PI * 2 - deg :  deg; //顺、逆时针方向
            var constMaxAngle = MaxAngle / 180;//圆的最大弧度常数
            var radian = deg / Math.PI; //除π得到常数值 [0,2π]/π=[0,2]
            var ratio = (radian / constMaxAngle) * 100;//鼠标移动的角度在圆的最大角度中的占比

            //限制value的最大最小值
            var maxValue = constMaxAngle * 100; //最大value值
            if (ratio > maxValue || ratio < 0)return; 
            if (ratio >= maxValue) ratio = maxValue;
            if (ratio <= 0) ratio = 0;


            /*在圆中360°和0°是首尾相连的,为了防止鼠标滑动到360在往前滑动变成0的问题,需要进行一个计算判断。
             * 举例当鼠标滑动到360度时ratio和degValue值都为100,此时鼠标再往上滑动ratio值就会从0开始。
             * 在赋值degValue时使用Math.Abs(ratio - degValue)求两个数的绝对值,然后在设置一个最大阈值10。即可解决问题
             * Math.Abs(100 - 0)得出结果为100。我们设置的最大阈值为10,当鼠标再往上滑动时超出最大阈值不在赋值
             */
            if (Math.Abs(ratio - degValue) > 10) return;

            //给value赋值
            if (degValue != Math.Round(ratio))
            {
                degValue = Math.Round(ratio);
                NormalizedValue = (float)degValue / 100;
                UpdateVisuals();
            }


        }
    }
    #region 获取角度(用弧度制π来表示)
    double XYToDeg(float lx, float ly)
    {
        /* 这里的lx和ly已经换算过的代表了鼠标以圆中心点为原点的坐标向量,
         * 连接原点和鼠标坐标位置形成一个直角三角形

                    |y轴
                    |
                * * | * *
             *      |      *
           *        |   。   *
          *         |  /|     *
    ------------------------|--------------------------->
          *         |         *     x轴
           *        |        *
             *      |      *
                * * | * *
                    |
                    |    
 */

        /* 1.获取角度(用弧度制π来表示)
         * 利用反三角函数Math.Atan(tanθ)来获取角度
         * 在三角函数中 lx代表邻边,ly代表对边。根据三角函数可以得出 tanθ=ly/lx (关于直角的绘制看上方例图)
         * 反三角函数Arctan(ly/lx)可得出角度
         */
        double adeg = Math.Atan(ly / lx);

        /* 2.将角度限制在[0 , 2π]区间。
         * 已知Math.Atan函数 返回的数值在[-π/2 , π/2] 换成角度是[-90,90],
         * 但我们需要获取[0 , 2π]即:[0,360]区间的实际值用于计算
         * 所以需要通过lx和ly的正负判断所在象限用于换算成[0 , 2π]区间的值
         */
        double deg = 0;

        if (lx >= 0 && ly >= 0)
        {
            /*第一象限: 
             * 得到的角度在[0,90]区间,即:[0,π/2]
             * 不换算
             */
            deg = adeg;
        }
        if (lx <= 0 && ly >= 0) 
        {
            /*第二象限:
             * 得到的角度在[-90,0]区间,即:[-π/2, 0]
             * 需要换算为[90,180]区间 所以要+π。(在角度制中π为180)
             */
            deg = adeg + Math.PI;
        }
        if (lx <= 0 && ly <= 0)
        {
            /*第三象限:
             * 得到的角度在[0,90]区间,即:[0,π/2]
             * 需要换算为[180,270]区间 所以要+π。(在角度制中π为180)
             */
            deg = adeg + Math.PI;
        }
        if (lx > 0 && ly < 0) 
        {
            /*第四象限:
             * 得到的角度在[-90,00]区间,即:[-π/2, 0]
             * 需要换算为[270,360]区间 所以要+2π。(在角度制中π为180)
             */
            deg = adeg + Math.PI * 2;
        }

        return deg;
    }

    #endregion

    public override void OnPointerUp(PointerEventData eventData)
    {
        if (MayEvent(eventData))
        {
            OnEndDragged.Invoke();
            //Debug.Log("OnEndDragged");
        }
    }

    public void Rebuild(CanvasUpdate executing)
    {
    }

    public void LayoutComplete()
    {
    }

    public void GraphicUpdateComplete()
    {
    }

    /// <summary>
    /// 返回是否可交互
    /// </summary>
    /// <returns></returns>
    private bool MayEvent(PointerEventData eventData)
    {
        return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left;
    }

    /// <summary>
    /// 更新缓存引用
    /// </summary>
    private void UpdateCachedReferences()
    {
        if (_fillImage)
        {
            _fillImage.type = Image.Type.Filled;
            _fillImage.fillMethod = Image.FillMethod.Radial360;
            _fillImage.fillOrigin = (int)_fillOrigin;
            _fillImage.fillClockwise = _clockwise;
        }
    }

    /// <summary>
    /// 更新视觉效果
    /// </summary>
    private void UpdateVisuals()
    {
#if UNITY_EDITOR
        if (!Application.isPlaying)
            UpdateCachedReferences();
#endif

        var angle = NormalizedValue * _maxAngle;

        if (_fillImage)
        {
            _fillImage.fillAmount = angle / 360f;
        }

        if (_handleRect)
        {
            _handleRect.transform.localPosition = GetPointFromFillOrigin(ref angle);
            if (_towardCenter)
            {
                _handleRect.transform.localEulerAngles = new Vector3(0f, 0f, angle);
            }
        }
    }

    /// <summary>
    /// 更新Value
    /// </summary>
    /// <param name="value"></param>
    /// <param name="sendCallback"></param>
    private void UpdateValue(float value, bool sendCallback = true)
    {
        value = Mathf.Clamp(value, _minValue, _maxValue);
        if (_wholeNumbers) value = Mathf.Round(value);
        if (_value.Equals(value)) return;
        _value = value;

        UpdateVisuals();
        if (sendCallback)
        {
            _onValueChanged.Invoke(_value);
            //Debug.Log("OnValueChanged" + _value);
        }
    }

    /// <summary>
    /// 返回基于起始点的角度(0°~360°)
    /// </summary>
    /// <param name="point"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    private float GetAngleFromFillOrigin(Vector2 point)
    {
        var angle = Mathf.Atan2(point.y, point.x) * Mathf.Rad2Deg; //相对于X轴右侧(FillOrigin.Right)的角度
        //转换为相对于起始点的角度
        switch (_fillOrigin)
        {
            case Image.Origin360.Bottom:
                angle = _clockwise ? 270 - angle : 90 + angle;
                break;
            case Image.Origin360.Right:
                angle = _clockwise ? -angle : angle;
                break;
            case Image.Origin360.Top:
                angle = _clockwise ? 90 - angle : 270 + angle;
                break;
            case Image.Origin360.Left:
                angle = _clockwise ? 180 - angle : 180 + angle;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        转 360 °表示
        //if (angle > 360)
        //{
        //    angle -= 360;
        //}

        //if (angle < 0)
        //{
        //    angle += 360;
        //}

        return angle;
    }

    /// <summary>
    /// 返回基于起始点、角度、半径的位置
    /// </summary>
    /// <param name="angle"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    private Vector2 GetPointFromFillOrigin(ref float angle)
    {
        //转化为相对于X轴右侧(FillOrigin.Right)的角度
        switch (_fillOrigin)
        {
            case Image.Origin360.Bottom:
                angle = _clockwise ? 270 - angle : angle - 90;
                break;
            case Image.Origin360.Right:
                angle = _clockwise ? -angle : angle;
                break;
            case Image.Origin360.Top:
                angle = _clockwise ? 90 - angle : 90 + angle;
                break;
            case Image.Origin360.Left:
                angle = _clockwise ? 180 - angle : 180 + angle;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        var radian = angle * Mathf.Deg2Rad;
        return new Vector2(Mathf.Cos(radian) * _radius, Mathf.Sin(radian) * _radius);
    }

    //设置结构
    private static bool SetStruct<T>(ref T setValue, T value) where T : struct
    {
        if (EqualityComparer<T>.Default.Equals(setValue, value)) return false;
        setValue = value;
        return true;
    }

    private static bool SetClass<T>(ref T setValue, T value) where T : class
    {
        if (setValue == null && value == null || setValue != null && setValue.Equals(value)) return false;
        setValue = value;
        return true;
    }
}

三、使用方法

相关推荐
WarPigs7 小时前
游戏签到系统
unity
小拉达不是臭老鼠10 小时前
Unity中的UI系统之UGUI
学习·ui·unity
万兴丶10 小时前
Coplay适用于 Unity 的“Al 代理”使用指南
unity·游戏引擎·ai编程
魔士于安13 小时前
Unity材质球大合集
unity·游戏引擎·材质
mxwin14 小时前
Unity Shader 冰面 Shader 制作原理与流程
unity·游戏引擎·shader
玖玥拾16 小时前
Cocos学习笔记:关卡系统、音频管理与物理控制
游戏引擎·cocos2d
小拉达不是臭老鼠16 小时前
Unity中的UI系统之UGUI_登陆面板实现
ui·unity
郝学胜-神的一滴16 小时前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
玖玥拾1 天前
Cocos学习笔记:滚动视图、关卡系统与本地存储
游戏引擎·cocos2d
元气少女小圆丶1 天前
SenseGlove Nova 2+Unity开发笔记2
笔记·unity·游戏引擎