- Home /
[UI] How to best set up connected/dependent sliders(or other user input oriented UI elements)?
What would be the best way to set up and X number of code instantiated sliders that add up to a shared total? E.g. Slider A is on 100%, Slider B and C are on 0%. Moving Slider A to 90% would result in Sliders B and C increasing to 5% each.
Simply adding a listener onValueChanged and controling the slider.value through code doesn't quite work because any change of a slider's value invokes onValueChanged in an infinite loop.
I thought about using IPointerUpHandler and IPointerDownHandler to subscribe/unsubscribe listeners to onValueChanged in order to prevent the infinite loop, but it seems kinda hacky. E.g. onValueChanged listeners are subscribed-->user modifies a slider by clicking on it-->OnPointerDown unsubscribes all the onValueChanged listeners of sliders except the slider that has been clicked-->OnPointerUp subscribes the listeners of the aforementioned sliders.
Using IpointerUpHandler to determine which slider has been modified by detecting which UI element is under the mouse seems finicky at best.
Answer by Vosheck · Jul 07, 2020 at 08:29 AM
I eventually made a script for this:
using System;
using UnityEngine.Events;
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class DependentUIElementManager : MonoBehaviour {
//NOTICE:UnityEventBase.RemoveAllListeners() will disrupt completely the functioning of this script. Please use
//RemoveListener() instead. There is an example of RemoveListener()'s use below.
#region Variables
//The combined max value of all the dependent sliders and/or input fields
[SerializeField]
float sharedTotal = 1f;
//If both sliders and input fields are used, then the correspondent UI element indices should be the same.
//E.g. The value in the 0th slidercorresponds to the value of the 0th inputField
[SerializeField]
Slider[] sliders;
[SerializeField]
InputField[] inputFields;
//Cached Actions for RemoveListener(), in order to prevent an infinite loop
UnityAction<float> onValueChangedListener;
UnityAction<string> onStringChangedListener;
private int indexOfTheUIElementBeingModified;
private bool allowIndexModification=true;
private List<int> orderOfLastModifiedSliders;
#endregion
#region Initialization
/// <summary>
/// Initializes the UnityActions and sets the starting values.
/// </summary>
/// <param name="initialValues"></param>
public void InitializeSetValues (float [] initialValues) {
onValueChangedListener += ModifyTheValuesOfAllDependentSliders;
onStringChangedListener += ModifyTheStringsOfAllDependentInputFields;
for (int i = 0; i < sliders.Length; i++) {
sliders[i].value = initialValues[i];
//Contemporance
int j = i;
sliders[i].onValueChanged.AddListener((value) => { SetTheIndexOfTheUIElementBeingModified(j); });
sliders[i].onValueChanged.AddListener(onValueChangedListener);
}
for (int i = 0; i < inputFields.Length; i++) {
inputFields[i].text = initialValues[i].ToString();
//Contemporance
int j = i;
inputFields[i].onValueChanged.AddListener((value) => { SetTheIndexOfTheUIElementBeingModified(j); });
inputFields[i].onValueChanged.AddListener(onStringChangedListener);
}
orderOfLastModifiedSliders = new List<int>();
for (int i = 0; i < sliders.Length; i++) {
orderOfLastModifiedSliders.Add(i);
}
orderOfLastModifiedSliders.Reverse();
}
[ContextMenu("Initialize Evenly Distributed")]
public void InitializeEvenlyDistributedValues() {
onValueChangedListener += ModifyTheValuesOfAllDependentSliders;
onStringChangedListener += ModifyTheStringsOfAllDependentInputFields;
for (int i = 0; i < sliders.Length; i++) {
sliders[i].value = sharedTotal / sliders.Length;
//Contemporance
int j = i;
sliders[i].onValueChanged.AddListener((value) => { SetTheIndexOfTheUIElementBeingModified(j); });
sliders[i].onValueChanged.AddListener(onValueChangedListener);
}
for (int i = 0; i < inputFields.Length; i++) {
inputFields[i].text = (sharedTotal/inputFields.Length).ToString();
//Contemporance
int j = i;
inputFields[i].onValueChanged.AddListener((value) => { SetTheIndexOfTheUIElementBeingModified(j); });
inputFields[i].onValueChanged.AddListener(onStringChangedListener);
}
}
private void CheckValidityOfInitialValues(float[] initialValues) {
if (sliders.Length != 0 && initialValues.Length != sliders.Length)
Debug.LogError("Error in initializing the initial values of dependent sliders. InitialValues length is: " + initialValues.Length +
" ,while the slider's array length is: " + sliders.Length);
else if (inputFields.Length != 0 && initialValues.Length != inputFields.Length)
Debug.LogError("Error in initializing the initial values of dependent inputFields. InitialValues length is: " + initialValues.Length +
" ,while the inputFields's array length is: " + inputFields.Length);
float initialValuesSum = 0f;
for (int i = 0; i < initialValues.Length; i++) {
initialValuesSum += initialValues[i];
}
if (initialValuesSum != sharedTotal)
Debug.LogError("Value mismatch in the initial values provided and the shared total of the dependent sliders. Initial value sum is: "+
initialValuesSum+" ,while the shared total is: "+sharedTotal);
}
public void ReinitializeArrays() {
sliders = new Slider[0];
inputFields = new InputField[0];
}
#endregion
#region Methods for adding sliders/input fields into the arrays
/// <summary>
/// Appends a slider to the array that holds the sliders whose values are interdependent
/// </summary>
/// <param name="slider"></param>
public void AddToDependentSliders(Slider slider) {
Array.Resize(ref sliders, sliders.Length + 1);
sliders[sliders.Length - 1] = slider;
}
/// <summary>
/// Appends an array of sliders to the array that holds the sliders whose values are interdependent
/// </summary>
/// <param name="slidersToAdd"></param>
public void AddToDependentSliders(Slider[] slidersToAdd) {
int initialLength = sliders.Length;
Array.Resize(ref sliders, sliders.Length + slidersToAdd.Length);
slidersToAdd.CopyTo(sliders, initialLength);
}
/// <summary>
/// Appends an input field to the array that holds the input fields whose values are interdependent
/// </summary>
/// <param name="inputField"></param>
public void AddToDependentInputField(InputField inputField) {
Array.Resize(ref inputFields, inputFields.Length + 1);
inputFields[inputFields.Length - 1] = inputField;
}
/// <summary>
/// Appends an array of input fields to the array that holds the input fields whose values are interdependent
/// </summary>
/// <param name="inputFieldsToAdd"></param>
public void AddToDependentInputField(InputField[] inputFieldsToAdd) {
int initialLength = inputFields.Length;
Array.Resize(ref inputFields, inputFields.Length + inputFieldsToAdd.Length);
inputFieldsToAdd.CopyTo(inputFields, initialLength);
}
#endregion
#region Listeners
/// <summary>
/// The listener method that controls the distribution of the total value throughout the dependent sliders
/// </summary>
/// <param name="newValue"></param>
public void ModifyTheValuesOfAllDependentSliders(float newValue) {
/*allowIndexModification = false;
float clampedValue = Mathf.Clamp(newValue,sliders[indexOfTheUIElementBeingModified].minValue, sliders[indexOfTheUIElementBeingModified].maxValue);
float valueOfAllDependentSliders = (sharedTotal - clampedValue) / (sliders.Length-1);
for (int i = 0; i < sliders.Length; i++) {
//For all sliders except the one being currently modified by the player
if (i!=indexOfTheUIElementBeingModified) {
//Removing the listener is important to prevent an infinite loop of onValueChanged
sliders[i].onValueChanged.RemoveListener(onValueChangedListener);
sliders[i].value = valueOfAllDependentSliders;
sliders[i].onValueChanged.AddListener(onValueChangedListener);
}
}
if (ShouldEqualizeSliderAndInputField())
EqualizeInputFieldsToSliders();
allowIndexModification = true;*/
allowIndexModification = false;
float clampedValue = Mathf.Clamp(newValue, sliders[indexOfTheUIElementBeingModified].minValue, sliders[indexOfTheUIElementBeingModified].maxValue);
float valueOfAllDependentSliders = (sharedTotal - clampedValue) / (sliders.Length - 1);
orderOfLastModifiedSliders.Remove(indexOfTheUIElementBeingModified);
orderOfLastModifiedSliders.Add(indexOfTheUIElementBeingModified);
float valueToRedistribute =sharedTotal - clampedValue - sumValueOfSlidersNotBeingModified();
//Debug.Log("value to redistribute: " + valueToRedistribute + "clamped value: " + clampedValue + "sum Of Not modified: " + sumValueOfSlidersNotBeingModified());
for (int i = 0; i < sliders.Length; i++) {
if (valueToRedistribute != 0) {
int indexToModify = orderOfLastModifiedSliders[i];
if (indexToModify != indexOfTheUIElementBeingModified) {
float ableToRedistributeToThisSlider =valueToRedistribute>0 ? Mathf.Min(sliders[indexToModify].maxValue-sliders[indexToModify].value,valueToRedistribute):
Mathf.Max(sliders[indexToModify].minValue-sliders[indexToModify].value,valueToRedistribute);
//Debug.Log("able to redistribute to slider " + indexToModify + " " + ableToRedistributeToThisSlider);
valueToRedistribute -= ableToRedistributeToThisSlider;
sliders[indexToModify].onValueChanged.RemoveListener(onValueChangedListener);
sliders[indexToModify].value += ableToRedistributeToThisSlider;
sliders[indexToModify].onValueChanged.AddListener(onValueChangedListener);
}
}
}
if (ShouldEqualizeSliderAndInputField())
EqualizeInputFieldsToSliders();
allowIndexModification = true;
}
float sumValueOfSlidersNotBeingModified() {
float cumulus = 0f;
for (int i = 0; i < sliders.Length; i++) {
if (i != indexOfTheUIElementBeingModified)
cumulus += sliders[i].value;
}
return cumulus;
}
/// <summary>
/// The listener method that controls the distribution of the total value throughout the dependent input fields
/// </summary>
/// <param name="newValue"></param>
public void ModifyTheStringsOfAllDependentInputFields(string newValue) {
if (!string.IsNullOrEmpty(newValue)) {
allowIndexModification = false;
float parsedValue = 0f;
float.TryParse(newValue, out parsedValue);
float clampedValue = Mathf.Clamp(parsedValue, sliders[indexOfTheUIElementBeingModified].minValue, sliders[indexOfTheUIElementBeingModified].maxValue);
float valueOfAllDependentInputFields = (sharedTotal - clampedValue) / (inputFields.Length - 1);
for (int i = 0; i < inputFields.Length; i++) {
//For all input fields except the one being currently modified by the player
if (i != indexOfTheUIElementBeingModified) {
//Removing the listener is important to prevent an infinite loop of onValueChanged
inputFields[i].onValueChanged.RemoveListener(onStringChangedListener);
inputFields[i].text = valueOfAllDependentInputFields.ToString();
inputFields[i].onValueChanged.AddListener(onStringChangedListener);
}
}
if (ShouldEqualizeSliderAndInputField())
EqualizeSlidersToInputFields();
allowIndexModification = true;
}
}
/// <summary>
/// A simple check to find out if only sliders/ input fields are used, or both
/// </summary>
/// <returns></returns>
private bool ShouldEqualizeSliderAndInputField() {
if (sliders.Length == 0 || inputFields.Length == 0)
return false;
else if (sliders.Length != inputFields.Length) {
Debug.LogError("There are inequal amount of dependent sliders and input fields.");
return false;
}
else
return true;
}
/// <summary>
/// Sets the Input fields' texts to correspond to the slider values
/// </summary>
private void EqualizeInputFieldsToSliders() {
for (int i = 0; i < inputFields.Length; i++) {
inputFields[i].onValueChanged.RemoveListener(onStringChangedListener);
inputFields[i].text = sliders[i].value.ToString();
inputFields[i].onValueChanged.AddListener(onStringChangedListener);
}
}
/// <summary>
/// Sets the Sliders' values to correspond to the input field texts
/// </summary>
private void EqualizeSlidersToInputFields() {
for (int i = 0; i < sliders.Length; i++) {
sliders[i].onValueChanged.RemoveListener(onValueChangedListener);
sliders[i].value = float.Parse(inputFields[i].text);
sliders[i].onValueChanged.AddListener(onValueChangedListener);
}
}
/// <summary>
/// Method that determines the slider/Input field being modified by the player
/// </summary>
/// <param name="newIndex"></param>
public void SetTheIndexOfTheUIElementBeingModified(int newIndex) {
if(allowIndexModification)
indexOfTheUIElementBeingModified = newIndex;
}
#endregion
}