using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; namespace Kill.UI.Components { /// /// 滑动选择器组件 /// 支持:主次刻度 + 只在主刻度显示数字 + 小数精度 /// public class SlideSelector : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { [Header("数值范围")] public float MinValue = 2.0f; // 最小值 public float MaxValue = 4.0f; // 最大值 public float DefaultValue = 3.0f; // 默认值 public float Step = 0.1f; // 刻度间隔(如 0.1 表示 0.1度) public int DecimalPlaces = 1; // 小数位数(显示用) [Header("间距设置")] public float TickSpacing = 20f; // 刻度间距(像素) public float NumberOffsetY = -50f; // 数字Y轴偏移 [Header("刻度类型设置")] public bool ShowNumberOnAllTicks = false; // true=每个刻度都显示数字,false=只在主刻度显示 public float MajorStep = 1.0f; // 主刻度间隔(如 1.0 表示每隔1度显示大刻度) public float MiddleStep = 0.5f; // 中刻度间隔(如 0.5 表示每隔0.5度显示中刻度,仅在ShowNumberOnAllTicks=false时有效) [Header("主刻度设置 (如 2, 3, 4)")] public float MajorTickHeight = 50f; // 主刻度高度 public float MajorTickWidth = 3f; // 主刻度宽度 public Color MajorTickColor = new Color(0.7f, 0.7f, 0.7f); // 主刻度颜色 [Header("中刻度设置 (如 2.5, 3.5)")] public float MiddleTickHeight = 35f; // 中刻度高度 public float MiddleTickWidth = 2f; // 中刻度宽度 public Color MiddleTickColor = new Color(0.6f, 0.6f, 0.6f); // 中刻度颜色 [Header("小刻度设置 (如 2.1, 2.2)")] public float MinorTickHeight = 20f; // 小刻度高度 public float MinorTickWidth = 2f; // 小刻度宽度 public Color MinorTickColor = new Color(0.5f, 0.5f, 0.5f); // 小刻度颜色 [Header("数字样式")] public float NumberSize = 36f; // 数字字体大小 public Color NumberColor = Color.white; // 数字颜色 public Vector2 NumberRectSize = new Vector2(60, 50); // 数字区域大小 [Header("选中效果")] public Color SelectedTickColor = new Color(0, 1, 1); // 选中刻度颜色(青色) public Color SelectedNumberColor = new Color(0, 1, 1); // 选中数字颜色 public float SelectedScale = 1.0f; // 选中缩放 public float NormalScale = 1.0f; // 普通缩放 public float SnapDuration = 0.15f; // 吸附动画时长 [Header("组件引用")] public RectTransform Content; // 内容容器 public RectTransform Viewport; // 可视区域(用于计算中心) // 事件 public event Action OnValueChanged; // 值改变事件 public event Action OnValueSelected; // 值选中事件 // 运行时数据 private float currentValue; private bool isDragging = false; private float contentX = 0f; private Coroutine snapCoroutine; // 刻度数据 private class TickData { public float Value; public RectTransform TickRect; public Image TickImage; public RectTransform NumberRect; public Text NumberText; public bool IsMajor; } private List ticks = new List(); /// /// 初始化选择器 /// public void Initialize() { ClearContent(); CreateTicks(); SetValue(DefaultValue, false); } /// /// 清空内容 /// private void ClearContent() { foreach (var tick in ticks) { if (tick.TickRect != null) Destroy(tick.TickRect.gameObject); if (tick.NumberRect != null) Destroy(tick.NumberRect.gameObject); } ticks.Clear(); if (Content != null) { contentX = 0f; Content.anchoredPosition = Vector2.zero; } } /// /// 创建刻度 /// private void CreateTicks() { if (Content == null) { Debug.LogError("[SlideSelector] Content is null!"); return; } // 计算总刻度数 int totalSteps = Mathf.RoundToInt((MaxValue - MinValue) / Step); float totalWidth = totalSteps * TickSpacing; // 设置内容宽度 float viewportWidth = Viewport != null ? Viewport.rect.width : 800f; float centerOffset = viewportWidth / 2f; Content.sizeDelta = new Vector2(totalWidth + centerOffset * 2, Content.sizeDelta.y); for (int i = 0; i <= totalSteps; i++) { float value = MinValue + i * Step; // 判断刻度类型 float majorRemainder = Mathf.Abs(value % MajorStep); bool isMajor = majorRemainder < 0.001f || Mathf.Abs(majorRemainder - MajorStep) < 0.001f; // 只有在非"全部显示数字"模式下才判断中刻度 bool isMiddle = false; if (!ShowNumberOnAllTicks) { float middleRemainder = Mathf.Abs(value % MiddleStep); isMiddle = !isMajor && (middleRemainder < 0.001f || Mathf.Abs(middleRemainder - MiddleStep) < 0.001f); } TickData tick = new TickData { Value = value, IsMajor = isMajor }; float xPos = centerOffset + i * TickSpacing; // 确定刻度高宽和颜色 float tickHeight, tickWidth; Color tickColor; if (isMajor) { tickHeight = MajorTickHeight; tickWidth = MajorTickWidth; tickColor = MajorTickColor; } else if (isMiddle) { tickHeight = MiddleTickHeight; tickWidth = MiddleTickWidth; tickColor = MiddleTickColor; } else { tickHeight = MinorTickHeight; tickWidth = MinorTickWidth; tickColor = MinorTickColor; } // 创建刻度线 GameObject tickObj = new GameObject($"Tick_{value:F1}"); tickObj.transform.SetParent(Content, false); tick.TickRect = tickObj.AddComponent(); tick.TickImage = tickObj.AddComponent(); // 底部对齐:pivot 在底部,位置在 y=0(底部) tick.TickRect.anchorMin = new Vector2(0, 0); tick.TickRect.anchorMax = new Vector2(0, 0); tick.TickRect.pivot = new Vector2(0.5f, 0); tick.TickRect.anchoredPosition = new Vector2(xPos, 0); tick.TickRect.sizeDelta = new Vector2(tickWidth, tickHeight); // 刻度颜色 tick.TickImage.color = tickColor; // 判断是否显示数字 bool shouldShowNumber = ShowNumberOnAllTicks || isMajor; if (shouldShowNumber) { GameObject numObj = new GameObject($"Number_{value:F0}"); numObj.transform.SetParent(Content, false); tick.NumberRect = numObj.AddComponent(); tick.NumberText = numObj.AddComponent(); // 数字也底部对齐 tick.NumberRect.anchorMin = new Vector2(0, 0); tick.NumberRect.anchorMax = new Vector2(0, 0); tick.NumberRect.pivot = new Vector2(0.5f, 0); tick.NumberRect.anchoredPosition = new Vector2(xPos, NumberOffsetY); tick.NumberRect.sizeDelta = NumberRectSize; // 显示数值(整数或小数) if (ShowNumberOnAllTicks) { // 每个刻度都显示数字,根据精度显示 tick.NumberText.text = value.ToString($"F{DecimalPlaces}").TrimEnd('0').TrimEnd('.'); } else { // 只在主刻度显示整数 tick.NumberText.text = Mathf.RoundToInt(value).ToString(); } tick.NumberText.font = Resources.GetBuiltinResource("LegacyRuntime.ttf"); tick.NumberText.fontSize = Mathf.RoundToInt(NumberSize); tick.NumberText.alignment = TextAnchor.MiddleCenter; tick.NumberText.color = NumberColor; } ticks.Add(tick); } } public void OnBeginDrag(PointerEventData eventData) { isDragging = true; if (snapCoroutine != null) { StopCoroutine(snapCoroutine); snapCoroutine = null; } } public void OnDrag(PointerEventData eventData) { float delta = eventData.delta.x; contentX += delta; ApplyContentPosition(); UpdateHighlight(); } public void OnEndDrag(PointerEventData eventData) { isDragging = false; SnapToNearest(); } /// /// 应用内容位置 /// private void ApplyContentPosition() { if (Content == null) return; float viewportWidth = Viewport != null ? Viewport.rect.width : 800f; float centerOffset = viewportWidth / 2f; int totalSteps = Mathf.RoundToInt((MaxValue - MinValue) / Step); float totalWidth = totalSteps * TickSpacing; float maxX = centerOffset; float minX = -(totalWidth); contentX = Mathf.Clamp(contentX, minX, maxX); Content.anchoredPosition = new Vector2(contentX, 0); } /// /// 更新高亮效果 /// private void UpdateHighlight() { if (Content == null || ticks.Count == 0) return; float viewportWidth = Viewport != null ? Viewport.rect.width : 800f; float centerX = -contentX + viewportWidth / 2f; // 计算当前值(距离中心最近的刻度) float centerOffset = viewportWidth / 2f; float relativeX = centerX - centerOffset; int nearestIndex = Mathf.RoundToInt(relativeX / TickSpacing); nearestIndex = Mathf.Clamp(nearestIndex, 0, ticks.Count - 1); float newValue = ticks[nearestIndex].Value; if (Mathf.Abs(newValue - currentValue) > 0.001f) { currentValue = newValue; OnValueChanged?.Invoke(currentValue); } // 更新每个刻度的视觉效果 for (int i = 0; i < ticks.Count; i++) { var tick = ticks[i]; float tickX = centerOffset + i * TickSpacing; bool isSelected = (i == nearestIndex); // 刻度颜色 if (tick.TickImage != null) { if (isSelected) { tick.TickImage.color = SelectedTickColor; } else { tick.TickImage.color = tick.IsMajor ? MajorTickColor : MinorTickColor; } } // 数字样式(只有主刻度有数字) if (tick.NumberText != null) { if (isSelected) { tick.NumberText.color = SelectedNumberColor; tick.NumberRect.localScale = Vector3.one * SelectedScale; } else { tick.NumberText.color = NumberColor; tick.NumberRect.localScale = Vector3.one * NormalScale; } } } } /// /// 吸附到最近的值 /// private void SnapToNearest() { if (snapCoroutine != null) { StopCoroutine(snapCoroutine); } snapCoroutine = StartCoroutine(SnapCoroutine()); } private IEnumerator SnapCoroutine() { float viewportWidth = Viewport != null ? Viewport.rect.width : 800f; float centerOffset = viewportWidth / 2f; // 计算目标位置 int currentIndex = Mathf.RoundToInt((currentValue - MinValue) / Step); float targetX = -(currentIndex * TickSpacing); float startX = contentX; float elapsed = 0f; while (elapsed < SnapDuration) { elapsed += Time.deltaTime; float t = elapsed / SnapDuration; t = Mathf.SmoothStep(0, 1, t); contentX = Mathf.Lerp(startX, targetX, t); ApplyContentPosition(); UpdateHighlight(); yield return null; } contentX = targetX; ApplyContentPosition(); UpdateHighlight(); OnValueSelected?.Invoke(currentValue); snapCoroutine = null; } /// /// 设置值 /// public void SetValue(float value, bool animate = true) { value = Mathf.Clamp(value, MinValue, MaxValue); // 对齐到步长 value = MinValue + Mathf.Round((value - MinValue) / Step) * Step; currentValue = value; float viewportWidth = Viewport != null ? Viewport.rect.width : 800f; float centerOffset = viewportWidth / 2f; int currentIndex = Mathf.RoundToInt((currentValue - MinValue) / Step); float targetX = -(currentIndex * TickSpacing); if (animate && snapCoroutine != null) { StopCoroutine(snapCoroutine); } if (animate) { snapCoroutine = StartCoroutine(SnapToValueCoroutine(targetX)); } else { contentX = targetX; ApplyContentPosition(); UpdateHighlight(); } } private IEnumerator SnapToValueCoroutine(float targetX) { float startX = contentX; float elapsed = 0f; while (elapsed < SnapDuration) { elapsed += Time.deltaTime; float t = elapsed / SnapDuration; t = Mathf.SmoothStep(0, 1, t); contentX = Mathf.Lerp(startX, targetX, t); ApplyContentPosition(); UpdateHighlight(); yield return null; } contentX = targetX; ApplyContentPosition(); UpdateHighlight(); OnValueSelected?.Invoke(currentValue); snapCoroutine = null; } /// /// 获取当前值 /// public float GetValue() { return currentValue; } /// /// 重新初始化(用于运行时修改参数) /// public void Refresh() { Initialize(); } } }