UI Misc
Namespace: Vortex.Unity.UI.Misc
Сборка: ru.vortex.unity.ui.misc
Назначение
Утилитарные UI-компоненты общего назначения: расширенная кнопка, счётчики, слайдер с анимацией, контейнер данных, вспомогательные MonoBehaviour.
Компоненты
AdvancedButton
Расширенная кнопка с режимами клика, визуальными состояниями и корректной работой внутри ScrollRect. Реализует IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler.
Визуальные состояния (через UIStateSwitcher): Free, Hover, Pressed.
| Режим клика | Где обрабатывается | Защита от scroll-drag |
|---|---|---|
OnTap |
OnPointerDown (мгновенный отклик) |
нет (по дизайну — клик до движения) |
OnUpInBorders |
OnPointerClick (release над тем же объектом) |
✅ автоматически (Unity не вызовет OnPointerClick, если EventSystem распознала жест как drag) |
OnUpAnywhere |
OnPointerUp (любой release на этом объекте) |
нет (по контракту режима — release где угодно) |
OnClick |
OnPointerClick + проверка времени < TimeForClickMs (200мс) |
✅ автоматически |
OnUpInBorders и OnClick идут через канонический IPointerClickHandler Unity: EventSystem сама определяет, что это клик, а не drag, по превышению EventSystem.pixelDragThreshold. Если кнопка лежит в ScrollRect, при скролле родитель получит drag, и OnPointerClick на кнопке не вызовется — клик корректно подавится без таймеров и собственных порогов.
Если нужен собственный порог дистанции — выставляется глобально через EventSystem.current.pixelDragThreshold (Unity-настройка, влияет на всё UGUI).
Events (Action): OnClick, OnPressed, OnReleased, OnHover, OnExit.
UnityEvents (массивы): onClick[], onHover[], onExit[].
Внешнее управление: Press(), Release(), AddOnClick(UnityAction), RemoveOnClick(UnityAction).
Особенности:
OnPointerEnterсохраняет визуал Pressed, если кнопка ещё прижата (сценарий «нажали → увели за пределы → вернули обратно»).AddOnClick(UnityAction)идемпотентен: повторная подписка одного и того жеUnityActionигнорируется (_wrappedActions-словарь),RemoveOnClickкорректно снимет wrapper.- Внешний
Press()/Release()работает в режимеOnClick: приeventData == nullсмещение считается нулевым, проверка времени остаётся.Release()идемпотентен — no-op, если кнопка не была нажата. OnEnableсбрасывает внутренние флаги (_pressed = false), вызываетuiStateSwitcher.ResetStates()и переводит визуал вFree. Это исключает ситуацию «кнопку выключили во время удержания → при следующем включении она снова показывает Pressed».OnDisableлишь сбрасывает_inBorders, реальная инициализация состояния происходит вOnEnable.
DataStorage
Универсальный контейнер данных. Реализует IDataStorage : IDataSource. FIFO-поиск по типу.
storage.SetData(myModel); // полная замена всех данных → OnUpdateLink
storage.SetData(new[] { a, b }); // полная замена набором → OnUpdateLink
storage.AddData(extraData); // добавление/замена по типу → OnUpdateLink не вызывается
var model = storage.GetData<MyModel>(); // поиск по типу
storage.OnUpdateLink += ReBindAll; // переподцепить все ссылки
OnUpdateLink — link-level: вызывается только при полной замене содержимого (SetData), которая инвалидирует ранее полученные через GetData<T>() ссылки. AddData не инвоцирует событие, поскольку существующие ссылки остаются валидными.
Точки расширения для наследников:
Data(protected) — прямой доступ к внутреннему списку из подклассов. Использовать осторожно: запись мимоSetData/AddDataне инвоцируетOnUpdateLink.dataSwitcher(protected) — управление визуальным состоянием из подкласса.OnEnable()/OnDisable()(virtual) — переопределяются в наследниках для подключения дополнительной логики (например, реактивного слоя). Используется, в частности,DataStorageTransport(AssetCacheSystem), который грузит префаб по addressable-ссылке вOnEnableи освобождает вOnDisable.
DataCapturer
Late-binding мост: MonoBehaviour-источник + имя реактивного свойства → IDataStorage. Реализует IDataStorage : IDataSource. Конфигурируется в инспекторе; в редакторе выпадающий список свойств собирается через рефлексию по типу IReactiveData (включая IntData/BoolData/FloatData/любых наследников ReactiveValue<T>).
// На префабе:
// source = ссылка на любой MonoBehaviour-источник
// property = имя свойства (выбирается через ValueSelector с фильтром IReactiveData)
// Потребитель (Pool-итем, generic-handler и т.п.):
capturer.OnUpdateLink += () =>
{
var data = capturer.GetData<IntData>(); // ссылка на ReactiveValue
data.OnUpdate += v => UpdateView(v); // подписка на value-level
};
Жизненный цикл:
Awake— кешируетPropertyInfo. При отсутствии свойства логирует ошибку, выключает компонент (enabled = false) и не подписывается на источник.- Если источник реализует
IDataSource, подписывается на егоOnUpdateLinkдля отслеживания пересоздания реактивных полей. Start— первыйRefreshLink()после всехAwakeсцены, потребители успевают подписаться.RefreshLinkсравнивает новую и старую ссылки черезReferenceEqualsи инвоцируетOnUpdateLinkтолько при реальной смене ссылки.OnDestroy— отписка от источника, зануление кеша.
Ниша: связки, которые нельзя зашить через [UIComponentLink] (атрибутная привязка в коде виджета) — например, generic Pool-итемы, виджеты-шаблоны, сборка связки в инспекторе на префабе. Источник всегда MonoBehaviour — для ScriptableObject-настроек/пресетов используется прямая [SerializeField]-ссылка, не DataCapturer.
CounterViewBase<T> (abstract)
Базовый компонент для отображения счётчика с минимумом/максимумом/текущим значением, набором слайдеров, пульсацией и пороговыми визуальными состояниями. Извлечение чисел из модели и подписка на её реактивные поля — на подклассе; база берёт на себя lifecycle, рендер и переключение состояний.
Наследник реализует:
int GetValue()— текущее значениеint GetMinValue()— минимум диапазонаint GetMaxValue()— максимум диапазонаvoid Init()— подписка на реактивные поляDatavoid DeInit()— симметричная отписка (должна быть идемпотентной:-= handlerна пустом event — no-op, а обращения к внутренней ссылке на модель — под null-guard, потому чтоDeInitможет быть вызван до первогоInit)
Доступ к модели через protected T Data. Ссылка автоматически переустанавливается базой при UpdateLink (при полной смене содержимого storage'а).
Lifecycle:
OnEnable→ подписка наIDataStorage.OnUpdateLink+switcher.Set(Empty)+ однократныйUpdateLink()для первичной инициализации.UpdateLink(внутренний) при каждом сигнале от storage:DeInit()→ перезаборData→ null-guard → синхронизация кэша значения →Init()→ refresh всех блоков (min/max/value/switcher/sliders). Единый путь и для первого enable, и для последующих re-link'ов — подписки подкласса переезжают на актуальную модель без утечек.OnDisable→ отписка отOnUpdateLink+DeInit().
Поля инспектора (все опциональные — компонент работает с любым подмножеством):
| Группа | Поле | Что делает |
|---|---|---|
| Source | sourceValue |
IDataStorage (через ClassFilter+AutoLink) — источник модели данных |
| Min Value UI | min (UIComponent), patternMin = "{0}" |
Текстовый виджет минимума и его паттерн форматирования |
| Max Value UI | max (UIComponent), patternMax = "{0}" |
Текстовый виджет максимума |
| Current Value UI | value (UIComponent), patternValue = "{2} < {0} < {1}" |
Текстовый виджет текущего значения. Получает три аргумента: {0} = value, {1} = max, {2} = min |
| — | sliders (SliderView[]) |
Массив анимированных слайдеров — каждый обновляется теми же (value, max, min) |
| — | tweenPulsation (TweenerHub) |
Pulse-анимация на изменение значения |
| — | switcher (UIStateSwitcher по enum CounterStates) |
Пороговые визуальные состояния |
| Анимации | onUp / onDown |
Анимировать при росте / убывании значения |
Пороги CounterStates (по проценту заполнения диапазона):
| Состояние | Условие |
|---|---|
Empty |
value == minValue |
Less20 |
< 20% диапазона |
Less50 |
< 50% |
Less80 |
< 80% |
Less100 |
≥ 80% и < maxValue |
Fill |
value == maxValue |
API наследника:
UpdateValue()/UpdateMinValue()/UpdateMaxValue()— подкласс дёргает эти методы из своей подписки на реактивное поле, чтобы протолкнуть refresh соответствующего блока UI.
CounterViewAdvanced
Автономный компонент под типовой сценарий «три отдельных IDataStorage, в каждом лежит IntData». Не наследник CounterViewBase<T> — независимая реализация того же UI-поведения (те же пороги CounterStates, тот же pulse-tween, те же format-паттерны) с тремя раздельными источниками, самостоятельно подписывающимися на IntData.OnUpdate для value/min/max.
Особенности vs. CounterViewBase<T>:
- Три источника вместо одного (
sourceValue/sourceMin/sourceMax) — удобно, когда min/max живут в отдельных моделях или свойствах. - Один
SliderViewвместо массива. - Не требует подкласса — работает out-of-the-box, если данные представлены как
IntData. - Ограничен
IntData— для доменной модели с не-int полями или произвольным вычислением значений использоватьCounterViewBase<T>с подклассом.
SliderView
Анимированный Slider через AsyncTween.
sliderView.Set(0.75f, 1f); // value, max
| Поле | Тип | Описание |
|---|---|---|
slider |
Slider |
Целевой слайдер |
delay |
float |
Задержка перед анимацией ([Range(0, 3)]) |
duration |
float |
Длительность анимации (0..1 сек) |
ease |
EaseType |
Тип easing |
Пропускает обновление если value и max не изменились.
SliderStepHandler
Пошаговое изменение слайдера: публичные Up() / Down() сдвигают значение на 10% диапазона (normalizedValue ± 0.1). Вызываются из UnityEvent — кнопок, InputActionHandler (например, на скролл) и т.п.
| Поле | Тип | Описание |
|---|---|---|
slider |
Slider |
Целевой слайдер ([AutoLink]) |
Значение выставляется через сам слайдер: границы minValue/maxValue, wholeNumbers и onValueChanged работают как при перетаскивании — изменение подхватывают настройки и откат (RollbackControl). При wholeNumbers и диапазоне меньше 10 единиц шаг округляется — Up/Down могут не сдвинуть значение.
PointerEventsHandler
Раздельное чтение UGUI-событий указателя в UnityEvent'ы — для случаев, где нужны отдельные нажатие и отпускание (удержание): у Button и AdvancedButton в инспекторе есть только клик. Нужен raycast-target Graphic на объекте или его детях.
| Поле | Событие |
|---|---|
onPointerDown |
Нажатие над объектом |
onPointerUp |
Отпускание после нажатия на объекте — где бы ни был указатель |
onPointerClick |
Клик: нажатие и отпускание над объектом, без drag |
onPointerEnter |
Указатель вошёл на объект |
onPointerExit |
Указатель ушёл с объекта |
Кнопка указателя не фильтруется — события приходят от любой (ЛКМ/ПКМ/СКМ, тач).
Можно ставить на объект с AdvancedButton: UGUI отдаёт событие всем обработчикам первого объекта по иерархии — кнопка ведёт свой визуал и клик, хэндлер отдаёт раздельные нажатие и отпускание. Если объект выключили во время удержания, onPointerUp не придёт (UGUI не шлёт Up выключенному объекту).
AutoRectSetter
Автонастройка RectTransform через Inspector. [ExecuteAlways] — работает и в Editor, и в Play.
Настраиваемые параметры (каждый включается toggle):
- Borders (left, top, right, bottom, posZ)
- Anchors (anchorMin, anchorMax)
- Pivot
- Rotation (localEulerAngles)
Apply() — применить настройки. ReadFromCurrent() — захватить текущие значения RectTransform.
EnableDelayForChild
Отложенная активация дочерних объектов.
| Поле | Тип | Описание |
|---|---|---|
delay |
float |
Задержка (0..10 сек) |
Awake — деактивирует всех детей. OnEnable — планирует активацию через TimeController.Call(). OnDisable — деактивирует. Помечен [ExecuteInEditMode]: в эдиторе вне Play-режима активация детей происходит мгновенно (без задержки) — для превью раскладки.
ScrollRectResetHandler
Сброс ScrollRect в начальную позицию (normalizedPosition = Vector2.one) при Start.
LoaderWaitingHandler
Хэндлер «не показывать UI, пока не догрузятся ресурсы». Принимает массив MonoBehaviour[]-объектов (в инспекторе фильтруется атрибутом [ClassFilter(typeof(INeedDelay))]) и UIStateSwitcher, привязанный к enum DelayedState (Waiting, Ready).
Внутри использует DelayedObserver (Vortex.Core.System.Models) — ждёт готовности всех INeedDelay. По завершении переключает состояние в Ready. На каждую ошибку пишет Debug.LogError с типом упавшего источника.
OnEnable
├── stateSwitcher.Set(Waiting)
└── new DelayedObserver(delayed, OnReady, OnError, callOnError)
├── OnReady → stateSwitcher.Set(Ready)
└── OnError → Debug.LogError("[LoaderWaitingHandler] loading failed for {Type}")
OnDisable
└── observer.Dispose()
Параметры:
delayed(MonoBehaviour[]+ClassFilter) — компоненты-источники задержки. В инспектор можно положить только объекты, реализующиеINeedDelay.callOnError(bool, по умолчаниюtrue) — приtrueReadyподнимется даже после ошибок; приfalseсостояние останетсяWaitingнавсегда при первой же ошибке.stateSwitcher(UIStateSwitcher) — должен быть сконфигурирован под enumDelayedState.
Типичное использование: укладывается рядом с UI-блоком, который зависит от Spine/Audio/Addressable-загрузок. На время ожидания UI скрыт/заменён лоадером, после готовности — раскрывается.
Bool2StateSwitcherHandler / Int2StateSwitcherHandler
Гонят UIStateSwitcher по реактивному значению из связанного IDataStorage (потребители, логику не содержат).
Bool2StateSwitcherHandler— поBoolData:true→SwitcherState.On,false→Off. Свитчер конфигурируется под enumSwitcherState.Int2StateSwitcherHandler— поIntData: номер состояния =IntData.Value(свитчер без enum-привязки, любой номер).
Оба: источник — storage (MonoBehaviour + [ClassFilter(typeof(IDataStorage))] + [AutoLink]). Переподключаются по OnUpdateLink и реагируют на OnUpdateData (данные могут прийти, например, через DataStorageTransport). Подписка/relink в OnEnable, отписка в OnDisable. Отсутствие IDataStorage/UIStateSwitcher — LogError.
DropDown
Компонент выпадающего списка. Состоит из четырёх классов:
DropDownComponent— контроллер: toggle open/close, конфигурация черезSetList(texts, callback, value). Поддержка сортировки (sorting),UnityEvent<int> onSelected,closeOnSelected,scrollSensitivity, переборPrev/Nextс флагомcyclic. При сортировке строит прямую (_map) и обратную (_mapBack) карту индексов сортированного → оригинального порядка. Позиция списка перевыставляется на каждый открыв (по точке привязки под текущее направление), а не кешируется от первого — список не отстаёт от сдвинувшейся кнопки.DropDownList— Pool-based список, scroll-позиционирование к выбранному элементу черезScrollRect.normalizedPosition(сброс выполняется и на повторном открытии с тем же составом, а не только черезOnEnableотSetActive). Кеширует хэш текстов (string.Join) — при том же составе обновляет толькоCurrentи скролл, не пересоздавая пул. ПолеdirectionSwitcher(UIStateSwitcher) переключает визуальное состояние под направление раскрытия.DropDownItem— элемент списка. ПолучаетDropDownListModelиIntData(индекс) черезIDataStorage. Визуально выделяет текущий элемент черезUIComponent.SetSwitcher(SwitcherState.On/Off). Подписывается наOnUpdateDataдля обновления.IsSelected— пункт выбран (читает, например,FocusDropDownSelectedиз VirtualCursorSystem; название — с расчётом на будущий мультивыбор).DropDownListModel—IReactiveDataмодель: callbacks (select, close), тексты, текущий выбор,closeOnSelected,ScrollSensitivity.Dispose()очищает подписчиков.
Направление раскрытия (DropDownDirection). Enum { RightDown, RightTop, LeftDown, LeftTop } (горизонталь + вертикаль; порядок значений = порядок состояний свитчера, RightDown = 0 … LeftTop = 3). По умолчанию — RightDown (справа-вниз). Флаг autoOrientation включает автоопределение: экранная позиция базовой точки target (через RectTransformUtility.WorldToScreenPoint, камера канваса; для Overlay — null) сравнивается с центром экрана — кнопка левее центра раскрывает список вправо, выше центра — вниз (и наоборот), чтобы он уходил к центру, а не за край. При включённом флаге в инспекторе (Odin [ShowIf]) появляются три доп-точки привязки targetRightTop / targetLeftDown / targetLeftTop; отсутствующая точка фолбечится на обязательную target (RightDown). Вычисленное направление уходит в DropDownList.Set(...), где directionSwitcher принимает соответствующее состояние; поле помечено [StateSwitcher(typeof(DropDownDirection))] — в инспекторе состояния свитчера подписываются значениями enum (RightDown = 0 … LeftTop = 3).
Перебор без раскрытия (Prev / Next). Публичные методы сдвигают выбор на одну позицию — для списков, которые листают стрелками или геймпадом вместо раскрытия. Шаг идёт по видимому порядку: сдвигается номер в сортированном списке, то есть в том порядке, в каком пункты стоят в раскрытом списке; при выключенной sorting видимый и исходный порядки совпадают. Дальше работает обычный Select(...) — индекс приводится к исходному через _mapBack, вызываются _callback, onSelected и OnValueSelected, обновляется текст на кнопке; список, раскрытый в этот момент, переподхватывает новое выделение. Флаг cyclic задаёт поведение на границах: включён — с последнего пункта шаг вперёд уводит на первый (и наоборот), выключен — шаг не делается вовсе, без вызова колбэков. Вызов до инициализации сам прогоняет SetList(dataList, null) — так же, как это делает OpenList.
API:
dropDown.SetList(texts, OnSelect, currentValue); // конфигурация
dropDown.SetValue(3); // программное переключение
dropDown.Next(); // шаг по видимому порядку
dropDown.Prev();
int idx = dropDown.GetValue(); // оригинальный индекс
string text = dropDown.GetValueItem(); // текст выбранного
Callback Select() всегда возвращает оригинальный (не сортированный) индекс через _mapBack.
Список инстанцируется в Canvas при первом открытии, при закрытии деактивируется, при уничтожении контроллера — уничтожается.
Зависимости
| Зависимость | Назначение |
|---|---|
Vortex.Unity.UI.StateSwitcher |
UIStateSwitcher — визуальные состояния AdvancedButton |
Vortex.Unity.UI.TweenerSystem.UniTaskTweener |
AsyncTween, EaseType — анимация SliderView |
Vortex.Unity.AppSystem |
TimeController — отложенные вызовы |
Vortex.Core.System |
IDataSource, IDataStorage, IReactiveData |
Vortex.Core.Extensions.ReactiveValues |
ReactiveValue<T>, IntData, BoolData, FloatData — фильтр свойств DataCapturer |
Vortex.Core.Extensions |
ActionExt.Fire() |
| Sirenix Odin Inspector | ValueSelector, FoldoutGroup — UX DataCapturer |
| TextMeshPro | TMP в CounterView |
Граничные случаи
| Ситуация | Поведение |
|---|---|
AdvancedButton.OnClick в режиме OnClick/OnUpInBorders внутри ScrollRect |
При скролле OnPointerClick не вызывается — клик подавляется автоматически |
AdvancedButton.OnClick в режиме OnUpAnywhere внутри ScrollRect |
Срабатывает на любой release (по контракту режима, без защиты от drag) |
AdvancedButton.Press() / Release() извне |
Работает без pointer-событий; в режиме OnClick смещение считается нулевым, проверка времени применяется |
AdvancedButton.AddOnClick(action) повторно с тем же action |
Игнорируется (идемпотентно); RemoveOnClick всё равно снимет wrapper |
AdvancedButton.OnPointerEnter после exit с зажатой кнопкой |
Визуал возвращается в Pressed, не в Hover |
CounterViewBase.OnEnable при пустом источнике |
NRE на StorageValue.OnUpdateLink += ... (fail-fast) |
DataStorage.GetData<T>() — тип не найден |
Возвращает null |
DataStorage.AddData() — добавление |
OnUpdateLink не вызывается (link-level не нарушен) |
DataCapturer — свойство переименовано или отсутствует |
Debug.LogError + enabled = false; Start/RefreshLink не выполняются |
DataCapturer — источник реализует IDataSource |
Подписка на OnUpdateLink источника, RefreshLink при сигнале |
DataCapturer.RefreshLink — ссылка не изменилась |
OnUpdateLink не вызывается (ReferenceEquals) |
DataCapturer — source или property не заданы в инспекторе |
NRE в Awake (fail-fast по канону) |
SliderView.Set() — те же value/max |
Обновление пропускается |
EnableDelayForChild — OnDisable до срока |
Дети деактивируются, таймер снимается |
AutoRectSetter в Editor |
Обновляется при OnValidate |
DropDownComponent.Prev() / Next() на границе списка, cyclic выключен |
Шаг не делается, колбэки не вызываются |
DropDownComponent.Prev() / Next() до первого SetList или открытия |
Сам вызывает SetList(dataList, null); при пустом dataList — тихий выход |
DropDownComponent.Prev() / Next() при включённой sorting |
Шаг по видимому (сортированному) порядку; в колбэки уходит оригинальный индекс |