Rework score panel tracking to fix visual edge cases

This commit is contained in:
smoogipoo 2020-06-19 17:28:35 +09:00
parent 5530e2a1db
commit ec16b0fc5a
5 changed files with 155 additions and 139 deletions

View File

@ -3,7 +3,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -102,39 +101,6 @@ namespace osu.Game.Tests.Visual.Ranking
AddWaitStep("wait for transition", 10); AddWaitStep("wait for transition", 10);
} }
[Test]
public void TestSceneTrackingScorePanel()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
addPanelStep(score, PanelState.Contracted);
AddStep("enable tracking", () =>
{
panel.Anchor = Anchor.CentreLeft;
panel.Origin = Anchor.CentreLeft;
panel.Tracking = true;
Add(panel.CreateTrackingComponent().With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}));
});
assertTracking(true);
AddStep("expand panel", () => panel.State = PanelState.Expanded);
AddWaitStep("wait for transition", 2);
assertTracking(true);
AddStep("stop tracking", () => panel.Tracking = false);
assertTracking(false);
AddStep("start tracking", () => panel.Tracking = true);
assertTracking(true);
}
private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () =>
{ {
Child = panel = new ScorePanel(score) Child = panel = new ScorePanel(score)
@ -144,9 +110,5 @@ namespace osu.Game.Tests.Visual.Ranking
State = state State = state
}; };
}); });
private void assertTracking(bool tracking) => AddAssert($"{(tracking ? "is" : "is not")} tracking", () =>
Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.TopLeft, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.TopLeft) == tracking
&& Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.BottomRight, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.BottomRight) == tracking);
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -47,6 +48,7 @@ namespace osu.Game.Screens.Ranking
private StatisticsPanel statisticsPanel; private StatisticsPanel statisticsPanel;
private Drawable bottomPanel; private Drawable bottomPanel;
private ScorePanelList scorePanelList; private ScorePanelList scorePanelList;
private Container<ScorePanel> detachedPanelContainer;
protected ResultsScreen(ScoreInfo score, bool allowRetry = true) protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
{ {
@ -89,11 +91,15 @@ namespace osu.Game.Screens.Ranking
SelectedScore = { BindTarget = SelectedScore }, SelectedScore = { BindTarget = SelectedScore },
PostExpandAction = () => statisticsPanel.ToggleVisibility() PostExpandAction = () => statisticsPanel.ToggleVisibility()
}, },
detachedPanelContainer = new Container<ScorePanel>
{
RelativeSizeAxes = Axes.Both
},
statisticsPanel = new StatisticsPanel statisticsPanel = new StatisticsPanel
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Score = { BindTarget = SelectedScore } Score = { BindTarget = SelectedScore }
} },
} }
} }
}, },
@ -169,7 +175,7 @@ namespace osu.Game.Screens.Ranking
var req = FetchScores(scores => Schedule(() => var req = FetchScores(scores => Schedule(() =>
{ {
foreach (var s in scores) foreach (var s in scores)
scorePanelList.AddScore(s); addScore(s);
})); }));
if (req != null) if (req != null)
@ -208,42 +214,71 @@ namespace osu.Game.Screens.Ranking
return base.OnExiting(next); return base.OnExiting(next);
} }
private void addScore(ScoreInfo score)
{
var panel = scorePanelList.AddScore(score);
if (detachedPanel != null)
panel.Alpha = 0;
}
private ScorePanel detachedPanel;
private void onStatisticsStateChanged(ValueChangedEvent<Visibility> state) private void onStatisticsStateChanged(ValueChangedEvent<Visibility> state)
{ {
if (state.NewValue == Visibility.Hidden) if (state.NewValue == Visibility.Visible)
{ {
Background.FadeTo(0.5f, 150); // Detach the panel in its original location, and move into the desired location in the local container.
var expandedPanel = scorePanelList.GetPanelForScore(SelectedScore.Value);
var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft;
foreach (var panel in scorePanelList.Panels) // Detach and move into the local container.
{ scorePanelList.Detach(expandedPanel);
if (panel.State == PanelState.Contracted) detachedPanelContainer.Add(expandedPanel);
panel.FadeIn(150);
else // Move into its original location in the local container.
{ var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos);
panel.MoveTo(panel.GetTrackingPosition(), 150, Easing.OutQuint).OnComplete(p => expandedPanel.MoveTo(origLocation);
{ expandedPanel.MoveToX(origLocation.X);
scorePanelList.HandleScroll = true;
p.Tracking = true; // Move into the final location.
}); expandedPanel.MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint);
}
} // Hide contracted panels.
} foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
else contracted.FadeOut(150, Easing.OutQuint);
{ scorePanelList.HandleInput = false;
// Dim background.
Background.FadeTo(0.1f, 150); Background.FadeTo(0.1f, 150);
foreach (var panel in scorePanelList.Panels) detachedPanel = expandedPanel;
}
else if (detachedPanel != null)
{ {
if (panel.State == PanelState.Contracted) var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft;
panel.FadeOut(150, Easing.OutQuint);
else
{
scorePanelList.HandleScroll = false;
panel.Tracking = false; // Remove from the local container and re-attach.
panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); detachedPanelContainer.Remove(detachedPanel);
} scorePanelList.Attach(detachedPanel);
}
// Move into its original location in the attached container.
var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos);
detachedPanel.MoveTo(origLocation);
detachedPanel.MoveToX(origLocation.X);
// Move into the final location.
detachedPanel.MoveToX(0, 150, Easing.OutQuint);
// Show contracted panels.
foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeIn(150, Easing.OutQuint);
scorePanelList.HandleInput = true;
// Un-dim background.
Background.FadeTo(0.5f, 150);
detachedPanel = null;
} }
} }
} }

View File

@ -78,11 +78,6 @@ namespace osu.Game.Screens.Ranking
public event Action<PanelState> StateChanged; public event Action<PanelState> StateChanged;
public Action PostExpandAction; public Action PostExpandAction;
/// <summary>
/// Whether this <see cref="ScorePanel"/> should track the position of the tracking component created via <see cref="CreateTrackingComponent"/>.
/// </summary>
public bool Tracking;
/// <summary> /// <summary>
/// Whether this <see cref="ScorePanel"/> can enter into an <see cref="PanelState.Expanded"/> state. /// Whether this <see cref="ScorePanel"/> can enter into an <see cref="PanelState.Expanded"/> state.
/// </summary> /// </summary>
@ -194,20 +189,6 @@ namespace osu.Game.Screens.Ranking
} }
} }
protected override void Update()
{
base.Update();
if (Tracking && trackingComponent != null)
Position = GetTrackingPosition();
}
public Vector2 GetTrackingPosition()
{
Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft);
return topLeftPos - AnchorPosition + OriginPosition;
}
private void updateState() private void updateState()
{ {
topLayerContent?.FadeOut(content_fade_duration).Expire(); topLayerContent?.FadeOut(content_fade_duration).Expire();
@ -269,8 +250,8 @@ namespace osu.Game.Screens.Ranking
{ {
base.Size = value; base.Size = value;
if (trackingComponent != null) if (trackingContainer != null)
trackingComponent.Size = value; trackingContainer.Size = value;
} }
} }
@ -293,26 +274,14 @@ namespace osu.Game.Screens.Ranking
|| topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || topLayerContainer.ReceivePositionalInputAt(screenSpacePos)
|| middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos);
private TrackingComponent trackingComponent; private ScorePanelTrackingContainer trackingContainer;
public TrackingComponent CreateTrackingComponent() => trackingComponent ??= new TrackingComponent(this); public ScorePanelTrackingContainer CreateTrackingContainer()
public class TrackingComponent : Drawable
{ {
public readonly ScorePanel Panel; if (trackingContainer != null)
throw new InvalidOperationException("A score panel container has already been created.");
public TrackingComponent(ScorePanel panel) return trackingContainer = new ScorePanelTrackingContainer(this);
{
Panel = panel;
}
// In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container.
// So by forwarding input events, we remove the need to consider the order in which input is handled.
protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos);
public override bool IsPresent => Panel.IsPresent;
} }
} }
} }

View File

@ -32,9 +32,6 @@ namespace osu.Game.Screens.Ranking
public float CurrentScrollPosition => scroll.Current; public float CurrentScrollPosition => scroll.Current;
public IReadOnlyList<ScorePanel> Panels => panels;
private readonly Container<ScorePanel> panels;
private readonly Flow flow; private readonly Flow flow;
private readonly Scroll scroll; private readonly Scroll scroll;
private ScorePanel expandedPanel; private ScorePanel expandedPanel;
@ -49,10 +46,9 @@ namespace osu.Game.Screens.Ranking
InternalChild = scroll = new Scroll InternalChild = scroll = new Scroll
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
HandleScroll = () => HandleScroll && expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel.
Children = new Drawable[] Children = new Drawable[]
{ {
panels = new Container<ScorePanel> { RelativeSizeAxes = Axes.Both },
flow = new Flow flow = new Flow
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -72,34 +68,14 @@ namespace osu.Game.Screens.Ranking
SelectedScore.BindValueChanged(selectedScoreChanged, true); SelectedScore.BindValueChanged(selectedScoreChanged, true);
} }
private bool handleScroll = true;
public bool HandleScroll
{
get => handleScroll;
set
{
handleScroll = value;
foreach (var p in panels)
p.CanExpand = value;
scroll.ScrollbarVisible = value;
if (!value)
scroll.ScrollTo(CurrentScrollPosition, false);
}
}
/// <summary> /// <summary>
/// Adds a <see cref="ScoreInfo"/> to this list. /// Adds a <see cref="ScoreInfo"/> to this list.
/// </summary> /// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to add.</param> /// <param name="score">The <see cref="ScoreInfo"/> to add.</param>
public void AddScore(ScoreInfo score) public ScorePanel AddScore(ScoreInfo score)
{ {
var panel = new ScorePanel(score) var panel = new ScorePanel(score)
{ {
Tracking = true,
PostExpandAction = () => PostExpandAction?.Invoke() PostExpandAction = () => PostExpandAction?.Invoke()
}.With(p => }.With(p =>
{ {
@ -110,8 +86,7 @@ namespace osu.Game.Screens.Ranking
}; };
}); });
panels.Add(panel); flow.Add(panel.CreateTrackingContainer().With(d =>
flow.Add(panel.CreateTrackingComponent().With(d =>
{ {
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre; d.Origin = Anchor.Centre;
@ -132,6 +107,8 @@ namespace osu.Game.Screens.Ranking
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
} }
} }
return panel;
} }
/// <summary> /// <summary>
@ -187,13 +164,51 @@ namespace osu.Game.Screens.Ranking
flow.Padding = new MarginPadding { Horizontal = offset }; flow.Padding = new MarginPadding { Horizontal = offset };
} }
private class Flow : FillFlowContainer<ScorePanel.TrackingComponent> private bool handleInput = true;
public bool HandleInput
{
get => handleInput;
set
{
handleInput = value;
scroll.ScrollbarVisible = value;
}
}
public override bool PropagatePositionalInputSubTree => HandleInput && base.PropagatePositionalInputSubTree;
public override bool PropagateNonPositionalInputSubTree => HandleInput && base.PropagateNonPositionalInputSubTree;
public IEnumerable<ScorePanel> GetScorePanels() => flow.Select(t => t.Panel);
public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel;
public void Detach(ScorePanel panel)
{
var container = flow.FirstOrDefault(t => t.Panel == panel);
if (container == null)
throw new InvalidOperationException("Panel is not contained by the score panel list.");
container.Detach();
}
public void Attach(ScorePanel panel)
{
var container = flow.FirstOrDefault(t => t.Panel == panel);
if (container == null)
throw new InvalidOperationException("Panel is not contained by the score panel list.");
container.Attach();
}
private class Flow : FillFlowContainer<ScorePanelTrackingContainer>
{ {
public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren); public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
private IEnumerable<ScorePanel.TrackingComponent> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanel.TrackingComponent>() private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
.OrderByDescending(s => s.Panel.Score.TotalScore) .OrderByDescending(s => s.Panel.Score.TotalScore)
.ThenBy(s => s.Panel.Score.OnlineScoreID); .ThenBy(s => s.Panel.Score.OnlineScoreID);
} }

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Screens.Ranking
{
public class ScorePanelTrackingContainer : CompositeDrawable
{
public readonly ScorePanel Panel;
public ScorePanelTrackingContainer(ScorePanel panel)
{
Panel = panel;
Attach();
}
public void Detach()
{
if (InternalChildren.Count == 0)
throw new InvalidOperationException("Score panel container is not attached.");
RemoveInternal(Panel);
}
public void Attach()
{
if (InternalChildren.Count > 0)
throw new InvalidOperationException("Score panel container is already attached.");
AddInternal(Panel);
}
}
}