using AlicizaX.UI.Extension; using UnityEngine.EventSystems; using Direction = UnityEngine.UI.Slider.Direction; namespace UnityEngine.UI { [ExecuteAlways] [RequireComponent(typeof(RectTransform))] public class UXSlider : UXSelectable, IDragHandler, IInitializePotentialDragHandler, ICanvasElement { [SerializeField] private RectTransform m_FillRect; public RectTransform fillRect { get { return m_FillRect; } set { if (SetPropertyUtility.SetClass(ref m_FillRect, value)) { UpdateCachedReferences(); UpdateVisuals(); } } } [SerializeField] private RectTransform m_HandleRect; public RectTransform handleRect { get { return m_HandleRect; } set { if (SetPropertyUtility.SetClass(ref m_HandleRect, value)) { UpdateCachedReferences(); UpdateVisuals(); } } } [Space] [SerializeField] private Slider.Direction m_Direction = Slider.Direction.LeftToRight; public Slider.Direction direction { get { return m_Direction; } set { if (SetPropertyUtility.SetStruct(ref m_Direction, value)) UpdateVisuals(); } } [SerializeField] private float m_MinValue = 0; public float minValue { get { return m_MinValue; } set { if (SetPropertyUtility.SetStruct(ref m_MinValue, value)) { Set(m_Value); UpdateVisuals(); } } } [SerializeField] private float m_MaxValue = 1; public float maxValue { get { return m_MaxValue; } set { if (SetPropertyUtility.SetStruct(ref m_MaxValue, value)) { Set(m_Value); UpdateVisuals(); } } } [SerializeField] private bool m_WholeNumbers = false; public bool wholeNumbers { get { return m_WholeNumbers; } set { if (SetPropertyUtility.SetStruct(ref m_WholeNumbers, value)) { Set(m_Value); UpdateVisuals(); } } } [Space] [SerializeField] protected float m_Value; public virtual float value { get { return wholeNumbers ? Mathf.Round(m_Value) : m_Value; } set { Set(value); } } public virtual void SetValueWithoutNotify(float input) { Set(input, false); } public float normalizedValue { get { if (Mathf.Approximately(minValue, maxValue)) return 0; return Mathf.InverseLerp(minValue, maxValue, value); } set { this.value = Mathf.Lerp(minValue, maxValue, value); } } [Space] [SerializeField] private Slider.SliderEvent m_OnValueChanged = new Slider.SliderEvent(); public Slider.SliderEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } } // --- SMOOTHING FIELDS --- [Space] [SerializeField] private bool m_SmoothMovement = false; [SerializeField] private float m_SmoothSpeed = 8f; // value-per-second, larger=faster public bool smoothMovement { get { return m_SmoothMovement; } set { m_SmoothMovement = value; } } /// /// 平滑移动目标值(只有当 smoothMovement && !wholeNumbers 时生效) /// protected float m_TargetValue; protected bool m_IsSmoothMoving = false; // ------------------------ private Image m_FillImage; private Transform m_FillTransform; private RectTransform m_FillContainerRect; private Transform m_HandleTransform; private RectTransform m_HandleContainerRect; private Vector2 m_Offset = Vector2.zero; #pragma warning disable 649 private DrivenRectTransformTracker m_Tracker; #pragma warning restore 649 private bool m_DelayedUpdateVisuals = false; float stepSize { get { return wholeNumbers ? 1 : (maxValue - minValue) * 0.1f; } } protected UXSlider() { } #if UNITY_EDITOR protected override void OnValidate() { base.OnValidate(); if (wholeNumbers) { m_MinValue = Mathf.Round(m_MinValue); m_MaxValue = Mathf.Round(m_MaxValue); } //Onvalidate is called before OnEnabled. We need to make sure not to touch any other objects before OnEnable is run. if (IsActive()) { UpdateCachedReferences(); // Update rects in next update since other things might affect them even if value didn't change. m_DelayedUpdateVisuals = true; } if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying) CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this); } #endif protected override void OnEnable() { base.OnEnable(); UpdateCachedReferences(); Set(m_Value, false); // Update rects since they need to be initialized correctly. UpdateVisuals(); // 初始化 target m_TargetValue = m_Value; m_IsSmoothMoving = false; } protected override void OnDisable() { m_Tracker.Clear(); base.OnDisable(); } protected virtual void Update() { if (m_DelayedUpdateVisuals) { m_DelayedUpdateVisuals = false; Set(m_Value, false); UpdateVisuals(); } // 处理平滑移动(仅当开启时) if (m_IsSmoothMoving) { // 如果 wholeNumbers 为 true,则取消平滑,直接设置最终值(避免小数) if (wholeNumbers) { m_IsSmoothMoving = false; Set(m_TargetValue); } else { float maxDelta = m_SmoothSpeed * Time.unscaledDeltaTime; float newValue = Mathf.MoveTowards(m_Value, m_TargetValue, maxDelta); // 使用 Set 来保持更新视觉并触发回调(会在数值改变时触发) Set(newValue); if (Mathf.Approximately(newValue, m_TargetValue)) { m_IsSmoothMoving = false; // 确保最终值精确到目标(避免浮点残留) Set(m_TargetValue); } } } } protected override void OnDidApplyAnimationProperties() { m_Value = ClampValue(m_Value); float oldNormalizedValue = normalizedValue; if (m_FillContainerRect != null) { if (m_FillImage != null && m_FillImage.type == Image.Type.Filled) oldNormalizedValue = m_FillImage.fillAmount; else oldNormalizedValue = (reverseValue ? 1 - m_FillRect.anchorMin[(int)axis] : m_FillRect.anchorMax[(int)axis]); } else if (m_HandleContainerRect != null) oldNormalizedValue = (reverseValue ? 1 - m_HandleRect.anchorMin[(int)axis] : m_HandleRect.anchorMin[(int)axis]); UpdateVisuals(); if (oldNormalizedValue != normalizedValue) { UISystemProfilerApi.AddMarker("Slider.value", this); onValueChanged.Invoke(m_Value); } // UUM-34170 Apparently, some properties on slider such as IsInteractable and Normalcolor Animation is broken. // We need to call base here to render the animation on Scene base.OnDidApplyAnimationProperties(); } void UpdateCachedReferences() { if (m_FillRect && m_FillRect != (RectTransform)transform) { m_FillTransform = m_FillRect.transform; m_FillImage = m_FillRect.GetComponent(); if (m_FillTransform.parent != null) m_FillContainerRect = m_FillTransform.parent.GetComponent(); } else { m_FillRect = null; m_FillContainerRect = null; m_FillImage = null; } if (m_HandleRect && m_HandleRect != (RectTransform)transform) { m_HandleTransform = m_HandleRect.transform; if (m_HandleTransform.parent != null) m_HandleContainerRect = m_HandleTransform.parent.GetComponent(); } else { m_HandleRect = null; m_HandleContainerRect = null; } } float ClampValue(float input) { float newValue = Mathf.Clamp(input, minValue, maxValue); if (wholeNumbers) newValue = Mathf.Round(newValue); return newValue; } protected virtual void Set(float input, bool sendCallback = true) { // Clamp the input float newValue = ClampValue(input); // If the stepped value doesn't match the last one, it's time to update if (m_Value == newValue) return; m_Value = newValue; UpdateVisuals(); if (sendCallback) { UISystemProfilerApi.AddMarker("Slider.value", this); m_OnValueChanged.Invoke(newValue); } } protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); if (!IsActive()) return; UpdateVisuals(); } enum Axis { Horizontal = 0, Vertical = 1 } Axis axis { get { return (m_Direction == Direction.LeftToRight || m_Direction == Direction.RightToLeft) ? Axis.Horizontal : Axis.Vertical; } } bool reverseValue { get { return m_Direction == Direction.RightToLeft || m_Direction == Direction.TopToBottom; } } private void UpdateVisuals() { #if UNITY_EDITOR if (!Application.isPlaying) UpdateCachedReferences(); #endif m_Tracker.Clear(); if (m_FillContainerRect != null) { m_Tracker.Add(this, m_FillRect, DrivenTransformProperties.Anchors); Vector2 anchorMin = Vector2.zero; Vector2 anchorMax = Vector2.one; if (m_FillImage != null && m_FillImage.type == Image.Type.Filled) { m_FillImage.fillAmount = normalizedValue; } else { if (reverseValue) anchorMin[(int)axis] = 1 - normalizedValue; else anchorMax[(int)axis] = normalizedValue; } m_FillRect.anchorMin = anchorMin; m_FillRect.anchorMax = anchorMax; } if (m_HandleContainerRect != null) { m_Tracker.Add(this, m_HandleRect, DrivenTransformProperties.Anchors); Vector2 anchorMin = Vector2.zero; Vector2 anchorMax = Vector2.one; anchorMin[(int)axis] = anchorMax[(int)axis] = (reverseValue ? (1 - normalizedValue) : normalizedValue); m_HandleRect.anchorMin = anchorMin; m_HandleRect.anchorMax = anchorMax; } } void UpdateDrag(PointerEventData eventData, Camera cam) { RectTransform clickRect = m_HandleContainerRect ?? m_FillContainerRect; if (clickRect != null && clickRect.rect.size[(int)axis] > 0) { Vector2 position = Vector2.zero; if (!MultipleDisplayUtilities.GetRelativeMousePositionForDrag(eventData, ref position)) return; Vector2 localCursor; if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, position, cam, out localCursor)) return; localCursor -= clickRect.rect.position; float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]); normalizedValue = (reverseValue ? 1f - val : val); } } private bool MayDrag(PointerEventData eventData) { return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left; } public override void OnPointerDown(PointerEventData eventData) { if (!MayDrag(eventData)) return; base.OnPointerDown(eventData); // 取消任何平滑移动(开始拖拽时) m_IsSmoothMoving = false; m_TargetValue = m_Value; m_Offset = Vector2.zero; if (m_HandleContainerRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera)) { Vector2 localMousePos; if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.pressEventCamera, out localMousePos)) m_Offset = localMousePos; } else { // Outside the slider handle - jump to this point instead UpdateDrag(eventData, eventData.pressEventCamera); } } public void OnDrag(PointerEventData eventData) { if (!MayDrag(eventData)) return; // 拖拽也取消平滑 m_IsSmoothMoving = false; m_TargetValue = m_Value; UpdateDrag(eventData, eventData.pressEventCamera); } public override void OnMove(AxisEventData eventData) { if (!IsActive() || !IsInteractable()) { base.OnMove(eventData); return; } // 基准值:如果当前已经在平滑移动中,累加到 target 上;否则从当前 value 开始。 float currentBase = m_IsSmoothMoving ? m_TargetValue : value; switch (eventData.moveDir) { case MoveDirection.Left: if (axis == Axis.Horizontal && FindSelectableOnLeft() == null) { float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize); if (m_SmoothMovement && !wholeNumbers) { m_TargetValue = dest; m_IsSmoothMoving = true; } else { Set(dest); } } else base.OnMove(eventData); break; case MoveDirection.Right: if (axis == Axis.Horizontal && FindSelectableOnRight() == null) { float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize); if (m_SmoothMovement && !wholeNumbers) { m_TargetValue = dest; m_IsSmoothMoving = true; } else { Set(dest); } } else base.OnMove(eventData); break; case MoveDirection.Up: if (axis == Axis.Vertical && FindSelectableOnUp() == null) { float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize); if (m_SmoothMovement && !wholeNumbers) { m_TargetValue = dest; m_IsSmoothMoving = true; } else { Set(dest); } } else base.OnMove(eventData); break; case MoveDirection.Down: if (axis == Axis.Vertical && FindSelectableOnDown() == null) { float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize); if (m_SmoothMovement && !wholeNumbers) { m_TargetValue = dest; m_IsSmoothMoving = true; } else { Set(dest); } } else base.OnMove(eventData); break; } } public override UXSelectable FindSelectableOnLeft() { if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal) return null; return base.FindSelectableOnLeft(); } public override UXSelectable FindSelectableOnRight() { if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal) return null; return base.FindSelectableOnRight(); } public override UXSelectable FindSelectableOnUp() { if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical) return null; return base.FindSelectableOnUp(); } public override UXSelectable FindSelectableOnDown() { if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical) return null; return base.FindSelectableOnDown(); } public void OnInitializePotentialDrag(PointerEventData eventData) { eventData.useDragThreshold = false; } public void SetDirection(Direction direction, bool includeRectLayouts) { Axis oldAxis = axis; bool oldReverse = reverseValue; this.direction = direction; if (!includeRectLayouts) return; if (axis != oldAxis) RectTransformUtility.FlipLayoutAxes(transform as RectTransform, true, true); if (reverseValue != oldReverse) RectTransformUtility.FlipLayoutOnAxis(transform as RectTransform, (int)axis, true, true); } public void Rebuild(CanvasUpdate executing) { #if UNITY_EDITOR if (executing == CanvasUpdate.Prelayout) onValueChanged.Invoke(value); #endif } public void LayoutComplete() { } public void GraphicUpdateComplete() { } } }