You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

653 lines
23 KiB

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using UnityEngine.UI;
  6. using UnityEngine.Events;
  7. using UnityEngine.EventSystems;
  8. using UnityEngine.UI.CoroutineTween;
  9. namespace TMPro
  10. {
  11. [AddComponentMenu("UI/TMP Dropdown", 35)]
  12. [RequireComponent(typeof(RectTransform))]
  13. public class TMP_Dropdown : Selectable, IPointerClickHandler, ISubmitHandler, ICancelHandler
  14. {
  15. protected internal class DropdownItem : MonoBehaviour, IPointerEnterHandler, ICancelHandler
  16. {
  17. [SerializeField]
  18. private TMP_Text m_Text;
  19. [SerializeField]
  20. private Image m_Image;
  21. [SerializeField]
  22. private RectTransform m_RectTransform;
  23. [SerializeField]
  24. private Toggle m_Toggle;
  25. public TMP_Text text { get { return m_Text; } set { m_Text = value; } }
  26. public Image image { get { return m_Image; } set { m_Image = value; } }
  27. public RectTransform rectTransform { get { return m_RectTransform; } set { m_RectTransform = value; } }
  28. public Toggle toggle { get { return m_Toggle; } set { m_Toggle = value; } }
  29. public virtual void OnPointerEnter(PointerEventData eventData)
  30. {
  31. EventSystem.current.SetSelectedGameObject(gameObject);
  32. }
  33. public virtual void OnCancel(BaseEventData eventData)
  34. {
  35. TMP_Dropdown dropdown = GetComponentInParent<TMP_Dropdown>();
  36. if (dropdown)
  37. dropdown.Hide();
  38. }
  39. }
  40. [Serializable]
  41. public class OptionData
  42. {
  43. [SerializeField]
  44. private string m_Text;
  45. [SerializeField]
  46. private Sprite m_Image;
  47. public string text { get { return m_Text; } set { m_Text = value; } }
  48. public Sprite image { get { return m_Image; } set { m_Image = value; } }
  49. public OptionData()
  50. {
  51. }
  52. public OptionData(string text)
  53. {
  54. this.text = text;
  55. }
  56. public OptionData(Sprite image)
  57. {
  58. this.image = image;
  59. }
  60. public OptionData(string text, Sprite image)
  61. {
  62. this.text = text;
  63. this.image = image;
  64. }
  65. }
  66. [Serializable]
  67. public class OptionDataList
  68. {
  69. [SerializeField]
  70. private List<OptionData> m_Options;
  71. public List<OptionData> options { get { return m_Options; } set { m_Options = value; } }
  72. public OptionDataList()
  73. {
  74. options = new List<OptionData>();
  75. }
  76. }
  77. [Serializable]
  78. public class DropdownEvent : UnityEvent<int> { }
  79. // Template used to create the dropdown.
  80. [SerializeField]
  81. private RectTransform m_Template;
  82. public RectTransform template { get { return m_Template; } set { m_Template = value; RefreshShownValue(); } }
  83. // Text to be used as a caption for the current value. It's not required, but it's kept here for convenience.
  84. [SerializeField]
  85. private TMP_Text m_CaptionText;
  86. public TMP_Text captionText { get { return m_CaptionText; } set { m_CaptionText = value; RefreshShownValue(); } }
  87. [SerializeField]
  88. private Image m_CaptionImage;
  89. public Image captionImage { get { return m_CaptionImage; } set { m_CaptionImage = value; RefreshShownValue(); } }
  90. [Space]
  91. [SerializeField]
  92. private TMP_Text m_ItemText;
  93. public TMP_Text itemText { get { return m_ItemText; } set { m_ItemText = value; RefreshShownValue(); } }
  94. [SerializeField]
  95. private Image m_ItemImage;
  96. public Image itemImage { get { return m_ItemImage; } set { m_ItemImage = value; RefreshShownValue(); } }
  97. [Space]
  98. [SerializeField]
  99. private int m_Value;
  100. [Space]
  101. // Items that will be visible when the dropdown is shown.
  102. // We box this into its own class so we can use a Property Drawer for it.
  103. [SerializeField]
  104. private OptionDataList m_Options = new OptionDataList();
  105. public List<OptionData> options
  106. {
  107. get { return m_Options.options; }
  108. set { m_Options.options = value; RefreshShownValue(); }
  109. }
  110. [Space]
  111. // Notification triggered when the dropdown changes.
  112. [SerializeField]
  113. private DropdownEvent m_OnValueChanged = new DropdownEvent();
  114. public DropdownEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
  115. private GameObject m_Dropdown;
  116. private GameObject m_Blocker;
  117. private List<DropdownItem> m_Items = new List<DropdownItem>();
  118. private TweenRunner<FloatTween> m_AlphaTweenRunner;
  119. private bool validTemplate = false;
  120. private static OptionData s_NoOptionData = new OptionData();
  121. // Current value.
  122. public int value
  123. {
  124. get
  125. {
  126. return m_Value;
  127. }
  128. set
  129. {
  130. if (Application.isPlaying && (value == m_Value || options.Count == 0))
  131. return;
  132. m_Value = Mathf.Clamp(value, 0, options.Count - 1);
  133. RefreshShownValue();
  134. // Notify all listeners
  135. m_OnValueChanged.Invoke(m_Value);
  136. }
  137. }
  138. public bool IsExpanded
  139. { get { return m_Dropdown != null; } }
  140. protected TMP_Dropdown()
  141. { }
  142. protected override void Awake()
  143. {
  144. #if UNITY_EDITOR
  145. if (!Application.isPlaying)
  146. return;
  147. #endif
  148. m_AlphaTweenRunner = new TweenRunner<FloatTween>();
  149. m_AlphaTweenRunner.Init(this);
  150. if (m_CaptionImage)
  151. m_CaptionImage.enabled = (m_CaptionImage.sprite != null);
  152. if (m_Template)
  153. m_Template.gameObject.SetActive(false);
  154. }
  155. #if UNITY_EDITOR
  156. protected override void OnValidate()
  157. {
  158. base.OnValidate();
  159. if (!IsActive())
  160. return;
  161. RefreshShownValue();
  162. }
  163. #endif
  164. public void RefreshShownValue()
  165. {
  166. OptionData data = s_NoOptionData;
  167. if (options.Count > 0)
  168. data = options[Mathf.Clamp(m_Value, 0, options.Count - 1)];
  169. if (m_CaptionText)
  170. {
  171. if (data != null && data.text != null)
  172. m_CaptionText.text = data.text;
  173. else
  174. m_CaptionText.text = "";
  175. }
  176. if (m_CaptionImage)
  177. {
  178. if (data != null)
  179. m_CaptionImage.sprite = data.image;
  180. else
  181. m_CaptionImage.sprite = null;
  182. m_CaptionImage.enabled = (m_CaptionImage.sprite != null);
  183. }
  184. }
  185. public void AddOptions(List<OptionData> options)
  186. {
  187. this.options.AddRange(options);
  188. RefreshShownValue();
  189. }
  190. public void AddOptions(List<string> options)
  191. {
  192. for (int i = 0; i < options.Count; i++)
  193. this.options.Add(new OptionData(options[i]));
  194. RefreshShownValue();
  195. }
  196. public void AddOptions(List<Sprite> options)
  197. {
  198. for (int i = 0; i < options.Count; i++)
  199. this.options.Add(new OptionData(options[i]));
  200. RefreshShownValue();
  201. }
  202. public void ClearOptions()
  203. {
  204. options.Clear();
  205. RefreshShownValue();
  206. }
  207. private void SetupTemplate()
  208. {
  209. validTemplate = false;
  210. if (!m_Template)
  211. {
  212. Debug.LogError("The dropdown template is not assigned. The template needs to be assigned and must have a child GameObject with a Toggle component serving as the item.", this);
  213. return;
  214. }
  215. GameObject templateGo = m_Template.gameObject;
  216. templateGo.SetActive(true);
  217. Toggle itemToggle = m_Template.GetComponentInChildren<Toggle>();
  218. validTemplate = true;
  219. if (!itemToggle || itemToggle.transform == template)
  220. {
  221. validTemplate = false;
  222. Debug.LogError("The dropdown template is not valid. The template must have a child GameObject with a Toggle component serving as the item.", template);
  223. }
  224. else if (!(itemToggle.transform.parent is RectTransform))
  225. {
  226. validTemplate = false;
  227. Debug.LogError("The dropdown template is not valid. The child GameObject with a Toggle component (the item) must have a RectTransform on its parent.", template);
  228. }
  229. else if (itemText != null && !itemText.transform.IsChildOf(itemToggle.transform))
  230. {
  231. validTemplate = false;
  232. Debug.LogError("The dropdown template is not valid. The Item Text must be on the item GameObject or children of it.", template);
  233. }
  234. else if (itemImage != null && !itemImage.transform.IsChildOf(itemToggle.transform))
  235. {
  236. validTemplate = false;
  237. Debug.LogError("The dropdown template is not valid. The Item Image must be on the item GameObject or children of it.", template);
  238. }
  239. if (!validTemplate)
  240. {
  241. templateGo.SetActive(false);
  242. return;
  243. }
  244. DropdownItem item = itemToggle.gameObject.AddComponent<DropdownItem>();
  245. item.text = m_ItemText;
  246. item.image = m_ItemImage;
  247. item.toggle = itemToggle;
  248. item.rectTransform = (RectTransform)itemToggle.transform;
  249. Canvas popupCanvas = GetOrAddComponent<Canvas>(templateGo);
  250. popupCanvas.overrideSorting = true;
  251. popupCanvas.sortingOrder = 30000;
  252. GetOrAddComponent<GraphicRaycaster>(templateGo);
  253. GetOrAddComponent<CanvasGroup>(templateGo);
  254. templateGo.SetActive(false);
  255. validTemplate = true;
  256. }
  257. private static T GetOrAddComponent<T>(GameObject go) where T : Component
  258. {
  259. T comp = go.GetComponent<T>();
  260. if (!comp)
  261. comp = go.AddComponent<T>();
  262. return comp;
  263. }
  264. public virtual void OnPointerClick(PointerEventData eventData)
  265. {
  266. Show();
  267. }
  268. public virtual void OnSubmit(BaseEventData eventData)
  269. {
  270. Show();
  271. }
  272. public virtual void OnCancel(BaseEventData eventData)
  273. {
  274. Hide();
  275. }
  276. // Show the dropdown.
  277. //
  278. // Plan for dropdown scrolling to ensure dropdown is contained within screen.
  279. //
  280. // We assume the Canvas is the screen that the dropdown must be kept inside.
  281. // This is always valid for screen space canvas modes.
  282. // For world space canvases we don't know how it's used, but it could be e.g. for an in-game monitor.
  283. // We consider it a fair constraint that the canvas must be big enough to contains dropdowns.
  284. public void Show()
  285. {
  286. if (!IsActive() || !IsInteractable() || m_Dropdown != null)
  287. return;
  288. if (!validTemplate)
  289. {
  290. SetupTemplate();
  291. if (!validTemplate)
  292. return;
  293. }
  294. // Get root Canvas.
  295. var list = TMP_ListPool<Canvas>.Get();
  296. gameObject.GetComponentsInParent(false, list);
  297. if (list.Count == 0)
  298. return;
  299. Canvas rootCanvas = list[0];
  300. TMP_ListPool<Canvas>.Release(list);
  301. m_Template.gameObject.SetActive(true);
  302. // Instantiate the drop-down template
  303. m_Dropdown = CreateDropdownList(m_Template.gameObject);
  304. m_Dropdown.name = "Dropdown List";
  305. m_Dropdown.SetActive(true);
  306. // Make drop-down RectTransform have same values as original.
  307. RectTransform dropdownRectTransform = m_Dropdown.transform as RectTransform;
  308. dropdownRectTransform.SetParent(m_Template.transform.parent, false);
  309. // Instantiate the drop-down list items
  310. // Find the dropdown item and disable it.
  311. DropdownItem itemTemplate = m_Dropdown.GetComponentInChildren<DropdownItem>();
  312. GameObject content = itemTemplate.rectTransform.parent.gameObject;
  313. RectTransform contentRectTransform = content.transform as RectTransform;
  314. itemTemplate.rectTransform.gameObject.SetActive(true);
  315. // Get the rects of the dropdown and item
  316. Rect dropdownContentRect = contentRectTransform.rect;
  317. Rect itemTemplateRect = itemTemplate.rectTransform.rect;
  318. // Calculate the visual offset between the item's edges and the background's edges
  319. Vector2 offsetMin = itemTemplateRect.min - dropdownContentRect.min + (Vector2)itemTemplate.rectTransform.localPosition;
  320. Vector2 offsetMax = itemTemplateRect.max - dropdownContentRect.max + (Vector2)itemTemplate.rectTransform.localPosition;
  321. Vector2 itemSize = itemTemplateRect.size;
  322. m_Items.Clear();
  323. Toggle prev = null;
  324. for (int i = 0; i < options.Count; ++i)
  325. {
  326. OptionData data = options[i];
  327. DropdownItem item = AddItem(data, value == i, itemTemplate, m_Items);
  328. if (item == null)
  329. continue;
  330. // Automatically set up a toggle state change listener
  331. item.toggle.isOn = value == i;
  332. item.toggle.onValueChanged.AddListener(x => OnSelectItem(item.toggle));
  333. // Select current option
  334. if (item.toggle.isOn)
  335. item.toggle.Select();
  336. // Automatically set up explicit navigation
  337. if (prev != null)
  338. {
  339. Navigation prevNav = prev.navigation;
  340. Navigation toggleNav = item.toggle.navigation;
  341. prevNav.mode = Navigation.Mode.Explicit;
  342. toggleNav.mode = Navigation.Mode.Explicit;
  343. prevNav.selectOnDown = item.toggle;
  344. prevNav.selectOnRight = item.toggle;
  345. toggleNav.selectOnLeft = prev;
  346. toggleNav.selectOnUp = prev;
  347. prev.navigation = prevNav;
  348. item.toggle.navigation = toggleNav;
  349. }
  350. prev = item.toggle;
  351. }
  352. // Reposition all items now that all of them have been added
  353. Vector2 sizeDelta = contentRectTransform.sizeDelta;
  354. sizeDelta.y = itemSize.y * m_Items.Count + offsetMin.y - offsetMax.y;
  355. contentRectTransform.sizeDelta = sizeDelta;
  356. float extraSpace = dropdownRectTransform.rect.height - contentRectTransform.rect.height;
  357. if (extraSpace > 0)
  358. dropdownRectTransform.sizeDelta = new Vector2(dropdownRectTransform.sizeDelta.x, dropdownRectTransform.sizeDelta.y - extraSpace);
  359. // Invert anchoring and position if dropdown is partially or fully outside of canvas rect.
  360. // Typically this will have the effect of placing the dropdown above the button instead of below,
  361. // but it works as inversion regardless of initial setup.
  362. Vector3[] corners = new Vector3[4];
  363. dropdownRectTransform.GetWorldCorners(corners);
  364. RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
  365. Rect rootCanvasRect = rootCanvasRectTransform.rect;
  366. for (int axis = 0; axis < 2; axis++)
  367. {
  368. bool outside = false;
  369. for (int i = 0; i < 4; i++)
  370. {
  371. Vector3 corner = rootCanvasRectTransform.InverseTransformPoint(corners[i]);
  372. if (corner[axis] < rootCanvasRect.min[axis] || corner[axis] > rootCanvasRect.max[axis])
  373. {
  374. outside = true;
  375. break;
  376. }
  377. }
  378. if (outside)
  379. RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, axis, false, false);
  380. }
  381. for (int i = 0; i < m_Items.Count; i++)
  382. {
  383. RectTransform itemRect = m_Items[i].rectTransform;
  384. itemRect.anchorMin = new Vector2(itemRect.anchorMin.x, 0);
  385. itemRect.anchorMax = new Vector2(itemRect.anchorMax.x, 0);
  386. itemRect.anchoredPosition = new Vector2(itemRect.anchoredPosition.x, offsetMin.y + itemSize.y * (m_Items.Count - 1 - i) + itemSize.y * itemRect.pivot.y);
  387. itemRect.sizeDelta = new Vector2(itemRect.sizeDelta.x, itemSize.y);
  388. }
  389. // Fade in the popup
  390. AlphaFadeList(0.15f, 0f, 1f);
  391. // Make drop-down template and item template inactive
  392. m_Template.gameObject.SetActive(false);
  393. itemTemplate.gameObject.SetActive(false);
  394. m_Blocker = CreateBlocker(rootCanvas);
  395. }
  396. protected virtual GameObject CreateBlocker(Canvas rootCanvas)
  397. {
  398. // Create blocker GameObject.
  399. GameObject blocker = new GameObject("Blocker");
  400. // Setup blocker RectTransform to cover entire root canvas area.
  401. RectTransform blockerRect = blocker.AddComponent<RectTransform>();
  402. blockerRect.SetParent(rootCanvas.transform, false);
  403. blockerRect.anchorMin = Vector3.zero;
  404. blockerRect.anchorMax = Vector3.one;
  405. blockerRect.sizeDelta = Vector2.zero;
  406. // Make blocker be in separate canvas in same layer as dropdown and in layer just below it.
  407. Canvas blockerCanvas = blocker.AddComponent<Canvas>();
  408. blockerCanvas.overrideSorting = true;
  409. Canvas dropdownCanvas = m_Dropdown.GetComponent<Canvas>();
  410. blockerCanvas.sortingLayerID = dropdownCanvas.sortingLayerID;
  411. blockerCanvas.sortingOrder = dropdownCanvas.sortingOrder - 1;
  412. // Add raycaster since it's needed to block.
  413. blocker.AddComponent<GraphicRaycaster>();
  414. // Add image since it's needed to block, but make it clear.
  415. Image blockerImage = blocker.AddComponent<Image>();
  416. blockerImage.color = Color.clear;
  417. // Add button since it's needed to block, and to close the dropdown when blocking area is clicked.
  418. Button blockerButton = blocker.AddComponent<Button>();
  419. blockerButton.onClick.AddListener(Hide);
  420. return blocker;
  421. }
  422. protected virtual void DestroyBlocker(GameObject blocker)
  423. {
  424. Destroy(blocker);
  425. }
  426. protected virtual GameObject CreateDropdownList(GameObject template)
  427. {
  428. return (GameObject)Instantiate(template);
  429. }
  430. protected virtual void DestroyDropdownList(GameObject dropdownList)
  431. {
  432. Destroy(dropdownList);
  433. }
  434. protected virtual DropdownItem CreateItem(DropdownItem itemTemplate)
  435. {
  436. return (DropdownItem)Instantiate(itemTemplate);
  437. }
  438. protected virtual void DestroyItem(DropdownItem item)
  439. {
  440. // No action needed since destroying the dropdown list destroys all contained items as well.
  441. }
  442. // Add a new drop-down list item with the specified values.
  443. private DropdownItem AddItem(OptionData data, bool selected, DropdownItem itemTemplate, List<DropdownItem> items)
  444. {
  445. // Add a new item to the dropdown.
  446. DropdownItem item = CreateItem(itemTemplate);
  447. item.rectTransform.SetParent(itemTemplate.rectTransform.parent, false);
  448. item.gameObject.SetActive(true);
  449. item.gameObject.name = "Item " + items.Count + (data.text != null ? ": " + data.text : "");
  450. if (item.toggle != null)
  451. {
  452. item.toggle.isOn = false;
  453. }
  454. // Set the item's data
  455. if (item.text)
  456. item.text.text = data.text;
  457. if (item.image)
  458. {
  459. item.image.sprite = data.image;
  460. item.image.enabled = (item.image.sprite != null);
  461. }
  462. items.Add(item);
  463. return item;
  464. }
  465. private void AlphaFadeList(float duration, float alpha)
  466. {
  467. CanvasGroup group = m_Dropdown.GetComponent<CanvasGroup>();
  468. AlphaFadeList(duration, group.alpha, alpha);
  469. }
  470. private void AlphaFadeList(float duration, float start, float end)
  471. {
  472. if (end.Equals(start))
  473. return;
  474. FloatTween tween = new FloatTween { duration = duration, startValue = start, targetValue = end };
  475. tween.AddOnChangedCallback(SetAlpha);
  476. tween.ignoreTimeScale = true;
  477. m_AlphaTweenRunner.StartTween(tween);
  478. }
  479. private void SetAlpha(float alpha)
  480. {
  481. if (!m_Dropdown)
  482. return;
  483. CanvasGroup group = m_Dropdown.GetComponent<CanvasGroup>();
  484. group.alpha = alpha;
  485. }
  486. // Hide the dropdown.
  487. public void Hide()
  488. {
  489. if (m_Dropdown != null)
  490. {
  491. AlphaFadeList(0.15f, 0f);
  492. // User could have disabled the dropdown during the OnValueChanged call.
  493. if (IsActive())
  494. StartCoroutine(DelayedDestroyDropdownList(0.15f));
  495. }
  496. if (m_Blocker != null)
  497. DestroyBlocker(m_Blocker);
  498. m_Blocker = null;
  499. Select();
  500. }
  501. private IEnumerator DelayedDestroyDropdownList(float delay)
  502. {
  503. yield return new WaitForSecondsRealtime(delay);
  504. for (int i = 0; i < m_Items.Count; i++)
  505. {
  506. if (m_Items[i] != null)
  507. DestroyItem(m_Items[i]);
  508. m_Items.Clear();
  509. }
  510. if (m_Dropdown != null)
  511. DestroyDropdownList(m_Dropdown);
  512. m_Dropdown = null;
  513. }
  514. // Change the value and hide the dropdown.
  515. private void OnSelectItem(Toggle toggle)
  516. {
  517. if (!toggle.isOn)
  518. toggle.isOn = true;
  519. int selectedIndex = -1;
  520. Transform tr = toggle.transform;
  521. Transform parent = tr.parent;
  522. for (int i = 0; i < parent.childCount; i++)
  523. {
  524. if (parent.GetChild(i) == tr)
  525. {
  526. // Subtract one to account for template child.
  527. selectedIndex = i - 1;
  528. break;
  529. }
  530. }
  531. if (selectedIndex < 0)
  532. return;
  533. value = selectedIndex;
  534. Hide();
  535. }
  536. }
  537. }