2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DualSlider

Last updated at Posted at 2019-01-30

DualSlider

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

スクリーンショット 2019-01-30 18.53.48.png

サンプルプロジェクト: 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;
	}
}

参考(というかほぼ移植)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?