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;
    }
}

三、使用方法

相关推荐
weixin_531638943 小时前
Rokid AR交互开发工具对比
unity·游戏引擎·xr
Magnum Lehar13 小时前
wpf3d游戏引擎ControlTemplate.xaml.cs文件实现
游戏引擎·wpf
留待舞人归13 小时前
【Unity3D优化】优化多语言字体包大小
游戏·unity·游戏引擎·unity3d·优化
wsdchong之小马过河14 小时前
2025虚幻引擎一般用什么模型格式
游戏引擎·虚幻
Magnum Lehar20 小时前
wpf游戏引擎前端的Transform.cs实现
前端·游戏引擎·wpf
Magnum Lehar1 天前
wpf3d游戏引擎前端ControlTemplate实现
前端·游戏引擎·wpf
9765033351 天前
iOS 审核 cocos 4.3a【苹果机审的“分层阈值”设计】
flutter·游戏·unity·ios
EQ-雪梨蛋花汤2 天前
【Unity笔记】Unity Animation组件使用详解:Play方法重载与动画播放控制
笔记·unity·游戏引擎
AgilityBaby2 天前
Untiy打包安卓踩坑
android·笔记·学习·unity·游戏引擎
菌菌巧乐兹2 天前
Unity | AmplifyShaderEditor插件基础(第九集:旗子进阶版)
unity·游戏引擎