killapp/Assets/Scripts/UI/Components/SlideSelector.cs

467 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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