2
0

RangeSlider.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. /// Credit Ben MacKinnon @Dover8
  2. /// Sourced from - https://github.com/Dover8/Unity-UI-Extensions/tree/range-slider
  3. /// Usage: Extension of the standard slider. Two handles determine a low and high value between a Min and Max.
  4. /// Raises a UnityEvent passing the low and high values
  5. using System;
  6. using UnityEngine.Events;
  7. using UnityEngine.EventSystems;
  8. namespace UnityEngine.UI.Extensions
  9. {
  10. [AddComponentMenu("UI/Extensions/Range Slider", 34)]
  11. [ExecuteInEditMode]
  12. [RequireComponent(typeof(RectTransform))]
  13. public class RangeSlider : Selectable, IDragHandler, IInitializePotentialDragHandler, ICanvasElement
  14. {
  15. [Serializable]
  16. public class RangeSliderEvent : UnityEvent<float, float> { }
  17. [SerializeField]
  18. private RectTransform m_FillRect;
  19. public RectTransform FillRect { get { return m_FillRect; } set { if (SetClass(ref m_FillRect, value)) { UpdateCachedReferences(); UpdateVisuals(); } } }
  20. [SerializeField]
  21. private RectTransform m_LowHandleRect;
  22. public RectTransform LowHandleRect { get { return m_LowHandleRect; } set { if (SetClass(ref m_LowHandleRect, value)) { UpdateCachedReferences(); UpdateVisuals(); } } }
  23. [SerializeField]
  24. private RectTransform m_HighHandleRect;
  25. public RectTransform HighHandleRect { get { return m_HighHandleRect; } set { if (SetClass(ref m_HighHandleRect, value)) { UpdateCachedReferences(); UpdateVisuals(); } } }
  26. [Space]
  27. [SerializeField]
  28. private float m_MinValue = 0;
  29. public float MinValue { get { return m_MinValue; } set { if (SetStruct(ref m_MinValue, value)) { SetLow(m_LowValue); SetHigh(m_HighValue); UpdateVisuals(); } } }
  30. [SerializeField]
  31. private float m_MaxValue = 1;
  32. public float MaxValue { get { return m_MaxValue; } set { if (SetStruct(ref m_MaxValue, value)) { SetLow(m_LowValue); SetHigh(m_HighValue); UpdateVisuals(); } } }
  33. [SerializeField]
  34. private bool m_WholeNumbers = false;
  35. public bool WholeNumbers { get { return m_WholeNumbers; } set { if (SetStruct(ref m_WholeNumbers, value)) { SetLow(m_LowValue); SetHigh(m_HighValue); UpdateVisuals(); } } }
  36. [SerializeField]
  37. private float m_LowValue;
  38. public virtual float LowValue
  39. {
  40. get
  41. {
  42. if (WholeNumbers)
  43. {
  44. return Mathf.Round(m_LowValue);
  45. }
  46. return m_LowValue;
  47. }
  48. set
  49. {
  50. SetLow(value);
  51. }
  52. }
  53. public float NormalizedLowValue
  54. {
  55. get
  56. {
  57. if (Mathf.Approximately(MinValue, MaxValue))
  58. {
  59. return 0;
  60. }
  61. return Mathf.InverseLerp(MinValue, MaxValue, LowValue);
  62. }
  63. set
  64. {
  65. this.LowValue = Mathf.Lerp(MinValue, MaxValue, value);
  66. }
  67. }
  68. [SerializeField]
  69. private float m_HighValue;
  70. public virtual float HighValue
  71. {
  72. get
  73. {
  74. if (WholeNumbers)
  75. {
  76. return Mathf.Round(m_HighValue);
  77. }
  78. return m_HighValue;
  79. }
  80. set
  81. {
  82. SetHigh(value);
  83. }
  84. }
  85. public float NormalizedHighValue
  86. {
  87. get
  88. {
  89. if (Mathf.Approximately(MinValue, MaxValue))
  90. {
  91. return 0;
  92. }
  93. return Mathf.InverseLerp(MinValue, MaxValue, HighValue);
  94. }
  95. set
  96. {
  97. this.HighValue = Mathf.Lerp(MinValue, MaxValue, value);
  98. }
  99. }
  100. /// <summary>
  101. /// Set the value of the slider without invoking onValueChanged callback.
  102. /// </summary>
  103. /// <param name="input">The new value for the slider.</param>
  104. public virtual void SetValueWithoutNotify(float low, float high)
  105. {
  106. SetLow(low, false);
  107. SetHigh(high, false);
  108. }
  109. [Space]
  110. [SerializeField]
  111. private RangeSliderEvent m_OnValueChanged = new RangeSliderEvent();
  112. public RangeSliderEvent OnValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
  113. // Private fields
  114. /// <summary>
  115. /// An Enum that says in what state we and interacting with the slider
  116. /// </summary>
  117. private enum InteractionState
  118. {
  119. Low,
  120. High,
  121. Bar,
  122. None
  123. }
  124. private InteractionState interactionState = InteractionState.None;
  125. private Image m_FillImage;
  126. private Transform m_FillTransform;
  127. private RectTransform m_FillContainerRect;
  128. private Transform m_HighHandleTransform;
  129. private RectTransform m_HighHandleContainerRect;
  130. private Transform m_LowHandleTransform;
  131. private RectTransform m_LowHandleContainerRect;
  132. // The offset from handle position to mouse down position
  133. private Vector2 m_LowOffset = Vector2.zero;
  134. // The offset from handle position to mouse down position
  135. private Vector2 m_HighOffset = Vector2.zero;
  136. private DrivenRectTransformTracker m_Tracker;
  137. // This "delayed" mechanism is required for case 1037681.
  138. private bool m_DelayedUpdateVisuals = false;
  139. // Size of each step.
  140. float StepSize { get { return WholeNumbers ? 1 : (MaxValue - MinValue) * 0.1f; } }
  141. protected RangeSlider()
  142. { }
  143. #if UNITY_EDITOR
  144. protected override void OnValidate()
  145. {
  146. base.OnValidate();
  147. if (WholeNumbers)
  148. {
  149. m_MinValue = Mathf.Round(m_MinValue);
  150. m_MaxValue = Mathf.Round(m_MaxValue);
  151. }
  152. if (IsActive())
  153. {
  154. UpdateCachedReferences();
  155. SetLow(m_LowValue, false);
  156. SetHigh(m_HighValue, false);
  157. //Update rects since other things might affect them even if value didn't change
  158. m_DelayedUpdateVisuals = true;
  159. }
  160. if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
  161. {
  162. CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
  163. }
  164. }
  165. #endif
  166. public virtual void Rebuild(CanvasUpdate executing)
  167. {
  168. #if UNITY_EDITOR
  169. if (executing == CanvasUpdate.Prelayout)
  170. {
  171. OnValueChanged.Invoke(LowValue, HighValue);
  172. }
  173. #endif
  174. }
  175. /// <summary>
  176. /// See ICanvasElement.LayoutComplete
  177. /// </summary>
  178. public virtual void LayoutComplete()
  179. { }
  180. /// <summary>
  181. /// See ICanvasElement.GraphicUpdateComplete
  182. /// </summary>
  183. public virtual void GraphicUpdateComplete()
  184. { }
  185. public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
  186. {
  187. if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
  188. return false;
  189. currentValue = newValue;
  190. return true;
  191. }
  192. public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
  193. {
  194. if (currentValue.Equals(newValue))
  195. return false;
  196. currentValue = newValue;
  197. return true;
  198. }
  199. protected override void OnEnable()
  200. {
  201. base.OnEnable();
  202. UpdateCachedReferences();
  203. SetLow(LowValue, false);
  204. SetHigh(HighValue, false);
  205. // Update rects since they need to be initialized correctly.
  206. UpdateVisuals();
  207. }
  208. protected override void OnDisable()
  209. {
  210. m_Tracker.Clear();
  211. base.OnDisable();
  212. }
  213. /// <summary>
  214. /// Update the rect based on the delayed update visuals.
  215. /// Got around issue of calling sendMessage from onValidate.
  216. /// </summary>
  217. protected virtual void Update()
  218. {
  219. if (m_DelayedUpdateVisuals)
  220. {
  221. m_DelayedUpdateVisuals = false;
  222. UpdateVisuals();
  223. }
  224. }
  225. protected override void OnDidApplyAnimationProperties()
  226. {
  227. base.OnDidApplyAnimationProperties();
  228. }
  229. void UpdateCachedReferences()
  230. {
  231. if (m_FillRect && m_FillRect != (RectTransform)transform)
  232. {
  233. m_FillTransform = m_FillRect.transform;
  234. m_FillImage = m_FillRect.GetComponent<Image>();
  235. if (m_FillTransform.parent != null)
  236. m_FillContainerRect = m_FillTransform.parent.GetComponent<RectTransform>();
  237. }
  238. else
  239. {
  240. m_FillRect = null;
  241. m_FillContainerRect = null;
  242. m_FillImage = null;
  243. }
  244. if (m_HighHandleRect && m_HighHandleRect != (RectTransform)transform)
  245. {
  246. m_HighHandleTransform = m_HighHandleRect.transform;
  247. if (m_HighHandleTransform.parent != null)
  248. m_HighHandleContainerRect = m_HighHandleTransform.parent.GetComponent<RectTransform>();
  249. }
  250. else
  251. {
  252. m_HighHandleRect = null;
  253. m_HighHandleContainerRect = null;
  254. }
  255. if (m_LowHandleRect && m_LowHandleRect != (RectTransform)transform)
  256. {
  257. m_LowHandleTransform = m_LowHandleRect.transform;
  258. if (m_LowHandleTransform.parent != null)
  259. {
  260. m_LowHandleContainerRect = m_LowHandleTransform.parent.GetComponent<RectTransform>();
  261. }
  262. }
  263. else
  264. {
  265. m_LowHandleRect = null;
  266. m_LowHandleContainerRect = null;
  267. }
  268. }
  269. void SetLow(float input)
  270. {
  271. SetLow(input, true);
  272. }
  273. protected virtual void SetLow(float input, bool sendCallback)
  274. {
  275. // Clamp the input
  276. float newValue = Mathf.Clamp(input, MinValue, HighValue); //clamp between min and High
  277. if (WholeNumbers)
  278. {
  279. newValue = Mathf.Round(newValue);
  280. }
  281. // If the stepped value doesn't match the last one, it's time to update
  282. if (m_LowValue == newValue)
  283. return;
  284. m_LowValue = newValue;
  285. UpdateVisuals();
  286. if (sendCallback)
  287. {
  288. UISystemProfilerApi.AddMarker("RangeSlider.lowValue", this);
  289. m_OnValueChanged.Invoke(newValue, HighValue);
  290. }
  291. }
  292. void SetHigh(float input)
  293. {
  294. SetHigh(input, true);
  295. }
  296. protected virtual void SetHigh(float input, bool sendCallback)
  297. {
  298. // Clamp the input
  299. float newValue = Mathf.Clamp(input, LowValue, MaxValue); //clamp between min and High
  300. if (WholeNumbers)
  301. {
  302. newValue = Mathf.Round(newValue);
  303. }
  304. // If the stepped value doesn't match the last one, it's time to update
  305. if (m_HighValue == newValue)
  306. return;
  307. m_HighValue = newValue;
  308. UpdateVisuals();
  309. if (sendCallback)
  310. {
  311. UISystemProfilerApi.AddMarker("RangeSlider.highValue", this);
  312. m_OnValueChanged.Invoke(LowValue, newValue);
  313. }
  314. }
  315. protected override void OnRectTransformDimensionsChange()
  316. {
  317. base.OnRectTransformDimensionsChange();
  318. //This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called.
  319. if (!IsActive())
  320. return;
  321. UpdateVisuals();
  322. }
  323. // Force-update the slider. Useful if you've changed the properties and want it to update visually.
  324. private void UpdateVisuals()
  325. {
  326. #if UNITY_EDITOR
  327. if (!Application.isPlaying)
  328. UpdateCachedReferences();
  329. #endif
  330. m_Tracker.Clear();
  331. if (m_FillContainerRect != null)
  332. {
  333. m_Tracker.Add(this, m_FillRect, DrivenTransformProperties.Anchors);
  334. Vector2 anchorMin = Vector2.zero;
  335. Vector2 anchorMax = Vector2.one;
  336. //this is where some new magic must happen. Slider just uses a filled image
  337. //and changes the % of fill. We must move the image anchors to be between the two handles.
  338. anchorMin[0] = NormalizedLowValue;
  339. anchorMax[0] = NormalizedHighValue;
  340. m_FillRect.anchorMin = anchorMin;
  341. m_FillRect.anchorMax = anchorMax;
  342. }
  343. if (m_LowHandleContainerRect != null)
  344. {
  345. m_Tracker.Add(this, m_LowHandleRect, DrivenTransformProperties.Anchors);
  346. Vector2 anchorMin = Vector2.zero;
  347. Vector2 anchorMax = Vector2.one;
  348. anchorMin[0] = anchorMax[0] = NormalizedLowValue;
  349. m_LowHandleRect.anchorMin = anchorMin;
  350. m_LowHandleRect.anchorMax = anchorMax;
  351. }
  352. if (m_HighHandleContainerRect != null)
  353. {
  354. m_Tracker.Add(this, m_HighHandleRect, DrivenTransformProperties.Anchors);
  355. Vector2 anchorMin = Vector2.zero;
  356. Vector2 anchorMax = Vector2.one;
  357. anchorMin[0] = anchorMax[0] = NormalizedHighValue;
  358. m_HighHandleRect.anchorMin = anchorMin;
  359. m_HighHandleRect.anchorMax = anchorMax;
  360. }
  361. }
  362. // Update the slider's position based on the mouse.
  363. void UpdateDrag(PointerEventData eventData, Camera cam)
  364. {
  365. //this needs to differ from slider in that we have two handles, and need to move the right one.
  366. //and if it was neither handle, we will have a separate case where both handles move uniformly
  367. //moving the entire range
  368. //this is where we use our interationState
  369. switch (interactionState)
  370. {
  371. case InteractionState.Low:
  372. NormalizedLowValue = CalculateDrag(eventData, cam, m_LowHandleContainerRect, m_LowOffset);
  373. break;
  374. case InteractionState.High:
  375. NormalizedHighValue = CalculateDrag(eventData, cam, m_HighHandleContainerRect, m_HighOffset);
  376. break;
  377. case InteractionState.Bar:
  378. //special case
  379. CalculateBarDrag(eventData, cam);
  380. break;
  381. case InteractionState.None:
  382. break;
  383. }
  384. }
  385. private float CalculateDrag(PointerEventData eventData, Camera cam, RectTransform containerRect, Vector2 offset)
  386. {
  387. RectTransform clickRect = containerRect ?? m_FillContainerRect;
  388. if (clickRect != null && clickRect.rect.size[0] > 0)
  389. {
  390. Vector2 localCursor;
  391. if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, eventData.position, cam, out localCursor))
  392. {
  393. return 0f;
  394. }
  395. localCursor -= clickRect.rect.position;
  396. float val = Mathf.Clamp01((localCursor - offset)[0] / clickRect.rect.size[0]);
  397. return val;
  398. }
  399. return 0;
  400. }
  401. private void CalculateBarDrag(PointerEventData eventData, Camera cam)
  402. {
  403. RectTransform clickRect = m_FillContainerRect;
  404. if (clickRect != null && clickRect.rect.size[0] > 0)
  405. {
  406. Vector2 localCursor;
  407. if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, eventData.position, cam, out localCursor))
  408. {
  409. return;
  410. }
  411. localCursor -= clickRect.rect.position;
  412. //now we need to get the delta drag on the bar
  413. //and move both the normalized low and high values by this amount
  414. //but also check that neither is going beyond the bounds
  415. if (NormalizedLowValue >= 0 && NormalizedHighValue <= 1)
  416. {
  417. //find the mid point on the current bar
  418. float mid = (NormalizedHighValue + NormalizedLowValue)/2;
  419. //find where the new mid point should be
  420. float val = Mathf.Clamp01((localCursor)[0] / clickRect.rect.size[0]);
  421. //calculate the delta
  422. float delta = val - mid;
  423. //check the clamp range
  424. if (NormalizedLowValue + delta < 0)
  425. {
  426. delta = -NormalizedLowValue;
  427. }
  428. else if (NormalizedHighValue + delta > 1)
  429. {
  430. delta = 1 - NormalizedHighValue;
  431. }
  432. //adjust both ends
  433. NormalizedLowValue += delta;
  434. NormalizedHighValue += delta;
  435. }
  436. }
  437. }
  438. private bool MayDrag(PointerEventData eventData)
  439. {
  440. return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left;
  441. }
  442. public override void OnPointerDown(PointerEventData eventData)
  443. {
  444. if (!MayDrag(eventData))
  445. return;
  446. //HANDLE DRAG EVENTS
  447. m_LowOffset = m_HighOffset = Vector2.zero;
  448. Vector2 localMousePos;
  449. if (m_HighHandleRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_HighHandleRect, eventData.position, eventData.enterEventCamera))
  450. {
  451. //dragging the high value handle
  452. if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HighHandleRect, eventData.position, eventData.pressEventCamera, out localMousePos))
  453. {
  454. m_HighOffset = localMousePos;
  455. }
  456. interactionState = InteractionState.High;
  457. if (transition == Transition.ColorTint)
  458. {
  459. targetGraphic = m_HighHandleRect.GetComponent<Graphic>();
  460. }
  461. }
  462. else if (m_LowHandleRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_LowHandleRect, eventData.position, eventData.enterEventCamera))
  463. {
  464. //dragging the low value handle
  465. if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_LowHandleRect, eventData.position, eventData.pressEventCamera, out localMousePos))
  466. {
  467. m_LowOffset = localMousePos;
  468. }
  469. interactionState = InteractionState.Low;
  470. if (transition == Transition.ColorTint)
  471. {
  472. targetGraphic = m_LowHandleRect.GetComponent<Graphic>();
  473. }
  474. }
  475. else
  476. {
  477. //outside the handles, move the entire slider along
  478. UpdateDrag(eventData, eventData.pressEventCamera);
  479. if (eventData.pointerCurrentRaycast.gameObject == m_FillRect.gameObject)
  480. {
  481. interactionState = InteractionState.Bar;
  482. }
  483. if (transition == Transition.ColorTint)
  484. {
  485. targetGraphic = m_FillImage;
  486. }
  487. }
  488. base.OnPointerDown(eventData);
  489. }
  490. public virtual void OnDrag(PointerEventData eventData)
  491. {
  492. if (!MayDrag(eventData))
  493. {
  494. return;
  495. }
  496. UpdateDrag(eventData, eventData.pressEventCamera);
  497. }
  498. public override void OnPointerUp(PointerEventData eventData)
  499. {
  500. base.OnPointerUp(eventData);
  501. interactionState = InteractionState.None;
  502. }
  503. public override void OnMove(AxisEventData eventData)
  504. {
  505. //this requires further investigation
  506. }
  507. public virtual void OnInitializePotentialDrag(PointerEventData eventData)
  508. {
  509. eventData.useDragThreshold = false;
  510. }
  511. }
  512. }