I have created a user control that scrolls to the right automatically through a given set of images using a Virtualized Stack Panel.
The data context of the user control has any number of images.
These images are loaded initially in a randomized order into a List Collection View. The LCV live sorts by the UnloadedTimestamp
property on each image.
As an image scrolls out of view (detected by the UpdateSnapshot
method, the unloaded timestamp is set which moves the image to the end of the list, allowing for the scrolling marquee to loop indefinitely.
I'm also making use of CompositionTargetEx
which I found here Why is Frame Rate in WPF Irregular and Not Limited To Monitor Refresh? that seemed to improve the performance.
My issue is that the scrolling is "janky" in that it is not smooth and scrolls in small chunks. It's more like clicking the scrollbar arrows repeatedly then dragging the scrollbar thumb.
Can anyone suggest improvements to the code or a better approach? Any input would be much appreciated. Thanks!
public partial class ImageScroller : UserControl
{
private EventHandler<RenderingEventArgs>? renderHandler;
private VirtualizingStackPanel _panel;
private bool _shifting;
private bool ImageUnloaded;
private IList<MyImage> _snapshot = new List<MyImage>();
private IUserTag UserTag => (IUserTag)DataContext;
public ImageScroller()
{
InitializeComponent();
}
public void Shift(ScrollViewer target, double speed = 11.0, double distance = 20.0)
{
ScrollViewer scrollViewer = target;
double startOffset = scrollViewer.HorizontalOffset;
double destinationOffset = scrollViewer.HorizontalOffset + distance;
if (destinationOffset < 0.0)
{
destinationOffset = 0.0;
distance = scrollViewer.HorizontalOffset;
}
if (destinationOffset > scrollViewer.ScrollableWidth)
{
destinationOffset = scrollViewer.ScrollableWidth;
distance = scrollViewer.ScrollableWidth - scrollViewer.HorizontalOffset;
}
double animationTime = Math.Abs(distance / speed);
DateTime startTime = DateTime.Now;
CompositionTargetEx.Rendering -= renderHandler;
renderHandler = (object? sender, RenderingEventArgs e) =>
{
_shifting = true;
if (base.DataContext == BindingOperations.DisconnectedSource || Application.Current == null || Application.Current.MainWindow == null)
{
_shifting = false;
CompositionTargetEx.Rendering -= CompositionTargetEx_Rendering;
}
else
{
if (ImageUnloaded)
{
distance = scrollViewer.ScrollableWidth - scrollViewer.HorizontalOffset;
animationTime = Math.Abs(distance / speed);
startTime = DateTime.Now;
ImageUnloaded = false;
}
double totalSeconds = (DateTime.Now - startTime).TotalSeconds;
if (totalSeconds >= animationTime)
{
scrollViewer.ScrollToHorizontalOffset(destinationOffset);
_shifting = false;
CompositionTargetEx.Rendering -= renderHandler;
}
if (distance < 0.0)
{
scrollViewer.ScrollToHorizontalOffset(startOffset - totalSeconds * speed);
}
else
{
scrollViewer.ScrollToHorizontalOffset(startOffset + totalSeconds * speed);
}
}
};
CompositionTargetEx.Rendering += renderHandler;
}
private void VirtualizingStackPanel_Loaded(object sender, RoutedEventArgs e)
{
_panel = (VirtualizingStackPanel)sender;
_panel.ScrollOwner.ScrollChanged -= _scrollChanged;
_panel.ScrollOwner.ScrollChanged += _scrollChanged;
if (_shifting)
{
return;
}
IUserTag userTag = UserTag;
if (userTag != null && userTag.RotatingImages.Count > 1 || _panel.ScrollOwner.VerticalOffset != _panel.ScrollOwner.ScrollableWidth)
{
base.Dispatcher.BeginInvoke((Action)delegate
{
Shift(_panel.ScrollOwner, 2.0, _panel.ScrollOwner.ScrollableWidth);
}, DispatcherPriority.ContextIdle, null);
}
}
private void _scrollChanged(object sender, RoutedEventArgs e)
{
if (!_shifting)
{
IUserTag userTag = UserTag;
if (userTag != null && userTag.RotatingImages.Count > 1)
{
base.Dispatcher.BeginInvoke((Action)delegate
{
Shift(_panel.ScrollOwner, 2.0, _panel.ScrollOwner.ScrollableWidth);
}, DispatcherPriority.ContextIdle, null);
}
}
UpdateSnapshot();
}
private void UpdateSnapshot()
{
Rect layoutBounds = LayoutInformation.GetLayoutSlot(_panel);
List<MyImage> list = (from visualChild in _panel.GetChildren()
let childBounds = LayoutInformation.GetLayoutSlot(visualChild)
where layoutBounds.Contains(childBounds) || layoutBounds.IntersectsWith(childBounds)
select visualChild.DataContext).Cast<MyImage>().ToList();
foreach (MyImage item in _snapshot.Except(list))
{
item.UnloadedTimestamp = DateTimeOffset.Now;
ImageUnloaded = true;
}
_snapshot = list;
}
}