Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

best way to change Button label color along with button sprite?

Discussion in 'UGUI & TextMesh Pro' started by JoeStrout, Feb 28, 2017.

  1. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    This is driving me nuts.

    I very often have buttons that, when highlighted or pressed, change their sprites to something of a substantially different color. So to keep sufficient contrast, I have to change the label color too. (Example: normal button state is a dark gray button with white text; when pressed, this should be a bright yellow button with black text.)

    I have hacked out many different solutions over the years, and hate all of them:
    • replace Button with my own EventTrigger script (but doesn't play nicely with UI navigation)
    • bake the text into the sprite (not practical when I have dozens of buttons that differ only in their text)
    • write a script to watch the button state and update the label (works, but seems hackish)
    But actually that last one seems like it's headed in the right direction. Now I'd just like to make it more general. It seems like I could specify a public ColorBlock on my label-updater script, that specifies the colors that should be applied when the button changes state. But it's not clear exactly how to apply them (the math is nonobvious and undocumented as far as I know, and ColorBlock lacks any methods to do this math for you).

    Then it occurred to me, maybe the label itself should be Selectable, but not a raycast target. And then we'd just need to somehow tie the selection state of the label to that of the button. Assuming this doesn't cause the selection/navigation system to freak out (trying to have two things selected at once).

    This seems like it must be such a common problem... is there some good solution I'm just overlooking?
     
  2. keenanwoodall

    keenanwoodall

    Joined:
    May 30, 2014
    Posts:
    597
    You could create a class that inherits from Button and drag the script onto GameObjects instead of the standard Button script.
     
  3. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, that's one of the things I've tried. But there appear to be no methods one can override to get notified when it is pressed, and released. So instead I have to do something like

    Code (CSharp):
    1.     void Update() {
    2.         if (IsPressed()) {
    3.             label.color = Color.black;
    4.         } else {
    5.             label.color = Color.yellow;
    6.         }
    7.     }
    which works, but isn't very general. This is where I started thinking I could have a public ColorBlock on this script, which defines colors to apply under various conditions. That is probably the way to go, though it's still not clear exactly what the math is to reproduce what Unity does.
     
  4. keenanwoodall

    keenanwoodall

    Joined:
    May 30, 2014
    Posts:
    597
    You could also create a completely custom button, it wouldn't actually be that hard. I wrote a script that receives input the same way default buttons do. I think this might be a good starting point:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4. using UnityEngine.Events;
    5. using UnityEngine.EventSystems;
    6.  
    7.  
    8. public class PointerEventHandler : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
    9. {
    10.     public bool debug;
    11.     public UnityEvent
    12.         onPointerClick,
    13.         onPointerDown,
    14.         onPointerUp,
    15.         onPointerEnter,
    16.         onPointerExit;
    17.  
    18.     public void OnPointerClick (PointerEventData eventData)
    19.     {
    20.         onPointerClick.Invoke ();
    21.         if (debug)
    22.             Debug.Log ("Clicked " + name);
    23.     }
    24.  
    25.     public void OnPointerUp (PointerEventData eventData)
    26.     {
    27.         onPointerUp.Invoke ();
    28.         if (debug)
    29.             Debug.Log ("Released " + name);
    30.     }
    31.  
    32.     public void OnPointerDown (PointerEventData eventData)
    33.     {
    34.         onPointerDown.Invoke ();
    35.         if (debug)
    36.             Debug.Log ("Pressed " + name);
    37.     }
    38.  
    39.     public void OnPointerEnter (PointerEventData eventData)
    40.     {
    41.         onPointerEnter.Invoke ();
    42.         if (debug)
    43.             Debug.Log ("Entered " + name);
    44.     }
    45.  
    46.     public void OnPointerExit (PointerEventData eventData)
    47.     {
    48.         onPointerExit.Invoke ();
    49.         if (debug)
    50.             Debug.Log ("Exited " + name);
    51.     }
    52. }
    53.  
    Just rename it to AdvanceButton (or whatever) instead of PointerEventHandler. Then add variables holding graphics and modify them as you see fit; from within the OnPointerX methods.
     
  5. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yeah, I have scripts like that too, but they don't play nicely with Unity's UI navigation system. For High Frontier, I don't care, but now I have a project that will be targeting consoles, so it's important that people be able to navigate the UI with a gamepad.

    Your script above won't work with that either, will it?
     
  6. keenanwoodall

    keenanwoodall

    Joined:
    May 30, 2014
    Posts:
    597
    I'm actually glad you asked this question because it reminded me that Unitys UI is open source. You don't have to inherit from Button, you can just copy the button code and make whatever changes you like! I grabbed the code and added more advanced events.
    [EDIT] And because it inherits from the same stuff a Unity Button does, it should support gamepad navigation!
    [EDIT] Added events for selecting and deselecting.
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. using UnityEngine.EventSystems;
    6.  
    7. namespace AdvancedUI
    8. {
    9.     [AddComponentMenu ("UI/Advanced Button")]
    10.     public class AdvancedButton : Selectable, ISubmitHandler, ISelectHandler, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler
    11.     {
    12.         #region Fields
    13.  
    14.         public bool debug = true;
    15.  
    16.         #endregion
    17.  
    18.  
    19.  
    20.         #region Properties
    21.  
    22.         [SerializeField] private InteractableEvents events;
    23.         public InteractableEvents Events
    24.         {
    25.             get { return events; }
    26.             set { events = value; }
    27.         }
    28.  
    29.         #endregion
    30.  
    31.  
    32.  
    33.         #region Main Methods
    34.  
    35.         public void Press ()
    36.         {
    37.             if (!IsActive () || !IsInteractable ())
    38.                 return;
    39.  
    40.             if (debug)
    41.                 Debug.Log ("Pressed");
    42.         }
    43.  
    44.         #endregion
    45.  
    46.         #region Inherited Methods
    47.  
    48.         public void OnSubmit (BaseEventData eventData)
    49.         {
    50.             DoStateTransition (SelectionState.Pressed, false);
    51.             StartCoroutine (FinishSubmit ());
    52.  
    53.             if (debug)
    54.                 Debug.Log ("Submitted");
    55.         }
    56.  
    57.         public override void OnSelect (BaseEventData eventData)
    58.         {
    59.             base.OnSelect (eventData);
    60.             events.onSelect.Invoke (eventData);
    61.  
    62.             if (debug)
    63.                 Debug.Log ("Selected");
    64.         }
    65.         public override void OnDeselect (BaseEventData eventData)
    66.         {
    67.             base.OnDeselect (eventData);
    68.             events.onDeselect.Invoke (eventData);
    69.  
    70.             if (debug)
    71.                 Debug.Log ("Deselected");
    72.         }
    73.  
    74.         public void OnPointerClick (PointerEventData eventData)
    75.         {
    76.             events.onPointerClick.Invoke (eventData);
    77.  
    78.             if (debug)
    79.                 Debug.Log ("Clicked");
    80.         }
    81.         public override void OnPointerDown (PointerEventData eventData)
    82.         {
    83.             base.OnPointerDown (eventData);
    84.             events.onPointerDown.Invoke (eventData);
    85.  
    86.             if (debug)
    87.                 Debug.Log ("Pointer down");
    88.         }
    89.         public override void OnPointerUp (PointerEventData eventData)
    90.         {
    91.             base.OnPointerUp (eventData);
    92.             events.onPointerUp.Invoke (eventData);
    93.  
    94.             if (debug)
    95.                 Debug.Log ("Pointer up");
    96.         }
    97.         public override void OnPointerEnter (PointerEventData eventData)
    98.         {
    99.             base.OnPointerEnter (eventData);
    100.             events.onPointerEnter.Invoke (eventData);
    101.  
    102.             if (debug)
    103.                 Debug.Log ("Pointer enter");
    104.         }
    105.         public override void OnPointerExit (PointerEventData eventData)
    106.         {
    107.             base.OnPointerExit (eventData);
    108.             events.onPointerExit.Invoke (eventData);
    109.  
    110.             if (debug)
    111.                 Debug.Log ("Pointer exit");
    112.         }
    113.  
    114.         #endregion
    115.  
    116.  
    117.  
    118.         #region Coroutines
    119.  
    120.         private IEnumerator FinishSubmit ()
    121.         {
    122.             float
    123.                 fadeTime = colors.fadeDuration,
    124.                 elapsedTime = 0f;
    125.  
    126.             while (elapsedTime < fadeTime)
    127.             {
    128.                 elapsedTime += Time.unscaledDeltaTime;
    129.                 yield return null;
    130.             }
    131.  
    132.             DoStateTransition (currentSelectionState, false);
    133.         }
    134.  
    135.         #endregion
    136.     }
    137. }
    138.  
    Here's a class that holds all the extra events
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System;
    4.  
    5. namespace AdvancedUI
    6. {
    7.  
    8.     [Serializable]
    9.     public class BaseEventDataEvent : UnityEvent<BaseEventData> { }
    10.     [Serializable]
    11.     public class PointerEventDataEvent : UnityEvent<PointerEventData> { }
    12.  
    13.     [Serializable]
    14.     public class InteractableEvents
    15.     {
    16.         public BaseEventDataEvent
    17.             onSubmit                = new BaseEventDataEvent (),
    18.             onSelect                = new BaseEventDataEvent (),
    19.             onDeselect                = new BaseEventDataEvent ();
    20.         public PointerEventDataEvent
    21.             onPointerClick            = new PointerEventDataEvent (),
    22.             onPointerDown            = new PointerEventDataEvent (),
    23.             onPointerUp                = new PointerEventDataEvent (),
    24.             onPointerEnter            = new PointerEventDataEvent (),
    25.             onPointerExit            = new PointerEventDataEvent ();
    26.     }
    27.  
    And here's the original Button code written by Unity
     
    Last edited: Mar 1, 2017
    JoeStrout likes this.
  7. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    Rather than fiddle with code, what about changing the button's transition type to Animation? Then add animations that change the sprite's color (or image even) as well as the label's color. Seems much easier, and it doesn't require you to write and maintain custom code.
     
    keenanwoodall likes this.
  8. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    This is in fact exactly what we ended up doing on this project. The designer wanted some other effects on button push that pretty much required animation anyway, so having done that, then yeah, it was no big deal to animate the text color too.

    I don't know about "easier" though — I find animations harder to set up and maintain than code, for simple things at least. But there certainly is a threshold where the animation editor becomes worth the trouble.

    But for simple cases, I'm still annoyed that the built-in Button class doesn't expose events for when a button is pressed or highlighted (and @keenanwoodall's can't either — it turns out to be a real pain to discover when something is highlighted, short of using an animation or polling in a loop). Something that should be simple like playing a rollover sound is hard, unless you've jumped all-in with the Animation transition type already.

    But, enough... climbing down from my soapbox now. It's chilly up there. :)
     
  9. haruna9x

    haruna9x

    Joined:
    Jul 28, 2016
    Posts:
    10
    I am having the same problem and have taken some time searching for a solution. Although the topic is old, I want to write it down here, for someone to come later:

    I have inherited from Button class to not break UGUI system:

    Code (CSharp):
    1.  
    2. namespace UnityEngine.UI
    3. {
    4.     [System.Serializable]
    5.     public enum SelectableState
    6.     {
    7.         //
    8.         // Summary:
    9.         //     The UI object can be selected.
    10.         Normal = 0,
    11.         //
    12.         // Summary:
    13.         //     The UI object is highlighted.
    14.         Highlighted = 1,
    15.         //
    16.         // Summary:
    17.         //     The UI object is pressed.
    18.         Pressed = 2,
    19.         //
    20.         // Summary:
    21.         //     The UI object cannot be selected.
    22.         Disabled = 3
    23.     }
    24.    
    25.     [System.Serializable]
    26.     public class UnityEventSelectionState : UnityEvent<UI.SelectableState, bool> { }
    27.  
    28.     [AddComponentMenu("UI/Button Extension", 31)]
    29.     public class ButtonExtension : Button
    30.     {
    31.         [SerializeField] private UnityEventSelectionState m_OnStateChanged;
    32.  
    33.         public UnityEventSelectionState onStateChanged { get { return m_OnStateChanged; } }
    34.  
    35.         protected override void DoStateTransition(SelectionState state, bool instant)
    36.         {
    37.             base.DoStateTransition(state, instant);
    38.  
    39.             switch (state)
    40.             {
    41.                 case SelectionState.Normal:
    42.                     m_OnStateChanged?.Invoke(SelectableState.Normal, instant);
    43.                     break;
    44.                 case SelectionState.Highlighted:
    45.                     m_OnStateChanged?.Invoke(SelectableState.Highlighted, instant);
    46.                     break;
    47.                 case SelectionState.Pressed:
    48.                     m_OnStateChanged?.Invoke(SelectableState.Pressed, instant);
    49.                     break;
    50.                 case SelectionState.Disabled:
    51.                     m_OnStateChanged?.Invoke(SelectableState.Disabled, instant);
    52.                     break;
    53.             }
    54.         }
    55.     }
    56. }
    57.  

    Add a custom inspector.

    Code (CSharp):
    1.  
    2. namespace UnityEditor.UI
    3. {
    4.     [CustomEditor(typeof(ButtonExtension), true)]
    5.     [CanEditMultipleObjects]
    6.     /// <summary>
    7.     ///   Custom Editor for the Button Extension Component.
    8.     ///   Extend this class to write a custom editor for an Button-derived component.
    9.     /// </summary>
    10.     public class ButtonExtensionEditor : ButtonEditor
    11.     {
    12.         SerializedProperty m_OnStateChangedProperty;
    13.  
    14.         protected override void OnEnable()
    15.         {
    16.             base.OnEnable();
    17.             m_OnStateChangedProperty = serializedObject.FindProperty("m_OnStateChanged");
    18.         }
    19.  
    20.         public override void OnInspectorGUI()
    21.         {
    22.             base.OnInspectorGUI();
    23.             EditorGUILayout.Space();
    24.  
    25.             serializedObject.Update();
    26.             EditorGUILayout.PropertyField(m_OnStateChangedProperty);
    27.             serializedObject.ApplyModifiedProperties();
    28.         }
    29.     }
    30. }
    31.  
    Now I have the state of a button. Just do something with it.
    Code (CSharp):
    1.  
    2. namespace UnityEngine.UI
    3. {
    4.     [ExecuteInEditMode()]
    5.     [RequireComponent(typeof(ButtonExtension))]
    6.     public class ButtonExtensionColor : MonoBehaviour
    7.     {
    8.         public Graphic m_TargetGraphic;
    9.         private ButtonExtension m_Button;
    10.  
    11.         private void Awake()
    12.         {
    13.             m_Button = GetComponent<ButtonExtension>();
    14.  
    15.             m_Button.onStateChanged.AddListener(OnStateChange);
    16.         }
    17.  
    18.         private void OnDestroy()
    19.         {
    20.             m_Button.onStateChanged.RemoveListener(OnStateChange);
    21.         }
    22.  
    23.         private void OnStateChange(SelectableState selectionState, bool instant)
    24.         {
    25.             Color tintColor;
    26.  
    27.             switch (selectionState)
    28.             {
    29.                 case SelectableState.Normal:
    30.                     tintColor = m_Button.colors.normalColor;
    31.                     break;
    32.                 case SelectableState.Highlighted:
    33.                     tintColor = m_Button.colors.highlightedColor;
    34.                     break;
    35.                 case SelectableState.Pressed:
    36.                     tintColor = m_Button.colors.pressedColor;
    37.                     break;
    38.                 case SelectableState.Disabled:
    39.                     tintColor = m_Button.colors.disabledColor;
    40.                     break;
    41.                 default:
    42.                     tintColor = Color.black;
    43.                     break;
    44.             }
    45.  
    46.             if (m_TargetGraphic == null)
    47.                 return;
    48.  
    49.             m_TargetGraphic.CrossFadeColor(tintColor, instant ? 0f : m_Button.colors.fadeDuration, true, true);
    50.         }
    51.     }
    52. }
    53.  
     
    guneyozsan likes this.
  10. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    Necroing this again because I had to do the same thing today. Here's a much simpler solution that doesn't require scripting or animation. Set the button's transition to Color Tint and tint the button label text. Add an Event Trigger and add Pointer Enter, Pointer Exit, Select, and Deselect events to set the button sprite.

    upload_2019-5-27_16-21-37.png
     
  11. danijmn

    danijmn

    Joined:
    Dec 10, 2012
    Posts:
    9
    For anyone bumping into this in the future, the most consistent (and effortless) solution is simply to set the Text's color to the same value as the color of the button's CanvasRenderer:

    Code (CSharp):
    1. buttonText.color = buttonCanvasRenderer.GetColor();
    This already takes into account all the possible states (and is always up to date) because you're fetching the color directly from the renderer instead of the Button.
     
    Last edited: Sep 1, 2020
    silentslack and djweaver like this.
  12. tontonpiero

    tontonpiero

    Joined:
    Dec 28, 2013
    Posts:
    6
    My solution is to check the property 'overrideSprite' of the Image component in Update().
    My final script allows to set the desired color on the Text (or TexMeshPro) component when the sprite is changing.

    Code (CSharp):
    1. using TMPro;
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4.  
    5. [ExecuteInEditMode]
    6. [RequireComponent(typeof(Button))]
    7. public class ButtonStateColorText : MonoBehaviour
    8. {
    9.     private Button button;
    10.     public TMP_Text TextMesh;
    11.     public Text Text;
    12.  
    13.     private Image image;
    14.     private Sprite previousSprite = null;
    15.  
    16.     public Color NormalTextColor = Color.magenta;
    17.     public Color PressedTextColor = Color.magenta;
    18.     public Color HighlightedTextColor = Color.magenta;
    19.     public Color DisabledTextColor = Color.grey;
    20.  
    21.     private void Awake()
    22.     {
    23.         button = GetComponent<Button>();
    24.         TextMesh = GetComponentInChildren<TMP_Text>();
    25.         Text = GetComponentInChildren<Text>();
    26.         image = button.targetGraphic as Image;
    27.  
    28.         if (NormalTextColor == Color.magenta) NormalTextColor = GetTextColor();
    29.         if (PressedTextColor == Color.magenta) PressedTextColor = GetTextColor();
    30.         if (HighlightedTextColor == Color.magenta) HighlightedTextColor = GetTextColor();
    31.     }
    32.  
    33.     private void Update()
    34.     {
    35.         if (image.overrideSprite != previousSprite)
    36.         {
    37.             previousSprite = image.overrideSprite;
    38.             UpdateTextColor();
    39.         }
    40.     }
    41.  
    42.     private void UpdateTextColor()
    43.     {
    44.         if (image.overrideSprite == button.spriteState.pressedSprite)
    45.         {
    46.             SetTextColor(PressedTextColor);
    47.         }
    48.         else if (image.overrideSprite == button.spriteState.highlightedSprite)
    49.         {
    50.             SetTextColor(HighlightedTextColor);
    51.         }
    52.         else if (image.overrideSprite == button.spriteState.disabledSprite)
    53.         {
    54.             SetTextColor(DisabledTextColor);
    55.         }
    56.         else
    57.         {
    58.             SetTextColor(NormalTextColor);
    59.         }
    60.     }
    61.  
    62.     private Color GetTextColor()
    63.     {
    64.         if (TextMesh != null) return TextMesh.color;
    65.         if (Text != null) return Text.color;
    66.         return Color.black;
    67.     }
    68.  
    69.     private void SetTextColor(Color color)
    70.     {
    71.         if (TextMesh != null) TextMesh.color = color;
    72.         if (Text != null) Text.color = color;
    73.     }
    74. }
     
    mitaywalle likes this.
  13. Epsilone

    Epsilone

    Joined:
    Feb 1, 2016
    Posts:
    10
    I haven't heard of Event Trigger before, but it is so useful! Thank you TonyLi!
     
    TonyLi likes this.