DualSlider
範囲を指定出来るスライダーです。
こういうスライダーの正式名称なんて言うんでしょうね?
RangeSliderでググったら出てきましたが、名前の響きがデュアルの方がカッコいいのでこっちにしました。
実践投入してないので、ちゃんと動作するか試してないです。
サンプルプロジェクト: https://bitbucket.org/hyperkuz/dualslider/src/master/
コード
DualSlider.cs
using System;
using System.Diagnostics;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;
public sealed class DualSlider : Selectable, IDragHandler, IInitializePotentialDragHandler
{
// ----------------------------------------------------------------
// MonoBehaviour
// ----------------------------------------------------------------
protected override void Awake()
{
base.Awake();
UpdateVisual();
// nullチェック
if ((m_RangeFillRect != null && m_RangeFillRect.gameObject) &&
(m_RightHandleContent != null && m_RightHandleContent.m_HandleContainer != null && m_RightHandleContent.m_HandleGraphic != null) &&
(m_LeftHandleContent != null && m_LeftHandleContent.m_HandleContainer != null && m_LeftHandleContent.m_HandleGraphic != null))
{
// GameObjectとGraphicの関連付け
m_SelectableTargetGraphicDict = new Dictionary<GameObject, Graphic>(3);
m_SelectableTargetGraphicDict.Add(m_RangeFillRect.gameObject, m_RangeFillRect.GetComponent<Graphic>());
m_SelectableTargetGraphicDict.Add(m_RightHandleContent.m_HandleGraphic.gameObject, m_RightHandleContent.m_HandleGraphic);
m_SelectableTargetGraphicDict.Add(m_LeftHandleContent.m_HandleGraphic.gameObject, m_LeftHandleContent.m_HandleGraphic);
if (onRangeValueChanged != null)
{
// イベント発火
onRangeValueChanged.Invoke(m_RangeValue);
}
}
else
{
UnityEngine.Debug.LogAssertion("Property Invalid");
}
}
protected override void OnDestroy()
{
base.OnDestroy();
if( m_SelectableTargetGraphicDict != null )
{
m_SelectableTargetGraphicDict.Clear();
m_SelectableTargetGraphicDict = null;
}
}
#if UNITY_EDITOR
protected override void OnValidate()
{
UpdateVisual();
UpdateRangeValue();
}
#endif
// ----------------------------------------------------------------
// Implement Interface
// ----------------------------------------------------------------
public void OnInitializePotentialDrag(PointerEventData eventData)
{
// ドラッグ初動時にスライダーが滑り初めて欲しいので、ドラッグ閾値を無視する
eventData.useDragThreshold = false;
}
public override void OnPointerDown(PointerEventData eventData)
{
base.OnPointerDown(eventData);
LogFormat("OnPointerDown eventData.pointerEnter:{0}", eventData.pointerEnter);
m_Offset = Vector2.zero;
RectTransform targetRect = null;
Vector2 localMousePos;
m_CurrentClickRectType = GetClickRectType(eventData);
switch (m_CurrentClickRectType)
{
case ClickRectType.LeftHandle:
targetRect = m_LeftHandleContent.m_HandleContainer;
break;
case ClickRectType.RightHandle:
targetRect = m_RightHandleContent.m_HandleContainer;
break;
case ClickRectType.RangeFill:
targetRect = m_RangeFillColiderRect;
break;
case ClickRectType.Other:
break;
default:
m_CurrentClickRectType = ClickRectType.None;
return;
}
// スライド出来るRectの範囲内のScreenPointなら
if (targetRect != null && RectTransformUtility.ScreenPointToLocalPointInRectangle(targetRect, eventData.position, eventData.pressEventCamera, out localMousePos))
{
// クリック位置をキャッシュ
m_Offset = localMousePos;
}
// UIの色を押された時の色に変える
UpdateGraphicColor(eventData.pointerEnter, colors.pressedColor);
}
public void OnDrag(PointerEventData eventData)
{
Log("OnDrag");
/// Normalizeされた値を計算
var cam = eventData.pressEventCamera;
var normalizedValue = GetScreenPoint2NormalizedValue(cam, m_HandleSliderAreaRect, eventData.position);
// 左ハンドル始動
if (m_CurrentClickRectType == ClickRectType.LeftHandle)
{
leftNormalizeValue = Mathf.Clamp(normalizedValue, 0.0f, rightNormalizeValue);
}
// 右ハンドル始動
else if (m_CurrentClickRectType == ClickRectType.RightHandle)
{
rightNormalizeValue = Mathf.Clamp(normalizedValue, leftNormalizeValue, 1.0f);
}
// 範囲始動
else if (m_CurrentClickRectType == ClickRectType.RangeFill)
{
var deltaMid = normalizedValue - midNormalizedValue;
// 右側にスライドしてるなら
if (Mathf.Sign(deltaMid) < 0.0f)
{
var newLeftNormalizedValue = Mathf.Clamp(leftNormalizeValue + deltaMid, 0.0f, rightNormalizeValue);
var leftDeltaValue = newLeftNormalizedValue - leftNormalizeValue;
leftNormalizeValue = newLeftNormalizedValue;
rightNormalizeValue += leftDeltaValue;
}
// 左側にスライドしてるなら
else
{
var newRightNormalizedValue = Mathf.Clamp(rightNormalizeValue + deltaMid, leftNormalizeValue, 1.0f);
var rightDeltaValue = newRightNormalizedValue - rightNormalizeValue;
rightNormalizeValue = newRightNormalizedValue;
leftNormalizeValue += rightDeltaValue;
}
}
// 範囲の値更新
UpdateRangeValue();
// UIの更新
UpdateVisual();
}
public override void OnMove(AxisEventData eventData)
{
base.OnMove(eventData);
Log("OnMove!");
}
public override void OnPointerUp(PointerEventData eventData)
{
m_CurrentClickRectType = ClickRectType.None;
if (eventData.button != PointerEventData.InputButton.Left) { return; }
if (eventData.rawPointerPress == null) { return; }
LogFormat("OnPointerUp eventData.rawPointerPress:{0}", eventData.rawPointerPress);
// UIの色を戻す
UpdateGraphicColor(eventData.rawPointerPress, colors.normalColor);
}
// ----------------------------------------------------------------
// Method
// ----------------------------------------------------------------
/// <summary>
/// EventDataからどのRectがクリックされてるかを判定して返す
/// </summary>
/// <param name="eventData"></param>
/// <returns></returns>
private ClickRectType GetClickRectType(PointerEventData eventData)
{
if (RectTransformUtility.RectangleContainsScreenPoint(m_LeftHandleContent.m_HandleGraphic.rectTransform, eventData.position, eventData.enterEventCamera))
{
return ClickRectType.LeftHandle;
}
else if (RectTransformUtility.RectangleContainsScreenPoint(m_RightHandleContent.m_HandleGraphic.rectTransform, eventData.position, eventData.enterEventCamera))
{
return ClickRectType.RightHandle;
}
else if (RectTransformUtility.RectangleContainsScreenPoint(m_RangeFillColiderRect, eventData.position, eventData.enterEventCamera))
{
return ClickRectType.RangeFill;
}
return ClickRectType.Other;
}
/// <summary>
/// スクリーンポイントからRect上のx方向の値を0~1で取得する
/// </summary>
/// <param name="cam"></param>
/// <param name="clickRect"></param>
/// <param name="pos"></param>
/// <returns></returns>
private float GetScreenPoint2NormalizedValue(Camera cam, RectTransform clickRect, Vector2 pos)
{
if (clickRect.rect.size.x > 0)
{
Vector2 localCursor;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, pos, cam, out localCursor))
{
return 0.0f;
}
localCursor -= clickRect.rect.position;
return Mathf.Clamp01((localCursor.x - m_Offset.x) / clickRect.rect.size.x);
}
return 0.0f;
}
/// <summary>
/// Graphicの色を更新する
/// </summary>
/// <param name="targetGameObject"></param>
/// <param name="color"></param>
private void UpdateGraphicColor(GameObject targetGameObject, Color color)
{
if (targetGameObject == null) { return; }
// 2つのSliderの狭間のFillRectだったら
if (targetGameObject == m_RangeFillColiderRect.gameObject)
{
// 2つのSliderとFillRectの色をまとめて変化させる
foreach (var graphic in m_SelectableTargetGraphicDict.Values)
{
StartColorTween(graphic, color, false);
}
}
else
{
// 個別に変化
if (m_SelectableTargetGraphicDict.ContainsKey(targetGameObject))
{
StartColorTween(m_SelectableTargetGraphicDict[targetGameObject], color, false);
}
}
}
/// <summary>
/// NormalizedValueに応じてUIを更新する
/// /// </summary>
private void UpdateVisual()
{
var anchorMin = Vector2.zero;
var anchorMax = Vector2.one;
// 左ハンドルの更新
anchorMin.x = anchorMax.x = leftNormalizeValue;
m_LeftHandleContent.m_HandleContainer.anchorMin = anchorMin;
m_LeftHandleContent.m_HandleContainer.anchorMax = anchorMax;
// 右ハンドルの更新
anchorMin.x = anchorMax.x = rightNormalizeValue;
m_RightHandleContent.m_HandleContainer.anchorMin = anchorMin;
m_RightHandleContent.m_HandleContainer.anchorMax = anchorMax;
// FillRangeRectのoffset
var normalizedOffset = (m_RightHandleContent.m_HandleGraphic.rectTransform.rect.size.x / 2.0f) / m_HandleSliderAreaRect.rect.size.x;
// FillRangeRectの更新
anchorMin.x = leftNormalizeValue - normalizedOffset;
anchorMax.x = rightNormalizeValue + normalizedOffset;
m_RangeFillRect.anchorMin = anchorMin;
m_RangeFillRect.anchorMax = anchorMax;
}
/// <summary>
/// 範囲の数値計算
/// </summary>
private void UpdateRangeValue()
{
var leftLerpValue = Mathf.Lerp(m_TargetRangeValue.Min, m_TargetRangeValue.Max, leftNormalizeValue);
var rightLerpValue = Mathf.Lerp(m_TargetRangeValue.Min, m_TargetRangeValue.Max, rightNormalizeValue);
var min = m_WholeNumbers ? Mathf.Round(leftLerpValue) : leftLerpValue;
var max = m_WholeNumbers ? Mathf.Round(rightLerpValue) : rightLerpValue;
m_RangeValue.Min = min;
m_RangeValue.Max = max;
if (onRangeValueChanged != null)
{
// イベント発火
onRangeValueChanged.Invoke(m_RangeValue);
}
}
/// <summary>
/// Graphicの色を変える
/// </summary>
/// <param name="targetGraphic"></param>
/// <param name="targetColor"></param>
/// <param name="instant"></param>
private void StartColorTween(Graphic targetGraphic, Color targetColor, bool instant)
{
targetGraphic.CrossFadeColor(targetColor, instant ? 0f : 0.1f, true, true);
}
[Conditional("DUAL_SLIDER_DEBUG")]
private void Log(string log)
{
UnityEngine.Debug.Log(log);
}
[Conditional("DUAL_SLIDER_DEBUG")]
private void LogFormat(string format, params object[] args)
{
UnityEngine.Debug.LogFormat(format, args);
}
// ----------------------------------------------------------------
// Field
// ----------------------------------------------------------------
[SerializeField]
private RectTransform m_RangeFillRect;
[SerializeField]
private RectTransform m_RangeFillColiderRect;
[SerializeField]
private RectTransform m_HandleSliderAreaRect;
[SerializeField]
private bool m_WholeNumbers = false;
[SerializeField]
private RangeValue m_TargetRangeValue;
[SerializeField]
private RangeValue m_RangeValue;
[SerializeField]
private Handle m_RightHandleContent;
[SerializeField]
private Handle m_LeftHandleContent;
[SerializeField]
private ClickRectType m_CurrentClickRectType = ClickRectType.None;
public DualSliderEvent onRangeValueChanged;
private Dictionary<GameObject, Graphic> m_SelectableTargetGraphicDict;
private Vector2 m_Offset;
// ----------------------------------------------------------------
// Property
// ----------------------------------------------------------------
private float leftNormalizeValue
{
get
{
if (Mathf.Approximately(m_TargetRangeValue.Min, m_TargetRangeValue.Max))
return 0;
return Mathf.InverseLerp(m_TargetRangeValue.Min, m_TargetRangeValue.Max, m_RangeValue.Min);
}
set
{
m_RangeValue.Min = Mathf.Lerp(m_TargetRangeValue.Min, m_TargetRangeValue.Max, value);
}
}
private float rightNormalizeValue
{
get
{
if (Mathf.Approximately(m_TargetRangeValue.Min, m_TargetRangeValue.Max))
return 0;
return Mathf.InverseLerp(m_TargetRangeValue.Min, m_TargetRangeValue.Max, m_RangeValue.Max);
}
set
{
m_RangeValue.Max = Mathf.Lerp(m_TargetRangeValue.Min, m_TargetRangeValue.Max, value);
}
}
private float midNormalizedValue
{
get
{
return Mathf.Lerp(leftNormalizeValue, rightNormalizeValue, 0.5f);
}
}
// ----------------------------------------------------------------
// Enum
// ----------------------------------------------------------------
private enum ClickRectType
{
LeftHandle,
RightHandle,
RangeFill,
Other,
None,
}
// ----------------------------------------------------------------
// Class
// ----------------------------------------------------------------
[Serializable]
public class DualSliderEvent : UnityEvent<RangeValue> { }
[System.Serializable]
public class Handle
{
public RectTransform m_HandleContainer;
public Graphic m_HandleGraphic;
}
// ----------------------------------------------------------------
// Struct
// ----------------------------------------------------------------
[System.Serializable]
public struct RangeValue
{
public float Min
{
get
{
return m_Min;
}
set
{
m_Min = value;
}
}
public float Max
{
get
{
return m_Max;
}
set
{
m_Max = value;
}
}
[SerializeField]
private float m_Min;
[SerializeField]
private float m_Max;
}
}
参考(というかほぼ移植)