/// Credit David Gileadi /// Sourced from - https://bitbucket.org/UnityUIExtensions/unity-ui-extensions/pull-requests/11 using System; using UnityEngine.Events; using UnityEngine.EventSystems; namespace UnityEngine.UI.Extensions { // Stepper control [AddComponentMenu("UI/Extensions/Stepper")] [RequireComponent(typeof(RectTransform))] public class Stepper : UIBehaviour { private Selectable[] _sides; [SerializeField] [Tooltip("The current step value of the control")] private int _value = 0; [SerializeField] [Tooltip("The minimum step value allowed by the control. When reached it will disable the '-' button")] private int _minimum = 0; [SerializeField] [Tooltip("The maximum step value allowed by the control. When reached it will disable the '+' button")] private int _maximum = 100; [SerializeField] [Tooltip("The step increment used to increment / decrement the step value")] private int _step = 1; [SerializeField] [Tooltip("Does the step value loop around from end to end")] private bool _wrap = false; [SerializeField] [Tooltip("A GameObject with an Image to use as a separator between segments. Size of the RectTransform will determine the size of the separator used.\nNote, make sure to disable the separator GO so that it does not affect the scene")] private Graphic _separator; private float _separatorWidth = 0; private float separatorWidth { get { if (_separatorWidth == 0 && separator) { _separatorWidth = separator.rectTransform.rect.width; var image = separator.GetComponent(); if (image) _separatorWidth /= image.pixelsPerUnit; } return _separatorWidth; } } // Event delegates triggered on click. [SerializeField] private StepperValueChangedEvent _onValueChanged = new StepperValueChangedEvent(); [Serializable] public class StepperValueChangedEvent : UnityEvent { } public Selectable[] sides { get { if (_sides == null || _sides.Length == 0) { _sides = GetSides(); } return _sides; } } public int value { get { return _value; } set { _value = value; } } public int minimum { get { return _minimum; } set { _minimum = value; } } public int maximum { get { return _maximum; } set { _maximum = value; } } public int step { get { return _step; } set { _step = value; } } public bool wrap { get { return _wrap; } set { _wrap = value; } } public Graphic separator { get { return _separator; } set { _separator = value; _separatorWidth = 0; LayoutSides(sides); } } public StepperValueChangedEvent onValueChanged { get { return _onValueChanged; } set { _onValueChanged = value; } } protected Stepper() { } #if UNITY_EDITOR protected override void OnValidate() { base.OnValidate(); RecreateSprites(sides); if (separator) LayoutSides(); if (!wrap) { DisableAtExtremes(sides); } } #endif private Selectable[] GetSides() { var buttons = GetComponentsInChildren(); if (buttons.Length != 2) { throw new InvalidOperationException("A stepper must have two Button children"); } for (int i = 0; i < 2; i++) { var side = buttons[i].GetComponent(); if (side == null) { side = buttons[i].gameObject.AddComponent(); } } if (!wrap) { DisableAtExtremes(buttons); } LayoutSides(buttons); return buttons; } public void StepUp() { Step(step); } public void StepDown() { Step(-step); } private void Step(int amount) { value += amount; if (wrap) { if (value > maximum) value = minimum; if (value < minimum) value = maximum; } else { value = Math.Max(minimum, value); value = Math.Min(maximum, value); DisableAtExtremes(sides); } _onValueChanged.Invoke(value); } private void DisableAtExtremes(Selectable[] sides) { sides[0].interactable = wrap || value > minimum; sides[1].interactable = wrap || value < maximum; } private void RecreateSprites(Selectable[] sides) { for (int i = 0; i < 2; i++) { if (sides[i].image == null) continue; var sprite = CutSprite(sides[i].image.sprite, i == 0); var side = sides[i].GetComponent(); if (side) { side.cutSprite = sprite; } sides[i].image.overrideSprite = sprite; } } static internal Sprite CutSprite(Sprite sprite, bool leftmost) { if (sprite.border.x == 0 || sprite.border.z == 0) return sprite; var rect = sprite.rect; var border = sprite.border; if (leftmost) { rect.xMax = border.z; border.z = 0; } else { rect.xMin = border.x; border.x = 0; } return Sprite.Create(sprite.texture, rect, sprite.pivot, sprite.pixelsPerUnit, 0, SpriteMeshType.FullRect, border); } public void LayoutSides(Selectable[] sides = null) { sides = sides ?? this.sides; RecreateSprites(sides); RectTransform transform = this.transform as RectTransform; float width = (transform.rect.width / 2) - separatorWidth; for (int i = 0; i < 2; i++) { float insetX = i == 0 ? 0 : width + separatorWidth; var rectTransform = sides[i].GetComponent(); rectTransform.anchorMin = Vector2.zero; rectTransform.anchorMax = Vector2.zero; rectTransform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, insetX, width); rectTransform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, transform.rect.height); // TODO: maybe adjust text position } if (separator) { var sepTransform = gameObject.transform.Find("Separator"); Graphic sep = (sepTransform != null) ? sepTransform.GetComponent() : (GameObject.Instantiate(separator.gameObject) as GameObject).GetComponent(); sep.gameObject.name = "Separator"; sep.gameObject.SetActive(true); sep.rectTransform.SetParent(this.transform, false); sep.rectTransform.anchorMin = Vector2.zero; sep.rectTransform.anchorMax = Vector2.zero; sep.rectTransform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, width, separatorWidth); sep.rectTransform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, transform.rect.height); } } } [RequireComponent(typeof(Selectable))] public class StepperSide : UIBehaviour, IPointerClickHandler, ISubmitHandler, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler, ISelectHandler, IDeselectHandler { Selectable button { get { return GetComponent(); } } Stepper stepper { get { return GetComponentInParent(); } } bool leftmost { get { return button == stepper.sides[0]; } } internal Sprite cutSprite; protected StepperSide() { } public virtual void OnPointerClick(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; Press(); AdjustSprite(false); } public virtual void OnSubmit(BaseEventData eventData) { Press(); AdjustSprite(true); } public virtual void OnPointerEnter(PointerEventData eventData) { AdjustSprite(false); } public virtual void OnPointerExit(PointerEventData eventData) { AdjustSprite(true); } public virtual void OnPointerDown(PointerEventData eventData) { AdjustSprite(false); } public virtual void OnPointerUp(PointerEventData eventData) { AdjustSprite(false); } public virtual void OnSelect(BaseEventData eventData) { AdjustSprite(false); } public virtual void OnDeselect(BaseEventData eventData) { AdjustSprite(true); } private void Press() { if (!button.IsActive() || !button.IsInteractable()) return; if (leftmost) { stepper.StepDown(); } else { stepper.StepUp(); } } private void AdjustSprite(bool restore) { var image = button.image; if (!image || image.overrideSprite == cutSprite) return; if (restore) image.overrideSprite = cutSprite; else image.overrideSprite = Stepper.CutSprite(image.overrideSprite, leftmost); } } }