/// <summary> /// Increment selection in the carousel in a chosen direction. /// </summary> /// <param name="direction">The direction to increment. Negative is backwards.</param> /// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param> public void SelectNext(int direction = 1, bool skipDifficulties = true) { var visibleItems = Items.Where(s => !s.Item.Filtered).ToList(); if (!visibleItems.Any()) { return; } DrawableCarouselItem drawable = null; if (selectedBeatmap != null && (drawable = selectedBeatmap.Drawables.FirstOrDefault()) == null) { // if the selected beatmap isn't present yet, we can't correctly change selection. // we can fix this by changing this method to not reference drawables / Items in the first place. return; } int originalIndex = visibleItems.IndexOf(drawable); int currentIndex = originalIndex; // local function to increment the index in the required direction, wrapping over extremities. int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; while (incrementIndex() != originalIndex) { var item = visibleItems[currentIndex].Item; if (item.Filtered || item.State == CarouselItemState.Selected) { continue; } switch (item) { case CarouselBeatmap beatmap: if (skipDifficulties) { continue; } select(beatmap); return; case CarouselBeatmapSet set: if (skipDifficulties) { select(set); } else { select(direction > 0 ? set.Beatmaps.First(b => !b.Filtered) : set.Beatmaps.Last(b => !b.Filtered)); } return; } } }
/// <summary> /// Update a item's x position and multiplicative alpha based on its y position and /// the current scroll position. /// </summary> /// <param name="p">The item to be updated.</param> /// <param name="halfHeight">Half the draw height of the carousel container.</param> private void updateItem(DrawableCarouselItem p, float halfHeight) { var height = p.IsPresent ? p.DrawHeight : 0; float itemDrawY = p.Position.Y - Current + height / 2; float dist = Math.Abs(1f - itemDrawY / halfHeight); // Setting the origin position serves as an additive position on top of potential // local transformation we may want to apply (e.g. when a item gets selected, we // may want to smoothly transform it leftwards.) p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); // We are applying a multiplicative alpha (which is internally done by nesting an // additional container and setting that container's alpha) such that we can // layer transformations on top, with a similar reasoning to the previous comment. p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1)); }
protected override void Update() { base.Update(); if (!itemsCache.IsValid) { updateItems(); } if (!scrollPositionCache.IsValid) { updateScrollPosition(); } float drawHeight = DrawHeight; // Remove all items that should no longer be on-screen scrollableContent.RemoveAll(p => p.Y <Current - p.DrawHeight || p.Y> Current + drawHeight || !p.IsPresent); // Find index range of all items that should be on-screen Trace.Assert(Items.Count == yPositions.Count); int firstIndex = yPositions.BinarySearch(Current - DrawableCarouselItem.MAX_HEIGHT); if (firstIndex < 0) { firstIndex = ~firstIndex; } int lastIndex = yPositions.BinarySearch(Current + drawHeight); if (lastIndex < 0) { lastIndex = ~lastIndex; } int notVisibleCount = 0; // Add those items within the previously found index range that should be displayed. for (int i = firstIndex; i < lastIndex; ++i) { DrawableCarouselItem item = Items[i]; if (!item.Item.Visible) { if (!item.IsPresent) { notVisibleCount++; } continue; } float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); // Only add if we're not already part of the content. if (!scrollableContent.Contains(item)) { // Makes sure headers are always _below_ items, // and depth flows downward. item.Depth = depth; switch (item.LoadState) { case LoadState.NotLoaded: LoadComponentAsync(item); break; case LoadState.Loading: break; default: scrollableContent.Add(item); break; } } else { scrollableContent.ChangeChildDepth(item, depth); } } // this is not actually useful right now, but once we have groups may well be. if (notVisibleCount > 50) { itemsCache.Invalidate(); } // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). float halfHeight = drawHeight / 2; foreach (DrawableCarouselItem p in scrollableContent.Children) { updateItem(p, halfHeight); } }