// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using Doozy.Editor.Reactor.Internal; using Doozy.Editor.UIElements; using Doozy.Runtime.Reactor; using Doozy.Runtime.Reactor.Easings; using Doozy.Runtime.Reactor.Internal; using Doozy.Runtime.Reactor.Reactions; using Doozy.Runtime.UIElements.Extensions; using UnityEngine.Events; using UnityEngine.UIElements; // ReSharper disable MemberCanBePrivate.Global namespace Doozy.Editor.EditorUI.Components { public class FluidAnimatedContainer : VisualElement, IDisposable { public void Dispose() { ClearContent(); reaction?.Recycle(); } //layoutContainer //--fluidContainer #region State public enum State { Idle, IsShowing, Visible, IsHiding, Hidden } private State m_State; public State state { get => m_State; set { // Debug.Log($"{name}.{nameof(state)}: {state} > {value}"); reaction?.ClearOnFinishCallback(); switch (value) { case State.Idle: //do nothing break; case State.Visible: fluidContainer.SetStyleDisplay(DisplayStyle.Flex); break; case State.Hidden: fluidContainer.SetStyleDisplay(DisplayStyle.None); if (clearOnHide) ClearContent(); break; case State.IsShowing: fluidContainer.SetStyleDisplay(DisplayStyle.Flex); reaction?.SetOnFinishCallback(() => state = State.Visible); break; case State.IsHiding: fluidContainer.SetStyleDisplay(DisplayStyle.Flex); reaction?.SetOnFinishCallback(() => state = State.Hidden); break; default: throw new ArgumentOutOfRangeException(nameof(value), value, null); } m_State = value; onStateChanged?.Invoke(state); } } public bool isVisible => state == State.Visible || state == State.IsShowing; public bool isHidden => state == State.Hidden || state == State.IsHiding; #endregion #region Direction public enum Direction { Top, Bottom, Left, Right } public Direction direction { get; private set; } #endregion //SETTINGS public const Ease k_ReactionEase = Ease.InOutCubic; public const float k_ReactionDuration = 0.2f; private float fluidContainerWidth { get; set; } private float fluidContainerHeight { get; set; } public TemplateContainer templateContainer { get; } public VisualElement layoutContainer { get; } public VisualElement fluidContainer { get; } public FloatReaction reaction { get; private set; } public bool clearOnHide { get; set; } public UnityAction OnShowCallback; public UnityAction OnHideCallback; public UnityAction onStateChanged { get; private set; } public FluidAnimatedContainer(bool clearOnHide) : this(string.Empty, clearOnHide) {} public FluidAnimatedContainer(string name, bool clearOnHide) : this() { this .SetName(name) .SetClearOnHide(clearOnHide); } public FluidAnimatedContainer() { Add(templateContainer = EditorLayouts.EditorUI.FluidAnimatedContainer.CloneTree()); templateContainer .SetStyleFlexGrow(1) .AddStyle(EditorStyles.EditorUI.FluidAnimatedContainer); layoutContainer = templateContainer.Q(nameof(layoutContainer)); fluidContainer = layoutContainer.Q(nameof(fluidContainer)); reaction = Reaction.Get().SetEditorHeartbeat().SetEase(k_ReactionEase).SetDuration(k_ReactionDuration) .SetSetter(value => { RecalculateValues(); fluidContainer.SetStyleDisplay(value < 0.99f ? DisplayStyle.Flex : DisplayStyle.None); switch (direction) { case Direction.Top: case Direction.Bottom: // Debug.Log($"{reaction} > -{fluidContainerHeight} * {value.Round(2)} = {-fluidContainerHeight * value}"); if (fluidContainerHeight == 0) return; //this fixes the 'jumpy' effect when the contents of the container changes during animation Update(-fluidContainerHeight * value); break; case Direction.Left: case Direction.Right: // Debug.Log($"{reaction} > -{fluidContainerHeight} * {value.Round(2)} = {-fluidContainerHeight * value}"); if (fluidContainerWidth == 0) return; //this fixes the 'jumpy' effect when the contents of the container changes during animation Update(-fluidContainerWidth * value); break; default: throw new ArgumentOutOfRangeException(); } }); // Debug.Log($"{nameof(FluidAnimatedContainer)} - reaction type: {reaction.GetType().Name}"); direction = Direction.Top; state = State.Idle; fluidContainer.SetStyleDisplay(DisplayStyle.None); fluidContainer.visible = false; fluidContainer.RegisterCallback(OnGeometryChanged); fluidContainer.RegisterCallback(OnAttachToPanel); fluidContainer.RegisterCallback(OnDetachFromPanel); } private IVisualElementScheduledItem autoUpdate { get; set; } private void OnAttachToPanel(AttachToPanelEvent evt) { schedule.Execute(() => OnGeometryChanged(null)); autoUpdate ??= schedule.Execute(UpdateContainer).Every(1000); autoUpdate?.Resume(); } private void OnDetachFromPanel(DetachFromPanelEvent evt) { autoUpdate?.Pause(); } private void UpdateContainer() { RecalculateValues(); if (reaction.isActive) return; switch (state) { case State.Idle: case State.IsShowing: case State.IsHiding: return; case State.Visible: ResetToVisiblePosition(); break; case State.Hidden: ResetToHiddenPosition(); break; default: throw new ArgumentOutOfRangeException(); } } private bool m_ShowContainer; private void OnGeometryChanged(GeometryChangedEvent evt) { // Debug.Log($"{name}.{nameof(OnGeometryChanged)}: {evt}"); UpdateContainer(); if (!m_ShowContainer) return; if (reaction == null) return; if (reaction.isActive) return; ResetToHiddenPosition(); schedule.Execute(() => { m_ShowContainer = false; reaction.Play(PlayDirection.Reverse); fluidContainer.visible = true; }); } public void ResetToHiddenPosition() { switch (direction) { case Direction.Top: case Direction.Bottom: Update(-fluidContainerHeight); //reset to hidden break; case Direction.Left: case Direction.Right: Update(-fluidContainerWidth); //reset to hidden break; default: throw new ArgumentOutOfRangeException(); } } public void ResetToVisiblePosition() { switch (direction) { case Direction.Top: case Direction.Bottom: Update(0); //reset to visible break; case Direction.Left: case Direction.Right: Update(0); //reset to visible break; default: throw new ArgumentOutOfRangeException(); } } public void RecalculateValues() { // Debug.Log($"{nameof(RecalculateValues)}"); fluidContainerWidth = fluidContainer.resolvedStyle.width; fluidContainerHeight = fluidContainer.resolvedStyle.height; } public FluidAnimatedContainer SetDirection(Direction targetDirection) { if (targetDirection == direction) return this; if (reaction.isActive) reaction.SetProgressAtZero(); direction = targetDirection; return this; } private void Update(float toValue) { switch (direction) { case Direction.Top: fluidContainer.SetStyleMarginTop(toValue); break; case Direction.Bottom: fluidContainer.SetStyleBottom(toValue); fluidContainer.SetStyleMarginTop(toValue); break; case Direction.Left: fluidContainer.SetStyleMarginLeft(toValue); break; case Direction.Right: fluidContainer.SetStyleMarginRight(toValue); break; default: throw new ArgumentOutOfRangeException(nameof(direction), direction, null); } } public FluidAnimatedContainer Toggle(bool show, bool animateChange = true) => show ? Show(animateChange) : Hide(animateChange); public FluidAnimatedContainer Show(bool animateChange = true) { schedule.Execute(() => OnGeometryChanged(null)); fluidContainer.visible = true; if (!animateChange) { switch (state) { case State.Idle: case State.Hidden: OnShowCallback?.Invoke(); break; case State.Visible: //do nothing break; case State.IsShowing: case State.IsHiding: reaction?.Stop(); break; default: throw new ArgumentOutOfRangeException(); } Update(0); //reset state = State.Visible; } else //animation { switch (state) { case State.Idle: case State.Hidden: m_ShowContainer = true; OnShowCallback?.Invoke(); fluidContainer.visible = false; state = State.IsShowing; break; case State.IsHiding: reaction.Reverse(); state = State.IsShowing; break; case State.IsShowing: case State.Visible: //do nothing break; default: throw new ArgumentOutOfRangeException(); } } return this; } public FluidAnimatedContainer Hide(bool animateChange = true) { schedule.Execute(() => OnGeometryChanged(null)); if (!animateChange) { switch (state) { case State.Idle: case State.Hidden: //do nothing break; case State.Visible: OnHideCallback?.Invoke(); RecalculateValues(); break; case State.IsShowing: case State.IsHiding: reaction.Stop(); break; default: throw new ArgumentOutOfRangeException(); } switch (direction) { case Direction.Top: case Direction.Bottom: Update(-fluidContainerHeight); //reset break; case Direction.Left: case Direction.Right: Update(-fluidContainerWidth); //reset break; default: throw new ArgumentOutOfRangeException(); } fluidContainer.SetStyleDisplay(DisplayStyle.None); state = State.Hidden; } else //animation { switch (state) { case State.Idle: reaction.Play(PlayDirection.Forward); state = State.IsHiding; break; case State.Visible: RecalculateValues(); OnHideCallback?.Invoke(); reaction.Play(PlayDirection.Forward); state = State.IsHiding; break; case State.IsShowing: reaction.Reverse(); state = State.IsHiding; break; case State.IsHiding: case State.Hidden: //do nothing break; default: throw new ArgumentOutOfRangeException(); } } return this; } public FluidAnimatedContainer AddContent(VisualElement content) { fluidContainer.AddChild(content); return this; } /// Recycle and clear the animated container and then reset its layout public FluidAnimatedContainer ClearContent(bool resetLayout = false) { fluidContainer.RecycleAndClear(); if (resetLayout) fluidContainer.ResetLayout(); return this; } public FluidAnimatedContainer SetClearOnHide(bool clear) { clearOnHide = clear; return this; } #region OnShowCallback public FluidAnimatedContainer AddOnShowCallback(UnityAction callback) { OnShowCallback += callback; return this; } public FluidAnimatedContainer SetOnShowCallback(UnityAction callback) { OnShowCallback = callback; return this; } public FluidAnimatedContainer ClearOnShowCallback() { OnShowCallback = null; return this; } #endregion #region OnHideCallback public FluidAnimatedContainer AddOnHideCallback(UnityAction callback) { OnHideCallback += callback; return this; } public FluidAnimatedContainer SetOnHideCallback(UnityAction callback) { OnHideCallback = callback; return this; } public FluidAnimatedContainer ClearOnHideCallback() { OnHideCallback = null; return this; } #endregion } }