1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 17:27:24 +08:00

Merge pull request #28184 from frenzibyte/footer-v2-transitions

Add basic animation to new footer and buttons
This commit is contained in:
Dean Herbert 2024-06-08 19:43:23 +09:00 committed by GitHub
commit 7fc6ad5340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 271 additions and 242 deletions

View File

@ -2,34 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2.Footer;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
{
private ScreenFooterButtonRandom randomButton = null!;
private ScreenFooterButtonMods modsButton = null!;
private bool nextRandomCalled;
private bool previousRandomCalled;
private DummyOverlay overlay = null!;
private ScreenFooter screenFooter = null!;
private TestModSelectOverlay overlay = null!;
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
@ -37,160 +25,70 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp]
public void SetUp() => Schedule(() =>
{
nextRandomCalled = false;
previousRandomCalled = false;
ScreenFooter footer;
Children = new Drawable[]
{
overlay = new TestModSelectOverlay
{
Padding = new MarginPadding
{
Bottom = ScreenFooter.HEIGHT
}
},
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = footer = new ScreenFooter(),
Child = screenFooter = new ScreenFooter(),
},
overlay = new DummyOverlay()
};
footer.AddButton(modsButton = new ScreenFooterButtonMods { Current = SelectedMods }, overlay);
footer.AddButton(randomButton = new ScreenFooterButtonRandom
screenFooter.SetButtons(new ScreenFooterButton[]
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true
new ScreenFooterButtonMods(overlay) { Current = SelectedMods },
new ScreenFooterButtonRandom(),
new ScreenFooterButtonOptions(),
});
footer.AddButton(new ScreenFooterButtonOptions());
overlay.Hide();
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)));
AddStep("show footer", () => screenFooter.Show());
}
/// <summary>
/// Transition when moving from a screen with no buttons to a screen with buttons.
/// </summary>
[Test]
public void TestMods()
public void TestButtonsIn()
{
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
AddStep("two mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock() });
AddStep("three mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
AddStep("four mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() });
AddStep("five mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() });
AddStep("modified", () => SelectedMods.Value = new List<Mod> { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + one", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + two", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + three", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + four", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddWaitStep("wait", 3);
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddWaitStep("wait", 3);
AddStep("five mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() });
}
/// <summary>
/// Transition when moving from a screen with buttons to a screen with no buttons.
/// </summary>
[Test]
public void TestShowOptions()
public void TestButtonsOut()
{
AddStep("enable options", () =>
AddStep("clear buttons", () => screenFooter.SetButtons(Array.Empty<ScreenFooterButton>()));
}
/// <summary>
/// Transition when moving from a screen with buttons to a screen with buttons.
/// </summary>
[Test]
public void TestReplaceButtons()
{
AddStep("replace buttons", () => screenFooter.SetButtons(new[]
{
var optionsButton = this.ChildrenOfType<ScreenFooterButton>().Last();
optionsButton.Enabled.Value = true;
optionsButton.TriggerClick();
});
new ScreenFooterButton { Text = "One", Action = () => { } },
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
}));
}
[Test]
public void TestState()
private partial class TestModSelectOverlay : UserModSelectOverlay
{
AddToggleStep("set options enabled state", state => this.ChildrenOfType<ScreenFooterButton>().Last().Enabled.Value = state);
}
[Test]
public void TestFooterRandom()
{
AddStep("press F2", () => InputManager.Key(Key.F2));
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRandomViaMouse()
{
AddStep("click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRewind()
{
AddStep("press Shift+F2", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.F2);
InputManager.ReleaseKey(Key.F2);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaShiftMouseLeft()
{
AddStep("shift + click button", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaMouseRight()
{
AddStep("right click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Right);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestOverlayPresent()
{
AddStep("Press F1", () =>
{
InputManager.MoveMouseTo(modsButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible);
AddStep("Hide", () => overlay.Hide());
}
private partial class DummyOverlay : ShearedOverlayContainer
{
public DummyOverlay()
: base(OverlayColourScheme.Green)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "An overlay";
}
protected override bool ShowPresets => true;
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2.Footer;
@ -26,12 +27,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestSceneScreenFooterButtonMods()
{
Add(footerButtonMods = new TestScreenFooterButtonMods
Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay())
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
X = -100,
Action = () => { },
X = -100,
});
}
@ -112,9 +113,19 @@ namespace osu.Game.Tests.Visual.UserInterface
return expectedValue == footerButtonMods.MultiplierText.Current.Value;
}
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
}
private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods
{
public new OsuSpriteText MultiplierText => base.MultiplierText;
public TestScreenFooterButtonMods(ModSelectOverlay overlay)
: base(overlay)
{
}
}
}
}

View File

@ -2,54 +2,33 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Footer
{
public partial class ScreenFooter : InputBlockingContainer
public partial class ScreenFooter : OverlayContainer
{
private const int height = 60;
private const int padding = 60;
private const float delay_per_button = 30;
public const int HEIGHT = 60;
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
/// <param name="button">The button to be added.</param>
/// <param name="overlay">The <see cref="OverlayContainer"/> to be toggled by this button.</param>
public void AddButton(ScreenFooterButton button, OverlayContainer? overlay = null)
{
if (overlay != null)
{
overlays.Add(overlay);
button.Action = () => showOverlay(overlay);
button.OverlayState.BindTo(overlay.State);
}
buttons.Add(button);
}
private void showOverlay(OverlayContainer overlay)
{
foreach (var o in overlays)
{
if (o == overlay)
o.ToggleVisibility();
else
o.Hide();
}
}
private FillFlowContainer<ScreenFooterButton> buttons = null!;
private FillFlowContainer<ScreenFooterButton> buttonsFlow = null!;
private Container<ScreenFooterButton> removedButtonsContainer = null!;
public ScreenFooter()
{
RelativeSizeAxes = Axes.X;
Height = height;
Height = HEIGHT;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
}
@ -64,9 +43,10 @@ namespace osu.Game.Screens.Footer
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
},
buttons = new FillFlowContainer<ScreenFooterButton>
buttonsFlow = new FillFlowContainer<ScreenFooterButton>
{
Position = new Vector2(ScreenBackButton.BUTTON_WIDTH + padding, 10),
Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
Y = 10f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
@ -80,7 +60,95 @@ namespace osu.Game.Screens.Footer
Origin = Anchor.BottomLeft,
Action = () => { },
},
removedButtonsContainer = new Container<ScreenFooterButton>
{
Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
Y = 10f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
},
};
}
protected override void PopIn()
{
this.MoveToY(0, 400, Easing.OutQuint)
.FadeIn(400, Easing.OutQuint);
}
protected override void PopOut()
{
this.MoveToY(HEIGHT, 400, Easing.OutQuint)
.FadeOut(400, Easing.OutQuint);
}
public void SetButtons(IReadOnlyList<ScreenFooterButton> buttons)
{
overlays.Clear();
var oldButtons = buttonsFlow.ToArray();
for (int i = 0; i < oldButtons.Length; i++)
{
var oldButton = oldButtons[i];
buttonsFlow.Remove(oldButton, false);
removedButtonsContainer.Add(oldButton);
if (buttons.Count > 0)
makeButtonDisappearToRightAndExpire(oldButton, i, oldButtons.Length);
else
makeButtonDisappearToBottomAndExpire(oldButton, i, oldButtons.Length);
}
for (int i = 0; i < buttons.Count; i++)
{
var newButton = buttons[i];
if (newButton.Overlay != null)
{
newButton.Action = () => showOverlay(newButton.Overlay);
overlays.Add(newButton.Overlay);
}
Debug.Assert(!newButton.IsLoaded);
buttonsFlow.Add(newButton);
int index = i;
// ensure transforms are added after LoadComplete to not be aborted by the FinishTransforms call.
newButton.OnLoadComplete += _ =>
{
if (oldButtons.Length > 0)
makeButtonAppearFromLeft(newButton, index, buttons.Count, 240);
else
makeButtonAppearFromBottom(newButton, index);
};
}
}
private void makeButtonAppearFromLeft(ScreenFooterButton button, int index, int count, float startDelay)
=> button.AppearFromLeft(startDelay + (count - index) * delay_per_button);
private void makeButtonAppearFromBottom(ScreenFooterButton button, int index)
=> button.AppearFromBottom(index * delay_per_button);
private void makeButtonDisappearToRightAndExpire(ScreenFooterButton button, int index, int count)
=> button.DisappearToRightAndExpire((count - index) * delay_per_button);
private void makeButtonDisappearToBottomAndExpire(ScreenFooterButton button, int index, int count)
=> button.DisappearToBottomAndExpire((count - index) * delay_per_button);
private void showOverlay(OverlayContainer overlay)
{
foreach (var o in overlays)
{
if (o == overlay)
o.ToggleVisibility();
else
o.Hide();
}
}
}
}

View File

@ -55,7 +55,7 @@ namespace osu.Game.Screens.Footer
set => icon.Icon = value;
}
protected LocalisableString Text
public LocalisableString Text
{
set => text.Text = value;
}
@ -69,87 +69,94 @@ namespace osu.Game.Screens.Footer
private readonly Box glowBox;
private readonly Box flashLayer;
public ScreenFooterButton()
public readonly OverlayContainer? Overlay;
public ScreenFooterButton(OverlayContainer? overlay = null)
{
Overlay = overlay;
Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT);
Child = new Container
Children = new Drawable[]
{
EdgeEffect = new EdgeEffectParameters
new Container
{
Type = EdgeEffectType.Shadow,
Radius = 4,
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
},
Shear = BUTTON_SHEAR,
Masking = true,
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
backgroundBox = new Box
EdgeEffect = new EdgeEffectParameters
{
RelativeSizeAxes = Axes.Both
Type = EdgeEffectType.Shadow,
Radius = 4,
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
},
glowBox = new Box
Shear = BUTTON_SHEAR,
Masking = true,
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both
},
// For elements that should not be sheared.
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = -BUTTON_SHEAR,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
backgroundBox = new Box
{
TextContainer = new Container
RelativeSizeAxes = Axes.Both
},
glowBox = new Box
{
RelativeSizeAxes = Axes.Both
},
// For elements that should not be sheared.
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = -BUTTON_SHEAR,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 42,
AutoSizeAxes = Axes.Both,
Child = text = new OsuSpriteText
TextContainer = new Container
{
// figma design says the size is 16, but due to the issues with font sizes 19 matches better
Font = OsuFont.TorusAlternate.With(size: 19),
AlwaysPresent = true
}
},
icon = new SpriteIcon
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 42,
AutoSizeAxes = Axes.Both,
Child = text = new OsuSpriteText
{
// figma design says the size is 16, but due to the issues with font sizes 19 matches better
Font = OsuFont.TorusAlternate.With(size: 19),
AlwaysPresent = true
}
},
icon = new SpriteIcon
{
Y = 12,
Size = new Vector2(20),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
}
},
new Container
{
Shear = -BUTTON_SHEAR,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
Y = -CORNER_RADIUS,
Size = new Vector2(120, 6),
Masking = true,
CornerRadius = 3,
Child = bar = new Box
{
Y = 12,
Size = new Vector2(20),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
}
},
new Container
{
Shear = -BUTTON_SHEAR,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
Y = -CORNER_RADIUS,
Size = new Vector2(120, 6),
Masking = true,
CornerRadius = 3,
Child = bar = new Box
RelativeSizeAxes = Axes.Both,
}
},
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
}
Colour = Colour4.White.Opacity(0.9f),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
},
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White.Opacity(0.9f),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
},
}
};
}
@ -157,6 +164,9 @@ namespace osu.Game.Screens.Footer
{
base.LoadComplete();
if (Overlay != null)
OverlayState.BindTo(Overlay.State);
OverlayState.BindValueChanged(_ => updateDisplay());
Enabled.BindValueChanged(_ => updateDisplay(), true);
@ -215,5 +225,41 @@ namespace osu.Game.Screens.Footer
glowBox.FadeColour(ColourInfo.GradientVertical(buttonAccentColour.Opacity(0f), buttonAccentColour.Opacity(0.2f)), 150, Easing.OutQuint);
}
public void AppearFromLeft(double delay)
{
Content.MoveToX(-300f)
.FadeOut()
.Delay(delay)
.MoveToX(0f, 240, Easing.OutCubic)
.FadeIn(240, Easing.OutCubic);
}
public void AppearFromBottom(double delay)
{
Content.MoveToY(100f)
.FadeOut()
.Delay(delay)
.MoveToY(0f, 240, Easing.OutCubic)
.FadeIn(240, Easing.OutCubic);
}
public void DisappearToRightAndExpire(double delay)
{
Content.Delay(delay)
.FadeOut(240, Easing.InOutCubic)
.MoveToX(300f, 360, Easing.InOutCubic);
this.Delay(Content.LatestTransformEndTime - Time.Current).Expire();
}
public void DisappearToBottomAndExpire(double delay)
{
Content.Delay(delay)
.FadeOut(240, Easing.InOutCubic)
.MoveToY(100f, 240, Easing.InOutCubic);
this.Delay(Content.LatestTransformEndTime - Time.Current).Expire();
}
}
}

View File

@ -20,6 +20,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play.HUD;
@ -59,6 +60,11 @@ namespace osu.Game.Screens.SelectV2.Footer
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ScreenFooterButtonMods(ModSelectOverlay overlay)
: base(overlay)
{
}
[BackgroundDependencyLoader]
private void load()
{