diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneCompositeDrawable.cs b/osu.Framework.Tests/Visual/Containers/TestSceneCompositeDrawable.cs index 8064c71d70..7c3e521bd5 100644 --- a/osu.Framework.Tests/Visual/Containers/TestSceneCompositeDrawable.cs +++ b/osu.Framework.Tests/Visual/Containers/TestSceneCompositeDrawable.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osuTK; using osuTK.Graphics; @@ -89,6 +90,91 @@ public void TestClearTransformsOnDelayedAutoSize() AddUntilStep("container still autosized", () => container.Size == new Vector2(100)); } + [Test] + public void TestAutoSizeDuration() + { + Container parent = null; + Drawable child = null; + + AddStep("create hierarchy", () => + { + Child = parent = new Container + { + Masking = true, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Yellow, + }, + new Container + { + Padding = new MarginPadding(50), + AutoSizeAxes = Axes.Both, + Child = child = new Box + { + Size = new Vector2(100), + Colour = Color4.Red, + } + } + } + }; + }); + + AddSliderStep("AutoSizeDuration", 0f, 1500f, 500f, value => + { + if (parent != null) parent.AutoSizeDuration = value; + }); + AddSliderStep("Width", 0f, 300f, 100f, value => + { + if (child != null) child.Width = value; + }); + AddSliderStep("Height", 0f, 300f, 100f, value => + { + if (child != null) child.Height = value; + }); + } + + [Test] + public void TestFinishAutoSizeTransforms() + { + Container parent = null; + Drawable child = null; + + AddStep("create hierarchy", () => + { + Child = parent = new Container + { + Masking = true, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 1000, + Name = "Parent", + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Yellow, + }, + child = new Box + { + Size = new Vector2(100), + Colour = Color4.Red, + Alpha = 0.5f, + } + } + }; + }); + AddAssert("size matches child", () => Precision.AlmostEquals(parent.ChildSize, child.LayoutSize)); + AddStep("resize child", () => child.Size = new Vector2(200)); + AddAssert("size doesn't match child", () => !Precision.AlmostEquals(parent.ChildSize, child.LayoutSize)); + AddStep("finish autosize transform", () => parent.FinishAutoSizeTransforms()); + AddAssert("size matches child", () => Precision.AlmostEquals(parent.ChildSize, child.LayoutSize)); + } + private partial class SortableComposite : CompositeDrawable { public SortableComposite() diff --git a/osu.Framework.Tests/Visual/Layout/TestSceneLayoutDurations.cs b/osu.Framework.Tests/Visual/Layout/TestSceneLayoutDurations.cs index 0d868976ce..59e8bd9943 100644 --- a/osu.Framework.Tests/Visual/Layout/TestSceneLayoutDurations.cs +++ b/osu.Framework.Tests/Visual/Layout/TestSceneLayoutDurations.cs @@ -18,10 +18,9 @@ namespace osu.Framework.Tests.Visual.Layout public partial class TestSceneLayoutDurations : FrameworkTestScene { private ManualClock manualClock; - private Container autoSizeContainer; private FillFlowContainer fillFlowContainer; - private Box box1, box2; + private Box box; private const float duration = 1000; @@ -34,24 +33,6 @@ public void SetUp() => Schedule(() => Children = new Drawable[] { - autoSizeContainer = new Container - { - Clock = new FramedClock(manualClock), - AutoSizeEasing = Easing.None, - Children = new[] - { - new Box - { - Colour = Color4.Red, - RelativeSizeAxes = Axes.Both - }, - box1 = new Box - { - Colour = Color4.Transparent, - Size = Vector2.Zero, - }, - } - }, fillFlowContainer = new FillFlowContainer { Clock = new FramedClock(manualClock), @@ -60,27 +41,20 @@ public void SetUp() => Schedule(() => Children = new Drawable[] { new Box { Colour = Color4.Red, Size = new Vector2(100) }, - box2 = new Box { Colour = Color4.Blue, Size = new Vector2(100) }, + box = new Box { Colour = Color4.Blue, Size = new Vector2(100) }, } } }; paused = false; - autoSizeContainer.FinishTransforms(); fillFlowContainer.FinishTransforms(); - autoSizeContainer.AutoSizeAxes = Axes.None; - autoSizeContainer.AutoSizeDuration = 0; - autoSizeContainer.Size = Vector2.Zero; - box1.Size = Vector2.Zero; - fillFlowContainer.LayoutDuration = 0; fillFlowContainer.Size = new Vector2(200, 200); }); private void check(float ratio) => - AddAssert($"Check @{ratio}", () => Precision.AlmostEquals(autoSizeContainer.Size, new Vector2(changed_value * ratio)) && - Precision.AlmostEquals(box2.Position, new Vector2(changed_value * (1 - ratio), changed_value * ratio))); + AddAssert($"Check @{ratio}", () => Precision.AlmostEquals(box.Position, new Vector2(changed_value * (1 - ratio), changed_value * ratio))); private void skipTo(float ratio) => AddStep($"skip to {ratio}", () => { manualClock.CurrentTime = duration * ratio; }); @@ -91,13 +65,8 @@ public void TestChangeAfterDuration() { paused = true; manualClock.CurrentTime = 0; - autoSizeContainer.FinishTransforms(); fillFlowContainer.FinishTransforms(); - autoSizeContainer.AutoSizeAxes = Axes.Both; - autoSizeContainer.AutoSizeDuration = duration; - box1.Size = new Vector2(100); - fillFlowContainer.LayoutDuration = duration; fillFlowContainer.Width = 100; }); @@ -116,14 +85,11 @@ public void TestInterruptExistingDuration() { paused = true; manualClock.CurrentTime = 0; - autoSizeContainer.FinishTransforms(); fillFlowContainer.FinishTransforms(); - autoSizeContainer.AutoSizeAxes = Axes.Both; - autoSizeContainer.AutoSizeDuration = duration; fillFlowContainer.LayoutDuration = duration; - box1.Size = new Vector2(changed_value); + box.Size = new Vector2(changed_value); fillFlowContainer.Width = changed_value; }); @@ -132,7 +98,6 @@ public void TestInterruptExistingDuration() AddStep("set duration 0", () => { - autoSizeContainer.AutoSizeDuration = 0; fillFlowContainer.LayoutDuration = 0; }); @@ -146,7 +111,6 @@ public void TestInterruptExistingDuration() AddStep("alter values", () => { - box1.Size = new Vector2(0); fillFlowContainer.Width = 200; }); @@ -162,11 +126,10 @@ public void TestInterruptExistingDuration() protected override void Update() { - if (autoSizeContainer != null) + if (fillFlowContainer != null) { if (!paused) manualClock.CurrentTime = Clock.CurrentTime; - autoSizeContainer.Children[0].Invalidate(); fillFlowContainer.Invalidate(); } diff --git a/osu.Framework/Graphics/Containers/CompositeDrawable.cs b/osu.Framework/Graphics/Containers/CompositeDrawable.cs index 3d9de04b5f..1f1f5bde19 100644 --- a/osu.Framework/Graphics/Containers/CompositeDrawable.cs +++ b/osu.Framework/Graphics/Containers/CompositeDrawable.cs @@ -22,6 +22,7 @@ using osu.Framework.Statistics; using System.Threading.Tasks; using JetBrains.Annotations; +using osu.Framework.Caching; using osu.Framework.Development; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ExceptionExtensions; @@ -945,6 +946,7 @@ public override bool UpdateSubTree() UpdateAfterChildren(); updateChildrenSizeDependencies(); + applyAutoSize(); UpdateAfterAutoSize(); return true; } @@ -1813,17 +1815,11 @@ protected set } /// - /// The duration which automatic sizing should take. If zero, then it is instantaneous. - /// Otherwise, this is equivalent to applying an automatic size via a resize transform. + /// The duration which automatic sizing should approximately take. If zero, then it is instantaneous. + /// AutoSize is being applied continuously so the actual amount of time taken depends on the overall change in value. /// public float AutoSizeDuration { get; protected set; } - /// - /// The type of easing which should be used for smooth automatic sizing when - /// is non-zero. - /// - public Easing AutoSizeEasing { get; protected set; } - /// /// Fired after this 's is updated through autosize. /// @@ -1938,14 +1934,15 @@ private Vector2 computeAutoSize() private void updateAutoSize() { if (AutoSizeAxes == Axes.None) + { + targetAutoSize.Invalidate(); return; + } - Vector2 b = computeAutoSize() + Padding.Total; + targetAutoSize.Value = computeAutoSize() + Padding.Total; - autoSizeResizeTo(new Vector2( - AutoSizeAxes.HasFlagFast(Axes.X) ? b.X : base.Width, - AutoSizeAxes.HasFlagFast(Axes.Y) ? b.Y : base.Height - ), AutoSizeDuration, AutoSizeEasing); + if (!didInitialAutoSize || AutoSizeDuration <= 0) + autoSizeResizeTo(targetAutoSize.Value, 0); //note that this is called before autoSize becomes valid. may be something to consider down the line. //might work better to add an OnRefresh event in Cached<> and invoke there. @@ -1970,25 +1967,59 @@ private void updateChildrenSizeDependencies() } } - private void autoSizeResizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None) + private void applyAutoSize() + { + if (targetAutoSize.IsValid) + autoSizeResizeTo(targetAutoSize.Value, AutoSizeDuration); + + didInitialAutoSize = true; + } + + private void autoSizeResizeTo(Vector2 targetSize, double duration) { - var currentTransform = TransformsForTargetMember(nameof(baseSize)).FirstOrDefault() as AutoSizeTransform; + targetSize = new Vector2( + AutoSizeAxes.HasFlagFast(Axes.X) ? targetSize.X : base.Width, + AutoSizeAxes.HasFlagFast(Axes.Y) ? targetSize.Y : base.Height + ); - if ((currentTransform?.EndValue ?? Size) != newSize) + if (duration <= 0) { - if (duration == 0) - { - if (currentTransform != null) - ClearTransforms(false, nameof(baseSize)); - baseSize = newSize; - } - else - this.TransformTo(this.PopulateTransform(new AutoSizeTransform { Rewindable = false }, newSize, duration, easing)); + baseSize = targetSize; + targetAutoSize.Invalidate(); + return; + } + + Vector2 newSize = Interpolation.DampContinuously(baseSize, targetSize, duration / 4, Time.Elapsed); + + if (Precision.AlmostEquals(newSize, targetSize, 0.5f)) + { + newSize = targetSize; + targetAutoSize.Invalidate(); } + + baseSize = newSize; } /// - /// A helper property for to change the size of s with . + /// Immediately resizes to the current target size if is non-zero. + /// + protected void FinishAutoSizeTransforms() + { + updateChildrenSizeDependencies(); + + if (targetAutoSize.IsValid) + autoSizeResizeTo(targetAutoSize.Value, 0); + } + + /// + /// When valid, holds the current target size that should be approached when using automatic sizing and is non-zero. + /// + private readonly Cached targetAutoSize = new Cached(); + + private bool didInitialAutoSize; + + /// + /// A helper property for to change the size of s with . /// private Vector2 baseSize { @@ -2000,14 +2031,6 @@ private Vector2 baseSize } } - private class AutoSizeTransform : TransformCustom - { - public AutoSizeTransform() - : base(nameof(baseSize)) - { - } - } - #endregion } } diff --git a/osu.Framework/Graphics/Containers/Container.cs b/osu.Framework/Graphics/Containers/Container.cs index 2c996f04b1..f879f9742a 100644 --- a/osu.Framework/Graphics/Containers/Container.cs +++ b/osu.Framework/Graphics/Containers/Container.cs @@ -492,8 +492,8 @@ public void ChangeChildDepth(T child, float newDepth) } /// - /// The duration which automatic sizing should take. If zero, then it is instantaneous. - /// Otherwise, this is equivalent to applying an automatic size via a resize transform. + /// The duration which automatic sizing should approximately take. If zero, then it is instantaneous. + /// AutoSize is being applied continuously so the actual amount of time taken depends on the overall change in value. /// public new float AutoSizeDuration { @@ -502,14 +502,9 @@ public void ChangeChildDepth(T child, float newDepth) } /// - /// The type of easing which should be used for smooth automatic sizing when - /// is non-zero. + /// Immediately resizes to the current target size if is non-zero. /// - public new Easing AutoSizeEasing - { - get => base.AutoSizeEasing; - set => base.AutoSizeEasing = value; - } + public new void FinishAutoSizeTransforms() => base.FinishAutoSizeTransforms(); public struct Enumerator : IEnumerator { diff --git a/osu.Framework/Graphics/Containers/FlowContainer.cs b/osu.Framework/Graphics/Containers/FlowContainer.cs index 005202c749..5361970787 100644 --- a/osu.Framework/Graphics/Containers/FlowContainer.cs +++ b/osu.Framework/Graphics/Containers/FlowContainer.cs @@ -31,11 +31,7 @@ protected FlowContainer() /// /// The easing that should be used when children are moved to their position in the layout. /// - public Easing LayoutEasing - { - get => AutoSizeEasing; - set => AutoSizeEasing = value; - } + public Easing LayoutEasing { get; set; } /// /// The time it should take to move a child from its current position to its new layout position. diff --git a/osu.Framework/Utils/Interpolation.cs b/osu.Framework/Utils/Interpolation.cs index 68b077bf0a..3df9cbdd83 100644 --- a/osu.Framework/Utils/Interpolation.cs +++ b/osu.Framework/Utils/Interpolation.cs @@ -32,6 +32,21 @@ public static double Damp(double start, double final, double @base, double expon return Lerp(start, final, 1 - Math.Pow(@base, exponent)); } + /// + /// Interpolates between 2 vectors (start and final) using a given base and exponent. + /// + /// The start value. + /// The end value. + /// The base of the exponential. The valid range is [0, 1], where smaller values mean that the final value is achieved more quickly, and values closer to 1 results in slow convergence to the final value. + /// The exponent of the exponential. An exponent of 0 results in the start values, whereas larger exponents make the result converge to the final value. + public static Vector2 Damp(Vector2 start, Vector2 final, double @base, double exponent) + { + if (@base < 0 || @base > 1) + throw new ArgumentOutOfRangeException(nameof(@base), $"{nameof(@base)} has to lie in [0,1], but is {@base}."); + + return Vector2.Lerp(start, final, (float)(1 - Math.Pow(@base, exponent))); + } + /// /// Interpolate the current value towards the target value based on the elapsed time. /// If the current value is updated every frame using this function, the result is approximately frame-rate independent. @@ -50,6 +65,24 @@ public static double DampContinuously(double current, double target, double half return Damp(current, target, 0.5, exponent); } + /// + /// Interpolate the current value towards the target value based on the elapsed time. + /// If the current value is updated every frame using this function, the result is approximately frame-rate independent. + /// + /// + /// Because floating-point errors can accumulate over a long time, this function shouldn't be + /// used for things requiring accurate values. + /// + /// The current value. + /// The target value. + /// The time it takes to reach the middle value of the current and the target value. + /// The elapsed time of the current frame. + public static Vector2 DampContinuously(Vector2 current, Vector2 target, double halfTime, double elapsedTime) + { + double exponent = elapsedTime / halfTime; + return Damp(current, target, 0.5, exponent); + } + /// /// Interpolates between a set of points using a lagrange polynomial. ///