mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 08:52:55 +08:00
Rework score panel tracking to fix visual edge cases
This commit is contained in:
parent
5530e2a1db
commit
ec16b0fc5a
@ -3,7 +3,6 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -102,39 +101,6 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
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", () =>
|
||||
{
|
||||
Child = panel = new ScorePanel(score)
|
||||
@ -144,9 +110,5 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -47,6 +48,7 @@ namespace osu.Game.Screens.Ranking
|
||||
private StatisticsPanel statisticsPanel;
|
||||
private Drawable bottomPanel;
|
||||
private ScorePanelList scorePanelList;
|
||||
private Container<ScorePanel> detachedPanelContainer;
|
||||
|
||||
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
|
||||
{
|
||||
@ -89,11 +91,15 @@ namespace osu.Game.Screens.Ranking
|
||||
SelectedScore = { BindTarget = SelectedScore },
|
||||
PostExpandAction = () => statisticsPanel.ToggleVisibility()
|
||||
},
|
||||
detachedPanelContainer = new Container<ScorePanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
statisticsPanel = new StatisticsPanel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Score = { BindTarget = SelectedScore }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -169,7 +175,7 @@ namespace osu.Game.Screens.Ranking
|
||||
var req = FetchScores(scores => Schedule(() =>
|
||||
{
|
||||
foreach (var s in scores)
|
||||
scorePanelList.AddScore(s);
|
||||
addScore(s);
|
||||
}));
|
||||
|
||||
if (req != null)
|
||||
@ -208,42 +214,71 @@ namespace osu.Game.Screens.Ranking
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (panel.State == PanelState.Contracted)
|
||||
panel.FadeIn(150);
|
||||
else
|
||||
{
|
||||
panel.MoveTo(panel.GetTrackingPosition(), 150, Easing.OutQuint).OnComplete(p =>
|
||||
{
|
||||
scorePanelList.HandleScroll = true;
|
||||
p.Tracking = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Detach and move into the local container.
|
||||
scorePanelList.Detach(expandedPanel);
|
||||
detachedPanelContainer.Add(expandedPanel);
|
||||
|
||||
// Move into its original location in the local container.
|
||||
var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos);
|
||||
expandedPanel.MoveTo(origLocation);
|
||||
expandedPanel.MoveToX(origLocation.X);
|
||||
|
||||
// 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))
|
||||
contracted.FadeOut(150, Easing.OutQuint);
|
||||
scorePanelList.HandleInput = false;
|
||||
|
||||
// Dim background.
|
||||
Background.FadeTo(0.1f, 150);
|
||||
|
||||
foreach (var panel in scorePanelList.Panels)
|
||||
{
|
||||
if (panel.State == PanelState.Contracted)
|
||||
panel.FadeOut(150, Easing.OutQuint);
|
||||
else
|
||||
{
|
||||
scorePanelList.HandleScroll = false;
|
||||
detachedPanel = expandedPanel;
|
||||
}
|
||||
else if (detachedPanel != null)
|
||||
{
|
||||
var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft;
|
||||
|
||||
panel.Tracking = false;
|
||||
panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
// Remove from the local container and re-attach.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,11 +78,6 @@ namespace osu.Game.Screens.Ranking
|
||||
public event Action<PanelState> StateChanged;
|
||||
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>
|
||||
/// Whether this <see cref="ScorePanel"/> can enter into an <see cref="PanelState.Expanded"/> state.
|
||||
/// </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()
|
||||
{
|
||||
topLayerContent?.FadeOut(content_fade_duration).Expire();
|
||||
@ -269,8 +250,8 @@ namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
base.Size = value;
|
||||
|
||||
if (trackingComponent != null)
|
||||
trackingComponent.Size = value;
|
||||
if (trackingContainer != null)
|
||||
trackingContainer.Size = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,26 +274,14 @@ namespace osu.Game.Screens.Ranking
|
||||
|| topLayerContainer.ReceivePositionalInputAt(screenSpacePos)
|
||||
|| middleLayerContainer.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
private TrackingComponent trackingComponent;
|
||||
private ScorePanelTrackingContainer trackingContainer;
|
||||
|
||||
public TrackingComponent CreateTrackingComponent() => trackingComponent ??= new TrackingComponent(this);
|
||||
|
||||
public class TrackingComponent : Drawable
|
||||
public ScorePanelTrackingContainer CreateTrackingContainer()
|
||||
{
|
||||
public readonly ScorePanel Panel;
|
||||
if (trackingContainer != null)
|
||||
throw new InvalidOperationException("A score panel container has already been created.");
|
||||
|
||||
public TrackingComponent(ScorePanel panel)
|
||||
{
|
||||
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;
|
||||
return trackingContainer = new ScorePanelTrackingContainer(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,6 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public float CurrentScrollPosition => scroll.Current;
|
||||
|
||||
public IReadOnlyList<ScorePanel> Panels => panels;
|
||||
private readonly Container<ScorePanel> panels;
|
||||
|
||||
private readonly Flow flow;
|
||||
private readonly Scroll scroll;
|
||||
private ScorePanel expandedPanel;
|
||||
@ -49,10 +46,9 @@ namespace osu.Game.Screens.Ranking
|
||||
InternalChild = scroll = new Scroll
|
||||
{
|
||||
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[]
|
||||
{
|
||||
panels = new Container<ScorePanel> { RelativeSizeAxes = Axes.Both },
|
||||
flow = new Flow
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -72,34 +68,14 @@ namespace osu.Game.Screens.Ranking
|
||||
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>
|
||||
/// Adds a <see cref="ScoreInfo"/> to this list.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
Tracking = true,
|
||||
PostExpandAction = () => PostExpandAction?.Invoke()
|
||||
}.With(p =>
|
||||
{
|
||||
@ -110,8 +86,7 @@ namespace osu.Game.Screens.Ranking
|
||||
};
|
||||
});
|
||||
|
||||
panels.Add(panel);
|
||||
flow.Add(panel.CreateTrackingComponent().With(d =>
|
||||
flow.Add(panel.CreateTrackingContainer().With(d =>
|
||||
{
|
||||
d.Anchor = 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;
|
||||
}
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -187,15 +164,53 @@ namespace osu.Game.Screens.Ranking
|
||||
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 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>()
|
||||
.OrderByDescending(s => s.Panel.Score.TotalScore)
|
||||
.ThenBy(s => s.Panel.Score.OnlineScoreID);
|
||||
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
|
||||
.OrderByDescending(s => s.Panel.Score.TotalScore)
|
||||
.ThenBy(s => s.Panel.Score.OnlineScoreID);
|
||||
}
|
||||
|
||||
private class Scroll : OsuScrollContainer
|
||||
|
35
osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs
Normal file
35
osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user