Merge pull request #10973 from peppy/song-select-track-selected-better-2

Improve scroll tracking of selected item
This commit is contained in:
Dan Balasescu 2020-11-30 17:59:42 +09:00 committed by GitHub
commit 3ddba8e5b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 52 deletions

View File

@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
get get
{ {
foreach (var item in ScrollableContent) foreach (var item in Scroll.Children)
{ {
yield return item; yield return item;

View File

@ -12,7 +12,19 @@ using osuTK.Input;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
public class OsuScrollContainer : ScrollContainer<Drawable> public class OsuScrollContainer : OsuScrollContainer<Drawable>
{
public OsuScrollContainer()
{
}
public OsuScrollContainer(Direction direction)
: base(direction)
{
}
}
public class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable
{ {
public const float SCROLL_BAR_HEIGHT = 10; public const float SCROLL_BAR_HEIGHT = 10;
public const float SCROLL_BAR_PADDING = 3; public const float SCROLL_BAR_PADDING = 3;

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select
/// </summary> /// </summary>
public bool BeatmapSetsLoaded { get; private set; } public bool BeatmapSetsLoaded { get; private set; }
private readonly CarouselScrollContainer scroll; protected readonly CarouselScrollContainer Scroll;
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>(); private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
@ -112,9 +112,9 @@ namespace osu.Game.Screens.Select
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null; selectedBeatmapSet = null;
ScrollableContent.Clear(false); Scroll.Clear(false);
itemsCache.Invalidate(); itemsCache.Invalidate();
scrollPositionCache.Invalidate(); ScrollToSelected();
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
FlushPendingFilterOperations(); FlushPendingFilterOperations();
@ -130,9 +130,7 @@ namespace osu.Game.Screens.Select
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>(); private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
private readonly Cached itemsCache = new Cached(); private readonly Cached itemsCache = new Cached();
private readonly Cached scrollPositionCache = new Cached(); private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None;
protected readonly Container<DrawableCarouselItem> ScrollableContent;
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>(); public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = scroll = new CarouselScrollContainer Children = new Drawable[]
{ {
Masking = false, setPool,
RelativeSizeAxes = Axes.Both, Scroll = new CarouselScrollContainer
Children = new Drawable[]
{ {
setPool, RelativeSizeAxes = Axes.Both,
ScrollableContent = new Container<DrawableCarouselItem>
{
RelativeSizeAxes = Axes.X,
}
} }
} }
}; };
@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
RightClickScrollingEnabled.TriggerChange(); RightClickScrollingEnabled.TriggerChange();
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy(); itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// The position of the lower visible bound with respect to the current scroll position. /// The position of the lower visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom; private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
/// <summary> /// <summary>
/// The position of the upper visible bound with respect to the current scroll position. /// The position of the upper visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleUpperBound => scroll.Current - BleedTop; private float visibleUpperBound => Scroll.Current - BleedTop;
public void FlushPendingFilterOperations() public void FlushPendingFilterOperations()
{ {
@ -468,8 +461,8 @@ namespace osu.Game.Screens.Select
root.Filter(activeCriteria); root.Filter(activeCriteria);
itemsCache.Invalidate(); itemsCache.Invalidate();
if (alwaysResetScrollPosition || !scroll.UserScrolling) if (alwaysResetScrollPosition || !Scroll.UserScrolling)
ScrollToSelected(); ScrollToSelected(true);
} }
} }
@ -478,7 +471,12 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// Scroll to the current <see cref="SelectedBeatmap"/>. /// Scroll to the current <see cref="SelectedBeatmap"/>.
/// </summary> /// </summary>
public void ScrollToSelected() => scrollPositionCache.Invalidate(); /// <param name="immediate">
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
/// </param>
public void ScrollToSelected(bool immediate = false) =>
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
#region Key / button selection logic #region Key / button selection logic
@ -488,12 +486,12 @@ namespace osu.Game.Screens.Select
{ {
case Key.Left: case Key.Left:
if (!e.Repeat) if (!e.Repeat)
beginRepeatSelection(() => SelectNext(-1, true), e.Key); beginRepeatSelection(() => SelectNext(-1), e.Key);
return true; return true;
case Key.Right: case Key.Right:
if (!e.Repeat) if (!e.Repeat)
beginRepeatSelection(() => SelectNext(1, true), e.Key); beginRepeatSelection(() => SelectNext(), e.Key);
return true; return true;
} }
@ -580,6 +578,11 @@ namespace osu.Game.Screens.Select
if (revalidateItems) if (revalidateItems)
updateYPositions(); updateYPositions();
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
if (pendingScrollOperation != PendingScrollOperation.None)
updateScrollPosition();
// This data is consumed to find the currently displayable range. // This data is consumed to find the currently displayable range.
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn. // This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
var newDisplayRange = getDisplayRange(); var newDisplayRange = getDisplayRange();
@ -594,7 +597,7 @@ namespace osu.Game.Screens.Select
{ {
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
foreach (var panel in ScrollableContent.Children) foreach (var panel in Scroll.Children)
{ {
if (toDisplay.Remove(panel.Item)) if (toDisplay.Remove(panel.Item))
{ {
@ -620,24 +623,14 @@ namespace osu.Game.Screens.Select
panel.Depth = item.CarouselYPosition; panel.Depth = item.CarouselYPosition;
panel.Y = item.CarouselYPosition; panel.Y = item.CarouselYPosition;
ScrollableContent.Add(panel); Scroll.Add(panel);
} }
} }
} }
// Finally, if the filtered items have changed, animate drawables to their new locations.
// This is common if a selected/collapsed state has changed.
if (revalidateItems)
{
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
{
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
}
}
// Update externally controlled state of currently visible items (e.g. x-offset and opacity). // Update externally controlled state of currently visible items (e.g. x-offset and opacity).
// This is a per-frame update on all drawable panels. // This is a per-frame update on all drawable panels.
foreach (DrawableCarouselItem item in ScrollableContent.Children) foreach (DrawableCarouselItem item in Scroll.Children)
{ {
updateItem(item); updateItem(item);
@ -670,14 +663,6 @@ namespace osu.Game.Screens.Select
return (firstIndex, lastIndex); return (firstIndex, lastIndex);
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!scrollPositionCache.IsValid)
updateScrollPosition();
}
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem) private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{ {
if (weakItem.NewValue.TryGetTarget(out var item)) if (weakItem.NewValue.TryGetTarget(out var item))
@ -789,7 +774,8 @@ namespace osu.Game.Screens.Select
} }
currentY += visibleHalfHeight; currentY += visibleHalfHeight;
ScrollableContent.Height = currentY;
Scroll.ScrollContent.Height = currentY;
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
{ {
@ -809,12 +795,31 @@ namespace osu.Game.Screens.Select
if (firstScroll) if (firstScroll)
{ {
// reduce movement when first displaying the carousel. // reduce movement when first displaying the carousel.
scroll.ScrollTo(scrollTarget.Value - 200, false); Scroll.ScrollTo(scrollTarget.Value - 200, false);
firstScroll = false; firstScroll = false;
} }
scroll.ScrollTo(scrollTarget.Value); switch (pendingScrollOperation)
scrollPositionCache.Validate(); {
case PendingScrollOperation.Standard:
Scroll.ScrollTo(scrollTarget.Value);
break;
case PendingScrollOperation.Immediate:
// in order to simplify animation logic, rather than using the animated version of ScrollTo,
// we take the difference in scroll height and apply to all visible panels.
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
// to enter clamp-special-case mode where it animates completely differently to normal.
float scrollChange = scrollTarget.Value - Scroll.Current;
Scroll.ScrollTo(scrollTarget.Value, false);
foreach (var i in Scroll.Children)
i.Y += scrollChange;
break;
}
pendingScrollOperation = PendingScrollOperation.None;
} }
} }
@ -844,7 +849,7 @@ namespace osu.Game.Screens.Select
/// <param name="parent">For nested items, the parent of the item to be updated.</param> /// <param name="parent">For nested items, the parent of the item to be updated.</param>
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
{ {
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
float itemDrawY = posInScroll.Y - visibleUpperBound; float itemDrawY = posInScroll.Y - visibleUpperBound;
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
@ -858,6 +863,13 @@ namespace osu.Game.Screens.Select
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
} }
private enum PendingScrollOperation
{
None,
Standard,
Immediate,
}
/// <summary> /// <summary>
/// A carousel item strictly used for binary search purposes. /// A carousel item strictly used for binary search purposes.
/// </summary> /// </summary>
@ -889,7 +901,7 @@ namespace osu.Game.Screens.Select
} }
} }
private class CarouselScrollContainer : OsuScrollContainer protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem>
{ {
private bool rightMouseScrollBlocked; private bool rightMouseScrollBlocked;
@ -898,6 +910,12 @@ namespace osu.Game.Screens.Select
/// </summary> /// </summary>
public bool UserScrolling { get; private set; } public bool UserScrolling { get; private set; }
public CarouselScrollContainer()
{
// size is determined by the carousel itself, due to not all content necessarily being loaded.
ScrollContent.AutoSizeAxes = Axes.None;
}
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
} }
protected override void Update()
{
base.Update();
// position updates should not occur if the item is filtered away.
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
if (!Item.Visible)
return;
float targetY = Item.CarouselYPosition;
if (Precision.AlmostEquals(targetY, Y))
Y = targetY;
else
// algorithm for this is taken from ScrollContainer.
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
}
protected override void UpdateItem() protected override void UpdateItem()
{ {
base.UpdateItem(); base.UpdateItem();