开发过程有个很常见的需求:进度条上下限一致时,进度显示为0%或100%。
但ugui的slider组件默认显示进度为0%,并不能满足显示进度为100%的情况,此文主要为扩展slider组件显示100%进度。
如图所示,当上下进度一致时,游标默认停在0%处,且不能拖动。
如图所示,可以通过调整EqualShowValue字段,控制进度条停留位置。
通过查看上图slider源码可以发现,当上下限一致时,默认返回的是0,则进度显示为0%。所以我们可以通过修改此返回值,来达到修改slider游标的停留位置(修改如下图)。
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace ExtendUI
{
[AddComponentMenu("UI/ExtendSlider")]
[ExecuteInEditMode]
[RequireComponent(typeof(RectTransform))]
public class ExtendSlider : Slider
{
///
/// Set the value of the slider without invoking onValueChanged callback.
///
/// The new value for the slider.
public virtual void SetValueWithoutNotify(float input)
{
Set(input, false);
}
[SerializeField]
private float m_EqualShowValue = 0;
///
/// The current value of the slider normalized into a value between 0 and 1.
///
public new float normalizedValue
{
get
{
if (Mathf.Approximately(minValue, maxValue))
return m_EqualShowValue;
return Mathf.InverseLerp(minValue, maxValue, value);
}
set
{
this.value = Mathf.Lerp(minValue, maxValue, value);
}
}
[Space]
// Private fields
private Image m_FillImage;
private Transform m_FillTransform;
private RectTransform m_FillContainerRect;
private Transform m_HandleTransform;
private RectTransform m_HandleContainerRect;
// The offset from handle position to mouse down position
private Vector2 m_Offset = Vector2.zero;
private DrivenRectTransformTracker m_Tracker;
// This "delayed" mechanism is required for case 1037681.
private bool m_DelayedUpdateVisuals = false;
// Size of each step.
float stepSize { get { return wholeNumbers ? 1 : (maxValue - minValue) * 0.1f; } }
protected ExtendSlider()
{ }
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (wholeNumbers)
{
minValue = Mathf.Round(minValue);
maxValue = Mathf.Round(maxValue);
}
//Onvalidate is called before OnEnabled. We need to make sure not to touch any other objects before OnEnable is run.
if (IsActive())
{
UpdateCachedReferences();
Set(value, false);
// Update rects in next update since other things might affect them even if value didn't change.
m_DelayedUpdateVisuals = true;
}
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
}
#endif // if UNITY_EDITOR
public override void Rebuild(CanvasUpdate executing)
{
#if UNITY_EDITOR
if (executing == CanvasUpdate.Prelayout)
onValueChanged.Invoke(value);
#endif
}
protected override void OnEnable()
{
base.OnEnable();
UpdateCachedReferences();
Set(value, false);
// Update rects since they need to be initialized correctly.
UpdateVisuals();
}
protected override void OnDisable()
{
m_Tracker.Clear();
base.OnDisable();
}
///
/// Update the rect based on the delayed update visuals.
/// Got around issue of calling sendMessage from onValidate.
///
protected virtual void Update()
{
if (m_DelayedUpdateVisuals)
{
m_DelayedUpdateVisuals = false;
UpdateVisuals();
}
}
protected override void OnDidApplyAnimationProperties()
{
// Has value changed? Various elements of the slider have the old normalisedValue assigned, we can use this to perform a comparison.
// We also need to ensure the value stays within min/max.
value = ClampValue(value);
float oldNormalizedValue = normalizedValue;
if (m_FillContainerRect != null)
{
if (m_FillImage != null && m_FillImage.type == Image.Type.Filled)
oldNormalizedValue = m_FillImage.fillAmount;
else
oldNormalizedValue = (reverseValue ? 1 - fillRect.anchorMin[(int)axis] : fillRect.anchorMax[(int)axis]);
}
else if (m_HandleContainerRect != null)
oldNormalizedValue = (reverseValue ? 1 - handleRect.anchorMin[(int)axis] : handleRect.anchorMin[(int)axis]);
UpdateVisuals();
if (oldNormalizedValue != normalizedValue)
{
UISystemProfilerApi.AddMarker("Slider.value", this);
onValueChanged.Invoke(value);
}
}
void UpdateCachedReferences()
{
if (fillRect && fillRect != (RectTransform)transform)
{
m_FillTransform = fillRect.transform;
m_FillImage = fillRect.GetComponent();
if (m_FillTransform.parent != null)
m_FillContainerRect = m_FillTransform.parent.GetComponent();
}
else
{
fillRect = null;
m_FillContainerRect = null;
m_FillImage = null;
}
if (handleRect && handleRect != (RectTransform)transform)
{
m_HandleTransform = handleRect.transform;
if (m_HandleTransform.parent != null)
m_HandleContainerRect = m_HandleTransform.parent.GetComponent();
}
else
{
handleRect = null;
m_HandleContainerRect = null;
}
}
float ClampValue(float input)
{
float newValue = Mathf.Clamp(input, minValue, maxValue);
if (wholeNumbers)
newValue = Mathf.Round(newValue);
return newValue;
}
///
/// Set the value of the slider.
///
/// The new value for the slider.
/// If the OnValueChanged callback should be invoked.
///
/// Process the input to ensure the value is between min and max value. If the input is different set the value and send the callback is required.
///
protected override void Set(float input, bool sendCallback = true)
{
// Clamp the input
float newValue = ClampValue(input);
// If the stepped value doesn't match the last one, it's time to update
if (m_Value == newValue)
return;
m_Value = newValue;
UpdateVisuals();
if (sendCallback)
{
UISystemProfilerApi.AddMarker("Slider.value", this);
onValueChanged.Invoke(newValue);
}
}
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
//This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called.
if (!IsActive())
return;
UpdateVisuals();
}
enum Axis
{
Horizontal = 0,
Vertical = 1
}
Axis axis { get { return (direction == Direction.LeftToRight || direction == Direction.RightToLeft) ? Axis.Horizontal : Axis.Vertical; } }
bool reverseValue { get { return direction == Direction.RightToLeft || direction == Direction.TopToBottom; } }
// Force-update the slider. Useful if you've changed the properties and want it to update visually.
private void UpdateVisuals()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
UpdateCachedReferences();
#endif
m_Tracker.Clear();
if (m_FillContainerRect != null)
{
m_Tracker.Add(this, fillRect, DrivenTransformProperties.Anchors);
Vector2 anchorMin = Vector2.zero;
Vector2 anchorMax = Vector2.one;
if (m_FillImage != null && m_FillImage.type == Image.Type.Filled)
{
m_FillImage.fillAmount = normalizedValue;
}
else
{
if (reverseValue)
anchorMin[(int)axis] = 1 - normalizedValue;
else
anchorMax[(int)axis] = normalizedValue;
}
fillRect.anchorMin = anchorMin;
fillRect.anchorMax = anchorMax;
}
if (m_HandleContainerRect != null)
{
m_Tracker.Add(this, handleRect, DrivenTransformProperties.Anchors);
Vector2 anchorMin = Vector2.zero;
Vector2 anchorMax = Vector2.one;
anchorMin[(int)axis] = anchorMax[(int)axis] = (reverseValue ? (1 - normalizedValue) : normalizedValue);
handleRect.anchorMin = anchorMin;
handleRect.anchorMax = anchorMax;
}
}
// Update the slider's position based on the mouse.
void UpdateDrag(PointerEventData eventData, Camera cam)
{
RectTransform clickRect = m_HandleContainerRect ?? m_FillContainerRect;
if (clickRect != null && clickRect.rect.size[(int)axis] > 0)
{
Vector2 localCursor;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, eventData.position, cam, out localCursor))
return;
localCursor -= clickRect.rect.position;
float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]);
normalizedValue = (reverseValue ? 1f - val : val);
}
}
private bool MayDrag(PointerEventData eventData)
{
return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left;
}
public override void OnPointerDown(PointerEventData eventData)
{
if (!MayDrag(eventData))
return;
base.OnPointerDown(eventData);
m_Offset = Vector2.zero;
if (m_HandleContainerRect != null && RectTransformUtility.RectangleContainsScreenPoint(handleRect, eventData.position, eventData.enterEventCamera))
{
Vector2 localMousePos;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(handleRect, eventData.position, eventData.pressEventCamera, out localMousePos))
m_Offset = localMousePos;
}
else
{
// Outside the slider handle - jump to this point instead
UpdateDrag(eventData, eventData.pressEventCamera);
}
}
public override void OnDrag(PointerEventData eventData)
{
if (!MayDrag(eventData))
return;
UpdateDrag(eventData, eventData.pressEventCamera);
}
public override void OnMove(AxisEventData eventData)
{
if (!IsActive() || !IsInteractable())
{
base.OnMove(eventData);
return;
}
switch (eventData.moveDir)
{
case MoveDirection.Left:
if (axis == Axis.Horizontal && FindSelectableOnLeft() == null)
Set(reverseValue ? value + stepSize : value - stepSize);
else
base.OnMove(eventData);
break;
case MoveDirection.Right:
if (axis == Axis.Horizontal && FindSelectableOnRight() == null)
Set(reverseValue ? value - stepSize : value + stepSize);
else
base.OnMove(eventData);
break;
case MoveDirection.Up:
if (axis == Axis.Vertical && FindSelectableOnUp() == null)
Set(reverseValue ? value - stepSize : value + stepSize);
else
base.OnMove(eventData);
break;
case MoveDirection.Down:
if (axis == Axis.Vertical && FindSelectableOnDown() == null)
Set(reverseValue ? value + stepSize : value - stepSize);
else
base.OnMove(eventData);
break;
}
}
///
/// See Selectable.FindSelectableOnLeft
///
public override Selectable FindSelectableOnLeft()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Horizontal)
return null;
return base.FindSelectableOnLeft();
}
///
/// See Selectable.FindSelectableOnRight
///
public override Selectable FindSelectableOnRight()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Horizontal)
return null;
return base.FindSelectableOnRight();
}
///
/// See Selectable.FindSelectableOnUp
///
public override Selectable FindSelectableOnUp()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Vertical)
return null;
return base.FindSelectableOnUp();
}
///
/// See Selectable.FindSelectableOnDown
///
public override Selectable FindSelectableOnDown()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Vertical)
return null;
return base.FindSelectableOnDown();
}
public override void OnInitializePotentialDrag(PointerEventData eventData)
{
eventData.useDragThreshold = false;
}
}
}
using UnityEditor;
using UnityEditor.UI;
using UnityEngine.UI;
namespace ExtendUI
{
[CustomEditor(typeof(ExtendSlider), true)]
[CanEditMultipleObjects]
///
/// Custom Editor for the Slider Component.
/// Extend this class to write a custom editor for an Slider-derived component.
///
public class ExtendSliderEditor : SelectableEditor
{
SerializedProperty m_Direction;
SerializedProperty m_FillRect;
SerializedProperty m_HandleRect;
SerializedProperty m_MinValue;
SerializedProperty m_MaxValue;
SerializedProperty m_WholeNumbers;
SerializedProperty m_Value;
SerializedProperty m_OnValueChanged;
SerializedProperty m_EqualShowValue;
protected override void OnEnable()
{
base.OnEnable();
m_FillRect = serializedObject.FindProperty("m_FillRect");
m_HandleRect = serializedObject.FindProperty("m_HandleRect");
m_Direction = serializedObject.FindProperty("m_Direction");
m_MinValue = serializedObject.FindProperty("m_MinValue");
m_MaxValue = serializedObject.FindProperty("m_MaxValue");
m_WholeNumbers = serializedObject.FindProperty("m_WholeNumbers");
m_Value = serializedObject.FindProperty("m_Value");
m_OnValueChanged = serializedObject.FindProperty("m_OnValueChanged");
m_EqualShowValue = serializedObject.FindProperty("m_EqualShowValue");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUILayout.Space();
serializedObject.Update();
EditorGUILayout.PropertyField(m_FillRect);
EditorGUILayout.PropertyField(m_HandleRect);
if (m_FillRect.objectReferenceValue != null || m_HandleRect.objectReferenceValue != null)
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Direction);
if (EditorGUI.EndChangeCheck())
{
ExtendSlider.Direction direction = (ExtendSlider.Direction)m_Direction.enumValueIndex;
foreach (var obj in serializedObject.targetObjects)
{
ExtendSlider slider = obj as ExtendSlider;
slider.SetDirection(direction, true);
}
}
EditorGUILayout.PropertyField(m_MinValue);
EditorGUILayout.PropertyField(m_MaxValue);
EditorGUILayout.PropertyField(m_WholeNumbers);
EditorGUILayout.Slider(m_Value, m_MinValue.floatValue, m_MaxValue.floatValue);
if (m_MinValue.floatValue == m_MaxValue.floatValue)
{
EditorGUILayout.Slider(m_EqualShowValue, 0, 1);
}
bool warning = false;
foreach (var obj in serializedObject.targetObjects)
{
ExtendSlider slider = obj as ExtendSlider;
ExtendSlider.Direction dir = slider.direction;
if (dir == ExtendSlider.Direction.LeftToRight || dir == ExtendSlider.Direction.RightToLeft)
warning = (slider.navigation.mode != Navigation.Mode.Automatic && (slider.FindSelectableOnLeft() != null || slider.FindSelectableOnRight() != null));
else
warning = (slider.navigation.mode != Navigation.Mode.Automatic && (slider.FindSelectableOnDown() != null || slider.FindSelectableOnUp() != null));
}
if (warning)
EditorGUILayout.HelpBox("The selected slider direction conflicts with navigation. Not all navigation options may work.", MessageType.Warning);
// Draw the event notification options
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_OnValueChanged);
}
else
{
EditorGUILayout.HelpBox("Specify a RectTransform for the slider fill or the slider handle or both. Each must have a parent RectTransform that it can slide within.", MessageType.Info);
}
serializedObject.ApplyModifiedProperties();
}
}
}
using System;
using System.Reflection;
using ExtendUI;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace ExtendUnityEditor.UI
{
public class ExtendUIMenu : MonoBehaviour
{
private static Type mMenuOptions;
public static Type menuOptions
{
get
{
if (mMenuOptions == null)
{
var assembly = Assembly.Load("UnityEditor.UI");
mMenuOptions = assembly.GetType("UnityEditor.UI.MenuOptions", true, true);
}
return mMenuOptions;
}
}
[MenuItem("GameObject/UI/ExtendSlider", false, 2033)]
static public void AddSlider(MenuCommand menuCommand)
{
var addSlider = menuOptions?.GetMethod("AddSlider", BindingFlags.Public | BindingFlags.Static);
if (addSlider != null)
{
addSlider.Invoke(null, new object[] { menuCommand });
var obj = Selection.activeGameObject;
obj.name = nameof(ExtendSlider);
var slider = obj.GetComponent();
var targetGraphic = slider.targetGraphic;
var fillRect = slider.fillRect;
var handleRect = slider.handleRect;
DestroyImmediate(slider);
var extendSlider = obj.AddComponent();
extendSlider.targetGraphic = targetGraphic;
extendSlider.fillRect = fillRect;
extendSlider.handleRect = handleRect;
}
}
}
}