DualSlider

# DualSlider
範囲を指定出来るスライダーです。
こういうスライダーの正式名称なんて言うんでしょうね？
RangeSliderでググったら出てきましたが、名前の響きがデュアルの方がカッコいいのでこっちにしました。
実践投入してないので、ちゃんと動作するか試してないです。

![スクリーンショット 2019-01-30 18.53.48.png](https://qiita-image-store.s3.amazonaws.com/0/141994/ff5c0f74-f714-871b-a882-2f2e94f23764.png)

サンプルプロジェクト: https://bitbucket.org/hyperkuz/dualslider/src/master/

## コード
```csharp: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; }

		// ２つの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;
	}
}
```

# 参考(というかほぼ移植)
https://bitbucket.org/Unity-Technologies/ui/src/0651862509331da4e85f519de88c99d0529493a5/UnityEngine.UI/UI/Core/Slider.cs