467 lines
16 KiB
C#
467 lines
16 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.EventSystems;
|
||
|
||
namespace Kill.UI.Components
|
||
{
|
||
/// <summary>
|
||
/// 滑动选择器组件
|
||
/// 支持:主次刻度 + 只在主刻度显示数字 + 小数精度
|
||
/// </summary>
|
||
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<float> OnValueChanged; // 值改变事件
|
||
public event Action<float> 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<TickData> ticks = new List<TickData>();
|
||
|
||
|
||
/// <summary>
|
||
/// 初始化选择器
|
||
/// </summary>
|
||
public void Initialize()
|
||
{
|
||
ClearContent();
|
||
CreateTicks();
|
||
SetValue(DefaultValue, false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空内容
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建刻度
|
||
/// </summary>
|
||
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<RectTransform>();
|
||
tick.TickImage = tickObj.AddComponent<Image>();
|
||
|
||
// 底部对齐: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<RectTransform>();
|
||
tick.NumberText = numObj.AddComponent<Text>();
|
||
|
||
// 数字也底部对齐
|
||
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<Font>("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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用内容位置
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新高亮效果
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 吸附到最近的值
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置值
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前值
|
||
/// </summary>
|
||
public float GetValue()
|
||
{
|
||
return currentValue;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重新初始化(用于运行时修改参数)
|
||
/// </summary>
|
||
public void Refresh()
|
||
{
|
||
Initialize();
|
||
}
|
||
}
|
||
}
|