1443 lines
56 KiB
C#
Raw Normal View History

2026-05-06 15:07:56 +02:00
using System;
using UnityEngine.Serialization;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using DuloGames.UI.Tweens;
namespace DuloGames.UI
{
[ExecuteInEditMode, DisallowMultipleComponent, AddComponentMenu("UI/Select Field", 58), RequireComponent(typeof(Image))]
public class UISelectField : Toggle
{
public enum Direction
{
Auto,
Down,
Up
}
public enum VisualState
{
Normal,
Highlighted,
Pressed,
Active,
ActiveHighlighted,
ActivePressed,
Disabled
}
public enum ListAnimationType
{
None,
Fade,
Animation
}
public enum OptionTextTransitionType
{
None,
CrossFade
}
public enum OptionTextEffectType
{
None,
Shadow,
Outline
}
// Currently selected item
[HideInInspector]
[SerializeField]
private string m_SelectedItem;
private List<UISelectField_Option> m_OptionObjects = new List<UISelectField_Option>();
private VisualState m_CurrentVisualState = VisualState.Normal;
private bool m_PointerWasUsedOnOption = false;
private GameObject m_ListObject;
private ScrollRect m_ScrollRect;
private GameObject m_ListContentObject;
private CanvasGroup m_ListCanvasGroup;
private Vector2 m_LastListSize = Vector2.zero;
private GameObject m_StartSeparatorObject;
private Navigation.Mode m_LastNavigationMode;
private GameObject m_LastSelectedGameObject;
private GameObject m_Blocker;
[SerializeField]
private Direction m_Direction = Direction.Auto;
/// <summary>
/// The direction in which the list should pop.
/// </summary>
public Direction direction
{
get { return this.m_Direction; }
set { this.m_Direction = value; }
}
/// <summary>
/// Private list of the select options.
/// </summary>
[SerializeField, FormerlySerializedAs("options")]
private List<string> m_Options = new List<string>();
/// <summary>
/// Gets the list of options.
/// </summary>
public List<string> options
{
get { return this.m_Options; }
}
/// <summary>
/// Currently selected option.
/// </summary>
public string value
{
get
{
return this.m_SelectedItem;
}
set
{
this.SelectOption(value);
}
}
/// <summary>
/// Gets the index of the selected option.
/// </summary>
/// <value>The index of the selected option.</value>
public int selectedOptionIndex
{
get
{
return this.GetOptionIndex(this.m_SelectedItem);
}
}
// The label text
[SerializeField]
private Text m_LabelText;
// Select Field layout properties
public new ColorBlockExtended colors = ColorBlockExtended.defaultColorBlock;
public new SpriteStateExtended spriteState;
public new AnimationTriggersExtended animationTriggers = new AnimationTriggersExtended();
// List layout properties
public Sprite listBackgroundSprite;
public Image.Type listBackgroundSpriteType = Image.Type.Sliced;
public Color listBackgroundColor = Color.white;
public RectOffset listMargins;
public RectOffset listPadding;
public float listSpacing = 0f;
public ListAnimationType listAnimationType = ListAnimationType.Fade;
public float listAnimationDuration = 0.1f;
public RuntimeAnimatorController listAnimatorController;
public string listAnimationOpenTrigger = "Open";
public string listAnimationCloseTrigger = "Close";
// Scroll rect properties
public bool allowScrollRect = true;
public ScrollRect.MovementType scrollMovementType = ScrollRect.MovementType.Clamped;
public float scrollElasticity = 0.1f;
public bool scrollInertia = false;
public float scrollDecelerationRate = 0.135f;
public float scrollSensitivity = 1f;
public int scrollMinOptions = 5;
public float scrollListHeight = 512f;
public GameObject scrollBarPrefab;
public float scrollbarSpacing = 34f;
// Option text layout properties
public Font optionFont = FontData.defaultFontData.font;
public int optionFontSize = FontData.defaultFontData.fontSize;
public FontStyle optionFontStyle = FontData.defaultFontData.fontStyle;
public Color optionColor = Color.white;
public OptionTextTransitionType optionTextTransitionType = OptionTextTransitionType.CrossFade;
public ColorBlockExtended optionTextTransitionColors = ColorBlockExtended.defaultColorBlock;
public RectOffset optionPadding;
// Option text effect properties
public OptionTextEffectType optionTextEffectType = OptionTextEffectType.None;
public Color optionTextEffectColor = new Color(0f, 0f, 0f, 128f);
public Vector2 optionTextEffectDistance = new Vector2(1f, -1f);
public bool optionTextEffectUseGraphicAlpha = true;
// Option background properties
public Sprite optionBackgroundSprite;
public Color optionBackgroundSpriteColor = Color.white;
public Image.Type optionBackgroundSpriteType = Image.Type.Sliced;
public Selectable.Transition optionBackgroundTransitionType = Selectable.Transition.None;
public ColorBlockExtended optionBackgroundTransColors = ColorBlockExtended.defaultColorBlock;
public SpriteStateExtended optionBackgroundSpriteStates;
public AnimationTriggersExtended optionBackgroundAnimationTriggers = new AnimationTriggersExtended();
public RuntimeAnimatorController optionBackgroundAnimatorController;
public Sprite optionHoverOverlay;
public Color optionHoverOverlayColor = Color.white;
public ColorBlock optionHoverOverlayColorBlock = ColorBlock.defaultColorBlock;
public Sprite optionPressOverlay;
public Color optionPressOverlayColor = Color.white;
public ColorBlock optionPressOverlayColorBlock = ColorBlock.defaultColorBlock;
// List separator properties
public Sprite listSeparatorSprite;
public Image.Type listSeparatorType = Image.Type.Simple;
public Color listSeparatorColor = Color.white;
public float listSeparatorHeight = 0f;
public bool startSeparator = false;
[Serializable]
public class ChangeEvent : UnityEvent<int, string> { }
[Serializable]
public class TransitionEvent : UnityEvent<VisualState, bool> { }
/// <summary>
/// Event delegate triggered when the selected option changes.
/// </summary>
public ChangeEvent onChange = new ChangeEvent();
/// <summary>
/// Event delegate triggered when the select field transition to a visual state.
/// </summary>
public TransitionEvent onTransition = new TransitionEvent();
// Tween controls
[NonSerialized]
private readonly TweenRunner<FloatTween> m_FloatTweenRunner;
// Called by Unity prior to deserialization,
// should not be called by users
protected UISelectField()
{
if (this.m_FloatTweenRunner == null)
this.m_FloatTweenRunner = new TweenRunner<FloatTween>();
this.m_FloatTweenRunner.Init(this);
}
protected override void Awake()
{
base.Awake();
// Get the background image
if (this.targetGraphic == null)
this.targetGraphic = this.GetComponent<Image>();
}
protected override void Start()
{
base.Start();
// Prepare the toggle
this.toggleTransition = ToggleTransition.None;
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
// Make sure we always have a font
if (this.optionFont == null)
this.optionFont = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font;
}
#endif
protected override void OnEnable()
{
base.OnEnable();
// Hook the on change event
this.onValueChanged.AddListener(OnToggleValueChanged);
}
protected override void OnDisable()
{
base.OnDisable();
// Unhook the on change event
this.onValueChanged.RemoveListener(OnToggleValueChanged);
// Close if open
this.isOn = false;
// Transition to the current state
this.DoStateTransition(SelectionState.Disabled, true);
}
/// <summary>
/// Open the select field list.
/// </summary>
public void Open() { this.isOn = true; }
/// <summary>
/// Closes the select field list.
/// </summary>
public void Close() { this.isOn = false; }
/// <summary>
/// Gets a value indicating whether the list is open.
/// </summary>
/// <value><c>true</c> if the list is open; otherwise, <c>false</c>.</value>
public bool IsOpen
{
get
{
return this.isOn;
}
}
/// <summary>
/// Gets the index of the given option.
/// </summary>
/// <returns>The option index. (-1 if the option was not found)</returns>
/// <param name="optionValue">Option value.</param>
public int GetOptionIndex(string optionValue)
{
// Find the option index in the options list
if (this.m_Options != null && this.m_Options.Count > 0 && !string.IsNullOrEmpty(optionValue))
for (int i = 0; i < this.m_Options.Count; i++)
if (optionValue.Equals(this.m_Options[i], System.StringComparison.OrdinalIgnoreCase))
return i;
// Default
return -1;
}
/// <summary>
/// Selects the option by index.
/// </summary>
/// <param name="optionIndex">Option index.</param>
public void SelectOptionByIndex(int index)
{
// If the list is open, use the toggle to select the option
if (this.IsOpen)
{
UISelectField_Option option = this.m_OptionObjects[index];
if (option != null)
option.isOn = true;
}
else // otherwise set as selected
{
// Set as selected
this.m_SelectedItem = this.m_Options[index];
// Trigger change
this.TriggerChangeEvent();
}
}
/// <summary>
/// Selects the option by value.
/// </summary>
/// <param name="optionValue">The option value.</param>
public void SelectOption(string optionValue)
{
if (string.IsNullOrEmpty(optionValue))
return;
// Get the option
int index = this.GetOptionIndex(optionValue);
// Check if the option index is valid
if (index < 0 || index >= this.m_Options.Count)
return;
// Select the option
this.SelectOptionByIndex(index);
}
/// <summary>
/// Adds an option.
/// </summary>
/// <param name="optionValue">Option value.</param>
public void AddOption(string optionValue)
{
if (this.m_Options != null)
{
this.m_Options.Add(optionValue);
this.OptionListChanged();
}
}
/// <summary>
/// Adds an option at given index.
/// </summary>
/// <param name="optionValue">Option value.</param>
/// <param name="index">Index.</param>
public void AddOptionAtIndex(string optionValue, int index)
{
if (this.m_Options == null)
return;
// Check if the index is outside the list
if (index >= this.m_Options.Count)
{
this.m_Options.Add(optionValue);
}
else
{
this.m_Options.Insert(index, optionValue);
}
this.OptionListChanged();
}
/// <summary>
/// Removes the option.
/// </summary>
/// <param name="optionValue">Option value.</param>
public void RemoveOption(string optionValue)
{
if (this.m_Options == null)
return;
// Remove the option if exists
if (this.m_Options.Contains(optionValue))
{
this.m_Options.Remove(optionValue);
this.OptionListChanged();
this.ValidateSelectedOption();
}
}
/// <summary>
/// Removes the option at the given index.
/// </summary>
/// <param name="index">Index.</param>
public void RemoveOptionAtIndex(int index)
{
if (this.m_Options == null)
return;
// Remove the option if the index is valid
if (index >= 0 && index < this.m_Options.Count)
{
this.m_Options.RemoveAt(index);
this.OptionListChanged();
this.ValidateSelectedOption();
}
}
/// <summary>
/// Clears the option list.
/// </summary>
public void ClearOptions()
{
if (this.m_Options == null)
return;
this.m_Options.Clear();
this.OptionListChanged();
}
/// <summary>
/// Validates the selected option and makes corrections if it's missing.
/// </summary>
public void ValidateSelectedOption()
{
if (this.m_Options == null)
return;
// Fix the selected option if it no longer exists
if (!this.m_Options.Contains(this.m_SelectedItem))
{
// Select the first option
this.SelectOptionByIndex(0);
}
}
/// <summary>
/// Raises the option select event.
/// </summary>
/// <param name="eventData">Event data.</param>
/// <param name="option">Option.</param>
public void OnOptionSelect(string option)
{
if (string.IsNullOrEmpty(option))
return;
// Save the current string to compare later
string current = this.m_SelectedItem;
// Save the string
this.m_SelectedItem = option;
// Trigger change event
if (!current.Equals(this.m_SelectedItem))
this.TriggerChangeEvent();
// Close the list if it's opened and the pointer was used to select the option
if (this.IsOpen && this.m_PointerWasUsedOnOption)
{
// Reset the value
this.m_PointerWasUsedOnOption = false;
// Close the list
this.Close();
// Deselect the toggle
base.OnDeselect(new BaseEventData(EventSystem.current));
}
}
/// <summary>
/// Raises the option pointer up event (Used to close the list).
/// </summary>
/// <param name="eventData">Event data.</param>
public void OnOptionPointerUp(BaseEventData eventData)
{
// Flag to close the list on selection
this.m_PointerWasUsedOnOption = true;
}
/// <summary>
/// Tiggers the change event.
/// </summary>
protected virtual void TriggerChangeEvent()
{
// Apply the string to the label componenet
if (this.m_LabelText != null)
this.m_LabelText.text = this.m_SelectedItem;
// Invoke the on change event
if (onChange != null)
onChange.Invoke(this.selectedOptionIndex, this.m_SelectedItem);
}
/// <summary>
/// Raises the toggle value changed event (used to toggle the list).
/// </summary>
/// <param name="state">If set to <c>true</c> state.</param>
private void OnToggleValueChanged(bool state)
{
if (!Application.isPlaying)
return;
// Transition to the current state
this.DoStateTransition(this.currentSelectionState, false);
// Open / Close the list
this.ToggleList(this.isOn);
// Destroy the block on close
if (!this.isOn && this.m_Blocker != null)
Destroy(this.m_Blocker);
}
/// <summary>
/// Raises the deselect event.
/// </summary>
/// <param name="eventData">Event data.</param>
public override void OnDeselect(BaseEventData eventData)
{
// Check if the mouse is over our options list
if (this.m_ListObject != null)
{
UISelectField_List list = this.m_ListObject.GetComponent<UISelectField_List>();
if (list.IsHighlighted())
return;
}
// Check if the mouse is over one of our options
foreach (UISelectField_Option option in this.m_OptionObjects)
{
if (option.IsHighlighted())
return;
}
// When the select field loses focus
// close the list by deactivating the toggle
this.Close();
// Pass to base
base.OnDeselect(eventData);
}
/// <summary>
/// Raises the move event.
/// </summary>
/// <param name="eventData">Event data.</param>
public override void OnMove(AxisEventData eventData)
{
// Handle navigation for opened list
if (this.IsOpen)
{
int prevIndex = (this.selectedOptionIndex - 1);
int nextIndex = (this.selectedOptionIndex + 1);
// Highlight the new option
switch (eventData.moveDir)
{
case MoveDirection.Up:
{
if (prevIndex >= 0)
{
this.SelectOptionByIndex(prevIndex);
}
break;
}
case MoveDirection.Down:
{
if (nextIndex < this.m_Options.Count)
{
this.SelectOptionByIndex(nextIndex);
}
break;
}
}
// Use the event
eventData.Use();
}
else
{
// Pass to base
base.OnMove(eventData);
}
}
/// <summary>
/// Dos the state transition of the select field.
/// </summary>
/// <param name="state">State.</param>
/// <param name="instant">If set to <c>true</c> instant.</param>
protected override void DoStateTransition(Selectable.SelectionState state, bool instant)
{
if (!this.gameObject.activeInHierarchy)
return;
Color color = this.colors.normalColor;
Sprite newSprite = null;
string triggername = this.animationTriggers.normalTrigger;
// Check if this is the disabled state before any others
if (state == Selectable.SelectionState.Disabled)
{
this.m_CurrentVisualState = VisualState.Disabled;
color = this.colors.disabledColor;
newSprite = this.spriteState.disabledSprite;
triggername = this.animationTriggers.disabledTrigger;
}
else
{
// Prepare the state values
switch (state)
{
case Selectable.SelectionState.Normal:
this.m_CurrentVisualState = (this.isOn) ? VisualState.Active : VisualState.Normal;
color = (this.isOn) ? this.colors.activeColor : this.colors.normalColor;
newSprite = (this.isOn) ? this.spriteState.activeSprite : null;
triggername = (this.isOn) ? this.animationTriggers.activeTrigger : this.animationTriggers.normalTrigger;
break;
case Selectable.SelectionState.Highlighted:
this.m_CurrentVisualState = (this.isOn) ? VisualState.ActiveHighlighted : VisualState.Highlighted;
color = (this.isOn) ? this.colors.activeHighlightedColor : this.colors.highlightedColor;
newSprite = (this.isOn) ? this.spriteState.activeHighlightedSprite : this.spriteState.highlightedSprite;
triggername = (this.isOn) ? this.animationTriggers.activeHighlightedTrigger : this.animationTriggers.highlightedTrigger;
break;
case Selectable.SelectionState.Pressed:
this.m_CurrentVisualState = (this.isOn) ? VisualState.ActivePressed : VisualState.Pressed;
color = (this.isOn) ? this.colors.activePressedColor : this.colors.pressedColor;
newSprite = (this.isOn) ? this.spriteState.activePressedSprite : this.spriteState.pressedSprite;
triggername = (this.isOn) ? this.animationTriggers.activePressedTrigger : this.animationTriggers.pressedTrigger;
break;
}
}
// Do the transition
switch (this.transition)
{
case Selectable.Transition.ColorTint:
this.StartColorTween(color * this.colors.colorMultiplier, (instant ? 0f : this.colors.fadeDuration));
break;
case Selectable.Transition.SpriteSwap:
this.DoSpriteSwap(newSprite);
break;
case Selectable.Transition.Animation:
this.TriggerAnimation(triggername);
break;
}
// Invoke the transition event
if (this.onTransition != null)
{
this.onTransition.Invoke(this.m_CurrentVisualState, instant);
}
}
/// <summary>
/// Starts the color tween of the select field.
/// </summary>
/// <param name="color">Color.</param>
/// <param name="instant">If set to <c>true</c> instant.</param>
private void StartColorTween(Color color, float duration)
{
if (this.targetGraphic == null)
return;
this.targetGraphic.CrossFadeColor(color, duration, true, true);
}
/// <summary>
/// Does the sprite swap of the select field.
/// </summary>
/// <param name="newSprite">New sprite.</param>
private void DoSpriteSwap(Sprite newSprite)
{
Image image = this.targetGraphic as Image;
if (image == null)
return;
image.overrideSprite = newSprite;
}
/// <summary>
/// Triggers the animation of the select field.
/// </summary>
/// <param name="trigger">Trigger.</param>
private void TriggerAnimation(string trigger)
{
if (this.animator == null || !this.animator.enabled || !this.animator.isActiveAndEnabled || this.animator.runtimeAnimatorController == null || !this.animator.hasBoundPlayables || string.IsNullOrEmpty(trigger))
return;
this.animator.ResetTrigger(this.animationTriggers.normalTrigger);
this.animator.ResetTrigger(this.animationTriggers.pressedTrigger);
this.animator.ResetTrigger(this.animationTriggers.highlightedTrigger);
this.animator.ResetTrigger(this.animationTriggers.activeTrigger);
this.animator.ResetTrigger(this.animationTriggers.activeHighlightedTrigger);
this.animator.ResetTrigger(this.animationTriggers.activePressedTrigger);
this.animator.ResetTrigger(this.animationTriggers.disabledTrigger);
this.animator.SetTrigger(trigger);
}
/// <summary>
/// Toggles the list.
/// </summary>
/// <param name="state">If set to <c>true</c> state.</param>
protected virtual void ToggleList(bool state)
{
if (!this.IsActive())
return;
// Check if the list is not yet created
if (this.m_ListObject == null)
this.CreateList();
// Make sure the creating of the list was successful
if (this.m_ListObject == null)
return;
// Make sure we have the canvas group
if (this.m_ListCanvasGroup != null)
{
// Disable or enable list interaction
this.m_ListCanvasGroup.blocksRaycasts = state;
}
// Make sure navigation is enabled in open state
if (state)
{
this.m_LastNavigationMode = this.navigation.mode;
this.m_LastSelectedGameObject = EventSystem.current.currentSelectedGameObject;
Navigation newNav = this.navigation;
newNav.mode = Navigation.Mode.Vertical;
this.navigation = newNav;
// Set the select field as selected
EventSystem.current.SetSelectedGameObject(this.gameObject);
}
else
{
Navigation newNav = this.navigation;
newNav.mode = this.m_LastNavigationMode;
this.navigation = newNav;
if (!EventSystem.current.alreadySelecting && this.m_LastSelectedGameObject != null)
EventSystem.current.SetSelectedGameObject(this.m_LastSelectedGameObject);
}
// Bring to front
if (state) UIUtility.BringToFront(this.m_ListObject);
// Start the opening/closing animation
if (this.listAnimationType == ListAnimationType.None || this.listAnimationType == ListAnimationType.Fade)
{
float targetAlpha = (state ? 1f : 0f);
// Fade In / Out
this.TweenListAlpha(targetAlpha, ((this.listAnimationType == ListAnimationType.Fade) ? this.listAnimationDuration : 0f), true);
}
else if (this.listAnimationType == ListAnimationType.Animation)
{
this.TriggerListAnimation(state ? this.listAnimationOpenTrigger : this.listAnimationCloseTrigger);
}
}
/// <summary>
/// Creates the list and it's options.
/// </summary>
protected void CreateList()
{
// Get the root canvas
Canvas rootCanvas = UIUtility.FindInParents<Canvas>(this.gameObject);
// Reset the last list size
this.m_LastListSize = Vector2.zero;
// Clear the option texts list
this.m_OptionObjects.Clear();
// Create the list game object with the necessary components
this.m_ListObject = new GameObject("UISelectField - List", typeof(RectTransform));
this.m_ListObject.layer = this.gameObject.layer;
// Change the parent of the list
this.m_ListObject.transform.SetParent(this.transform, false);
// Get the select field list component
UISelectField_List listComp = this.m_ListObject.AddComponent<UISelectField_List>();
// Make sure it's the top-most element
UIAlwaysOnTop aot = this.m_ListObject.AddComponent<UIAlwaysOnTop>();
aot.order = UIAlwaysOnTop.SelectFieldOrder;
// Get the list canvas group component
this.m_ListCanvasGroup = this.m_ListObject.AddComponent<CanvasGroup>();
// Change the anchor and pivot of the list
RectTransform rect = (this.m_ListObject.transform as RectTransform);
rect.localScale = new Vector3(1f, 1f, 1f);
rect.localPosition = Vector3.zero;
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.zero;
rect.pivot = new Vector2(0f, 1f);
// Prepare the position of the list
rect.anchoredPosition = new Vector3(this.listMargins.left, (this.listMargins.top * -1f), 0f);
// Prepare the width of the list
float width = (this.transform as RectTransform).sizeDelta.x;
if (this.listMargins.left > 0) width -= this.listMargins.left; else width += Math.Abs(this.listMargins.left);
if (this.listMargins.right > 0) width -= this.listMargins.right; else width += Math.Abs(this.listMargins.right);
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
// Hook the Dimensions Change event
listComp.onDimensionsChange.AddListener(ListDimensionsChanged);
// Apply the background sprite
Image image = this.m_ListObject.AddComponent<Image>();
if (this.listBackgroundSprite != null)
image.sprite = this.listBackgroundSprite;
image.type = this.listBackgroundSpriteType;
image.color = this.listBackgroundColor;
if (this.allowScrollRect && this.m_Options.Count >= this.scrollMinOptions)
{
// Set the list height
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, this.scrollListHeight);
// Add scroll rect
GameObject scrollRectGo = new GameObject("Scroll Rect", typeof(RectTransform));
scrollRectGo.layer = this.m_ListObject.layer;
scrollRectGo.transform.SetParent(this.m_ListObject.transform, false);
RectTransform scrollRectRect = (scrollRectGo.transform as RectTransform);
scrollRectRect.localScale = new Vector3(1f, 1f, 1f);
scrollRectRect.localPosition = Vector3.zero;
scrollRectRect.anchorMin = Vector2.zero;
scrollRectRect.anchorMax = Vector2.one;
scrollRectRect.pivot = new Vector2(0f, 1f);
scrollRectRect.anchoredPosition = Vector2.zero;
scrollRectRect.offsetMin = new Vector2(this.listPadding.left, this.listPadding.bottom);
scrollRectRect.offsetMax = new Vector2(this.listPadding.right * -1f, this.listPadding.top * -1f);
// Add scroll rect component
this.m_ScrollRect = scrollRectGo.AddComponent<ScrollRect>();
this.m_ScrollRect.horizontal = false;
this.m_ScrollRect.movementType = this.scrollMovementType;
this.m_ScrollRect.elasticity = this.scrollElasticity;
this.m_ScrollRect.inertia = this.scrollInertia;
this.m_ScrollRect.decelerationRate = this.scrollDecelerationRate;
this.m_ScrollRect.scrollSensitivity = this.scrollSensitivity;
// Create the viewport
GameObject viewPortGo = new GameObject("View Port", typeof(RectTransform));
viewPortGo.layer = this.m_ListObject.layer;
viewPortGo.transform.SetParent(scrollRectGo.transform, false);
RectTransform viewPortRect = (viewPortGo.transform as RectTransform);
viewPortRect.localScale = new Vector3(1f, 1f, 1f);
viewPortRect.localPosition = Vector3.zero;
viewPortRect.anchorMin = Vector2.zero;
viewPortRect.anchorMax = Vector2.one;
viewPortRect.pivot = new Vector2(0f, 1f);
viewPortRect.anchoredPosition = Vector2.zero;
viewPortRect.offsetMin = Vector2.zero;
viewPortRect.offsetMax = Vector2.zero;
// Add image to the viewport
Image viewImage = viewPortGo.AddComponent<Image>();
viewImage.raycastTarget = false;
// Add mask to the viewport
Mask viewMask = viewPortGo.AddComponent<Mask>();
viewMask.showMaskGraphic = false;
// Create content
this.m_ListContentObject = new GameObject("Content", typeof(RectTransform));
this.m_ListContentObject.layer = this.m_ListObject.layer;
this.m_ListContentObject.transform.SetParent(viewPortRect, false);
RectTransform contentRect = (this.m_ListContentObject.transform as RectTransform);
contentRect.localScale = new Vector3(1f, 1f, 1f);
contentRect.localPosition = Vector3.zero;
contentRect.anchorMin = new Vector2(0f, 1f);
contentRect.anchorMax = new Vector2(0f, 1f);
contentRect.pivot = new Vector2(0f, 1f);
contentRect.anchoredPosition = Vector2.zero;
contentRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.sizeDelta.x);
// Add image to the content for easy scrolling
Image contentImage = this.m_ListContentObject.AddComponent<Image>();
contentImage.color = new Color(1f, 1f, 1f, 0f);
// Get the select field list component
UISelectField_List contentListComp = this.m_ListContentObject.AddComponent<UISelectField_List>();
contentListComp.onDimensionsChange.AddListener(ScrollContentDimensionsChanged);
// Set the content and viewport to the scroll rect
this.m_ScrollRect.content = contentRect;
this.m_ScrollRect.viewport = viewPortRect;
// Prepare the scroll bar
if (this.scrollBarPrefab != null)
{
GameObject scrollBarGo = Instantiate(this.scrollBarPrefab, scrollRectGo.transform);
this.m_ScrollRect.verticalScrollbar = scrollBarGo.GetComponent<Scrollbar>();
this.m_ScrollRect.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHideAndExpandViewport;
this.m_ScrollRect.verticalScrollbarSpacing = this.scrollbarSpacing;
}
// Prepare the vertical layout group without list padding
this.m_ListContentObject.AddComponent<VerticalLayoutGroup>();
}
else
{
// Use the list object as list content object
this.m_ListContentObject = this.m_ListObject;
// Prepare the vertical layout group with list padding
VerticalLayoutGroup layoutGroup = this.m_ListContentObject.AddComponent<VerticalLayoutGroup>();
layoutGroup.padding = this.listPadding;
layoutGroup.spacing = this.listSpacing;
}
// Prepare the content size fitter
ContentSizeFitter fitter = this.m_ListContentObject.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
// Get the list toggle group
ToggleGroup toggleGroup = this.m_ListObject.AddComponent<ToggleGroup>();
// Create the options
for (int i = 0; i < this.m_Options.Count; i++)
{
if (i == 0 && this.startSeparator)
this.m_StartSeparatorObject = this.CreateSeparator(i - 1);
// Create the option
this.CreateOption(i, toggleGroup);
// Create a separator if this is not the last option
if (i < (this.m_Options.Count - 1))
this.CreateSeparator(i);
}
// Prepare the list for the animation
if (this.listAnimationType == ListAnimationType.None || this.listAnimationType == ListAnimationType.Fade)
{
// Starting alpha should be zero
this.m_ListCanvasGroup.alpha = 0f;
}
else if (this.listAnimationType == ListAnimationType.Animation)
{
// Attach animator component
Animator animator = this.m_ListObject.AddComponent<Animator>();
// Set the animator controller
animator.runtimeAnimatorController = this.listAnimatorController;
// Set the animation triggers so we can use them to detect when animations finish
listComp.SetTriggers(this.listAnimationOpenTrigger, this.listAnimationCloseTrigger);
// Hook a callback on the finish event
listComp.onAnimationFinish.AddListener(OnListAnimationFinish);
}
// Check if the navigation is disabled
if (this.navigation.mode == Navigation.Mode.None)
{
this.CreateBlocker(rootCanvas);
}
// If we are using a scroll rect invoke the list dimensions change
if (this.allowScrollRect && this.m_Options.Count >= this.scrollMinOptions)
{
this.ListDimensionsChanged();
}
}
protected virtual void CreateBlocker(Canvas rootCanvas)
{
// Create blocker GameObject.
GameObject blocker = new GameObject("Blocker");
// Setup blocker RectTransform to cover entire root canvas area.
RectTransform blockerRect = blocker.AddComponent<RectTransform>();
blockerRect.SetParent(rootCanvas.transform, false);
blockerRect.localScale = Vector3.one;
blockerRect.localPosition = Vector3.zero;
blockerRect.anchorMin = Vector3.zero;
blockerRect.anchorMax = Vector3.one;
blockerRect.sizeDelta = Vector2.zero;
// Add image since it's needed to block, but make it clear.
Image blockerImage = blocker.AddComponent<Image>();
blockerImage.color = Color.clear;
// Add button since it's needed to block, and to close the dropdown when blocking area is clicked.
Button blockerButton = blocker.AddComponent<Button>();
blockerButton.onClick.AddListener(Close);
// Make sure it's the top-most element
UIAlwaysOnTop aot = blocker.AddComponent<UIAlwaysOnTop>();
aot.order = UIAlwaysOnTop.SelectFieldBlockerOrder;
this.m_Blocker = blocker;
}
/// <summary>
/// Creates a option.
/// </summary>
/// <param name="index">Index.</param>
protected void CreateOption(int index, ToggleGroup toggleGroup)
{
if (this.m_ListContentObject == null)
return;
// Create the option game object with it's components
GameObject optionObject = new GameObject("Option " + index.ToString(), typeof(RectTransform));
optionObject.layer = this.gameObject.layer;
// Change parents
optionObject.transform.SetParent(this.m_ListContentObject.transform, false);
optionObject.transform.localScale = new Vector3(1f, 1f, 1f);
optionObject.transform.localPosition = Vector3.zero;
// Get the option component
UISelectField_Option optionComp = optionObject.AddComponent<UISelectField_Option>();
// Prepare the option background
if (this.optionBackgroundSprite != null)
{
Image image = optionObject.AddComponent<Image>();
image.sprite = this.optionBackgroundSprite;
image.type = this.optionBackgroundSpriteType;
image.color = this.optionBackgroundSpriteColor;
// Add the graphic as the option transition target
optionComp.targetGraphic = image;
}
// Prepare the option for animation
if (this.optionBackgroundTransitionType == Transition.Animation)
{
// Attach animator component
Animator animator = optionObject.AddComponent<Animator>();
// Set the animator controller
animator.runtimeAnimatorController = this.optionBackgroundAnimatorController;
}
// Apply the option padding
VerticalLayoutGroup vlg = optionObject.AddComponent<VerticalLayoutGroup>();
vlg.padding = this.optionPadding;
// Create the option text
GameObject textObject = new GameObject("Label", typeof(RectTransform));
// Change parents
textObject.transform.SetParent(optionObject.transform, false);
textObject.transform.localScale = Vector3.one;
textObject.transform.localPosition = Vector3.zero;
// Apply pivot
(textObject.transform as RectTransform).pivot = new Vector2(0f, 1f);
// Prepare the text
Text text = textObject.AddComponent<Text>();
text.font = this.optionFont;
text.fontSize = this.optionFontSize;
text.fontStyle = this.optionFontStyle;
text.color = this.optionColor;
if (this.m_Options != null)
text.text = this.m_Options[index];
// Apply normal state transition color
if (this.optionTextTransitionType == OptionTextTransitionType.CrossFade)
text.canvasRenderer.SetColor(this.optionTextTransitionColors.normalColor);
// Add and prepare the text effect
if (this.optionTextEffectType != OptionTextEffectType.None)
{
if (this.optionTextEffectType == OptionTextEffectType.Shadow)
{
Shadow effect = textObject.AddComponent<Shadow>();
effect.effectColor = this.optionTextEffectColor;
effect.effectDistance = this.optionTextEffectDistance;
effect.useGraphicAlpha = this.optionTextEffectUseGraphicAlpha;
}
else if (this.optionTextEffectType == OptionTextEffectType.Outline)
{
Outline effect = textObject.AddComponent<Outline>();
effect.effectColor = this.optionTextEffectColor;
effect.effectDistance = this.optionTextEffectDistance;
effect.useGraphicAlpha = this.optionTextEffectUseGraphicAlpha;
}
}
// Prepare the option hover overlay
if (this.optionHoverOverlay != null)
{
GameObject hoverOverlayObj = new GameObject("Hover Overlay", typeof(RectTransform));
hoverOverlayObj.layer = this.gameObject.layer;
hoverOverlayObj.transform.localScale = Vector3.one;
hoverOverlayObj.transform.localPosition = Vector3.zero;
// Add layout element
LayoutElement hoverLayoutElement = hoverOverlayObj.AddComponent<LayoutElement>();
hoverLayoutElement.ignoreLayout = true;
// Change parents
hoverOverlayObj.transform.SetParent(optionObject.transform, false);
hoverOverlayObj.transform.localScale = new Vector3(1f, 1f, 1f);
// Add image
Image hoImage = hoverOverlayObj.AddComponent<Image>();
hoImage.sprite = this.optionHoverOverlay;
hoImage.color = this.optionHoverOverlayColor;
hoImage.type = Image.Type.Sliced;
// Apply pivot
(hoverOverlayObj.transform as RectTransform).pivot = new Vector2(0f, 1f);
// Apply anchors
(hoverOverlayObj.transform as RectTransform).anchorMin = new Vector2(0f, 0f);
(hoverOverlayObj.transform as RectTransform).anchorMax = new Vector2(1f, 1f);
// Apply offsets
(hoverOverlayObj.transform as RectTransform).offsetMin = new Vector2(0f, 0f);
(hoverOverlayObj.transform as RectTransform).offsetMax = new Vector2(0f, 0f);
// Add the highlight transition component
UISelectField_OptionOverlay hoht = optionObject.AddComponent<UISelectField_OptionOverlay>();
hoht.targetGraphic = hoImage;
hoht.transition = UISelectField_OptionOverlay.Transition.ColorTint;
hoht.colorBlock = this.optionHoverOverlayColorBlock;
hoht.InternalEvaluateAndTransitionToNormalState(true);
}
// Prepare the option press overlay
if (this.optionPressOverlay != null)
{
GameObject pressOverlayObj = new GameObject("Press Overlay", typeof(RectTransform));
pressOverlayObj.layer = this.gameObject.layer;
pressOverlayObj.transform.localScale = Vector3.one;
pressOverlayObj.transform.localPosition = Vector3.zero;
// Add layout element
LayoutElement pressLayoutElement = pressOverlayObj.AddComponent<LayoutElement>();
pressLayoutElement.ignoreLayout = true;
// Change parents
pressOverlayObj.transform.SetParent(optionObject.transform, false);
pressOverlayObj.transform.localScale = new Vector3(1f, 1f, 1f);
// Add image
Image poImage = pressOverlayObj.AddComponent<Image>();
poImage.sprite = this.optionPressOverlay;
poImage.color = this.optionPressOverlayColor;
poImage.type = Image.Type.Sliced;
// Apply pivot
(pressOverlayObj.transform as RectTransform).pivot = new Vector2(0f, 1f);
// Apply anchors
(pressOverlayObj.transform as RectTransform).anchorMin = new Vector2(0f, 0f);
(pressOverlayObj.transform as RectTransform).anchorMax = new Vector2(1f, 1f);
// Apply offsets
(pressOverlayObj.transform as RectTransform).offsetMin = new Vector2(0f, 0f);
(pressOverlayObj.transform as RectTransform).offsetMax = new Vector2(0f, 0f);
// Add the highlight transition component
UISelectField_OptionOverlay poht = optionObject.AddComponent<UISelectField_OptionOverlay>();
poht.targetGraphic = poImage;
poht.transition = UISelectField_OptionOverlay.Transition.ColorTint;
poht.colorBlock = this.optionPressOverlayColorBlock;
poht.InternalEvaluateAndTransitionToNormalState(true);
}
// Initialize the option component
optionComp.Initialize(this, text);
// Set active if it's the selected one
if (index == this.selectedOptionIndex)
optionComp.isOn = true;
// Register to the toggle group
if (toggleGroup != null)
optionComp.group = toggleGroup;
// Hook some events
optionComp.onSelectOption.AddListener(OnOptionSelect);
optionComp.onPointerUp.AddListener(OnOptionPointerUp);
// Add it to the list
if (this.m_OptionObjects != null)
this.m_OptionObjects.Add(optionComp);
}
/// <summary>
/// Creates a separator.
/// </summary>
/// <param name="index">Index.</param>
/// <returns>The separator game object.</returns>
protected GameObject CreateSeparator(int index)
{
if (this.m_ListContentObject == null || this.listSeparatorSprite == null)
return null;
GameObject separatorObject = new GameObject("Separator " + index.ToString(), typeof(RectTransform));
// Change parent
separatorObject.transform.SetParent(this.m_ListContentObject.transform, false);
separatorObject.transform.localScale = Vector3.one;
separatorObject.transform.localPosition = Vector3.zero;
// Apply the sprite
Image image = separatorObject.AddComponent<Image>();
image.sprite = this.listSeparatorSprite;
image.type = this.listSeparatorType;
image.color = this.listSeparatorColor;
// Apply preferred height
LayoutElement le = separatorObject.AddComponent<LayoutElement>();
le.preferredHeight = (this.listSeparatorHeight > 0f) ? this.listSeparatorHeight : this.listSeparatorSprite.rect.height;
return separatorObject;
}
/// <summary>
/// Does a list cleanup (Destroys the list and clears the option objects list).
/// </summary>
protected virtual void ListCleanup()
{
if (this.m_ListObject != null)
Destroy(this.m_ListObject);
this.m_OptionObjects.Clear();
}
/// <summary>
/// Positions the list for the given direction (Auto is not handled in this method).
/// </summary>
/// <param name="direction">Direction.</param>
public virtual void PositionListForDirection(Direction direction)
{
// Make sure the creating of the list was successful
if (this.m_ListObject == null)
return;
// Get the list rect transforms
RectTransform listRect = (this.m_ListObject.transform as RectTransform);
// Determine the direction of the pop
if (direction == Direction.Auto)
{
// Get the list world corners
Vector3[] listWorldCorner = new Vector3[4];
listRect.GetWorldCorners(listWorldCorner);
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(Camera.main, listWorldCorner[0]);
// Check if the list is going outside to the bottom
if (screenPoint.y < 0f)
{
direction = Direction.Up;
}
else
{
direction = Direction.Down;
}
}
// Handle up or down direction
if (direction == Direction.Down)
{
listRect.SetParent(this.transform, true);
listRect.pivot = new Vector2(0f, 1f);
listRect.anchorMin = new Vector2(0f, 0f);
listRect.anchorMax = new Vector2(0f, 0f);
listRect.anchoredPosition = new Vector2(listRect.anchoredPosition.x, this.listMargins.top * -1f);
UIUtility.BringToFront(listRect.gameObject);
}
else
{
listRect.SetParent(this.transform, true);
listRect.pivot = new Vector2(0f, 0f);
listRect.anchorMin = new Vector2(0f, 1f);
listRect.anchorMax = new Vector2(0f, 1f);
listRect.anchoredPosition = new Vector2(listRect.anchoredPosition.x, this.listMargins.bottom);
if (this.m_StartSeparatorObject != null)
this.m_StartSeparatorObject.transform.SetAsLastSibling();
UIUtility.BringToFront(listRect.gameObject);
}
}
/// <summary>
/// Event invoked when the list dimensions change.
/// </summary>
protected virtual void ListDimensionsChanged()
{
if (!this.IsActive() || this.m_ListObject == null)
return;
// Check if the list size has changed
if (this.m_LastListSize.Equals((this.m_ListObject.transform as RectTransform).sizeDelta))
return;
// Update the last list size
this.m_LastListSize = (this.m_ListObject.transform as RectTransform).sizeDelta;
// Update the list direction
this.PositionListForDirection(this.m_Direction);
}
/// <summary>
/// Event invoked when the scroll rect content dimensions change.
/// </summary>
protected virtual void ScrollContentDimensionsChanged()
{
if (!this.IsActive() || this.m_ScrollRect == null)
return;
float contentHeight = (this.m_ScrollRect.content as RectTransform).sizeDelta.y;
float optionHeight = contentHeight / (float)this.m_Options.Count;
float optionPosition = optionHeight * (float)this.selectedOptionIndex;
this.m_ScrollRect.content.anchoredPosition = new Vector2(this.m_ScrollRect.content.anchoredPosition.x, optionPosition);
}
/// <summary>
/// Event invoked when the option list changes.
/// </summary>
protected virtual void OptionListChanged() { }
/// <summary>
/// Tweens the list alpha.
/// </summary>
/// <param name="targetAlpha">Target alpha.</param>
/// <param name="duration">Duration.</param>
/// <param name="ignoreTimeScale">If set to <c>true</c> ignore time scale.</param>
private void TweenListAlpha(float targetAlpha, float duration, bool ignoreTimeScale)
{
if (this.m_ListCanvasGroup == null)
return;
float currentAlpha = this.m_ListCanvasGroup.alpha;
if (currentAlpha.Equals(targetAlpha))
return;
var floatTween = new FloatTween { duration = duration, startFloat = currentAlpha, targetFloat = targetAlpha };
floatTween.AddOnChangedCallback(SetListAlpha);
floatTween.AddOnFinishCallback(OnListTweenFinished);
floatTween.ignoreTimeScale = ignoreTimeScale;
this.m_FloatTweenRunner.StartTween(floatTween);
}
/// <summary>
/// Sets the list alpha.
/// </summary>
/// <param name="alpha">Alpha.</param>
private void SetListAlpha(float alpha)
{
if (this.m_ListCanvasGroup == null)
return;
// Set the alpha
this.m_ListCanvasGroup.alpha = alpha;
}
/// <summary>
/// Triggers the list animation.
/// </summary>
/// <param name="trigger">Trigger.</param>
private void TriggerListAnimation(string trigger)
{
if (this.m_ListObject == null || string.IsNullOrEmpty(trigger))
return;
Animator animator = this.m_ListObject.GetComponent<Animator>();
if (animator == null || !animator.enabled || !animator.isActiveAndEnabled || animator.runtimeAnimatorController == null || !animator.hasBoundPlayables)
return;
animator.ResetTrigger(this.listAnimationOpenTrigger);
animator.ResetTrigger(this.listAnimationCloseTrigger);
animator.SetTrigger(trigger);
}
/// <summary>
/// Raises the list tween finished event.
/// </summary>
protected virtual void OnListTweenFinished()
{
// If the list is closed do a cleanup
if (!this.IsOpen)
this.ListCleanup();
}
/// <summary>
/// Raises the list animation finish event.
/// </summary>
/// <param name="state">State.</param>
protected virtual void OnListAnimationFinish(UISelectField_List.State state)
{
// If the list is closed do a cleanup
if (state == UISelectField_List.State.Closed && !this.IsOpen)
this.ListCleanup();
}
}
}