From b12adc7bb2a04c74ca3d41ae1ce046b3b2f3cb03 Mon Sep 17 00:00:00 2001 From: melimion <33512073+melimion@users.noreply.github.com> Date: Fri, 18 Oct 2019 20:28:29 +0300 Subject: [PATCH] Fix ListView.ScrollTo does not work in WPF (#7947) * ScrollTo implementation added * ScrollToPosition.Center implemented * animation implemented --- Xamarin.Forms.Platform.WPF/Helpers/TreeHelper.cs | 16 +++ .../Renderers/ListViewRenderer.cs | 146 ++++++++++++++++++++- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/Xamarin.Forms.Platform.WPF/Helpers/TreeHelper.cs b/Xamarin.Forms.Platform.WPF/Helpers/TreeHelper.cs index 31acf9e..bd9f50b 100644 --- a/Xamarin.Forms.Platform.WPF/Helpers/TreeHelper.cs +++ b/Xamarin.Forms.Platform.WPF/Helpers/TreeHelper.cs @@ -227,5 +227,21 @@ namespace Xamarin.Forms.Platform.WPF.Helpers } } } + public static T FindVisualChild(this DependencyObject parent) where T : Visual + { + var child = default(T); + + int numVisuals = VisualTreeHelper.GetChildrenCount(parent); + for (var i = 0; i < numVisuals; i++) + { + var v = (Visual)VisualTreeHelper.GetChild(parent, i); + child = v as T ?? FindVisualChild(v); + if (child != null) + { + break; + } + } + return child; + } } } diff --git a/Xamarin.Forms.Platform.WPF/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.WPF/Renderers/ListViewRenderer.cs index 7307cd4..658429b 100644 --- a/Xamarin.Forms.Platform.WPF/Renderers/ListViewRenderer.cs +++ b/Xamarin.Forms.Platform.WPF/Renderers/ListViewRenderer.cs @@ -1,7 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; +using Xamarin.Forms.Platform.WPF.Helpers; using WList = System.Windows.Controls.ListView; using WpfScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility; @@ -9,6 +18,17 @@ namespace Xamarin.Forms.Platform.WPF { public class ListViewRenderer : ViewRenderer { + class ScrollViewerBehavior + { + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), + typeof(ScrollViewerBehavior), new UIPropertyMetadata(0.0, OnVerticalOffsetChanged)); + + static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) + { + var scrollViewer = target as ScrollViewer; + scrollViewer?.ScrollToVerticalOffset((double)e.NewValue); + } + } ITemplatedItemsView TemplatedItemsView => Element; WpfScrollBarVisibility? _defaultHorizontalScrollVisibility; WpfScrollBarVisibility? _defaultVerticalScrollVisibility; @@ -25,6 +45,7 @@ namespace Xamarin.Forms.Platform.WPF if (e.OldElement != null) // Clear old element event { e.OldElement.ItemSelected -= OnElementItemSelected; + e.OldElement.ScrollToRequested -= OnElementScrollToRequested; var templatedItems = ((ITemplatedItemsView)e.OldElement).TemplatedItems; templatedItems.CollectionChanged -= OnCollectionChanged; @@ -34,6 +55,7 @@ namespace Xamarin.Forms.Platform.WPF if (e.NewElement != null) { e.NewElement.ItemSelected += OnElementItemSelected; + e.NewElement.ScrollToRequested += OnElementScrollToRequested; if (Control == null) // Construct and SetNativeControl and suscribe control event { @@ -44,6 +66,8 @@ namespace Xamarin.Forms.Platform.WPF Style = (System.Windows.Style)System.Windows.Application.Current.Resources["ListViewTemplate"] }; + VirtualizingPanel.SetVirtualizationMode(listView, VirtualizationMode.Recycling); + VirtualizingPanel.SetScrollUnit(listView, ScrollUnit.Pixel); SetNativeControl(listView); Control.MouseUp += OnNativeMouseUp; @@ -68,6 +92,11 @@ namespace Xamarin.Forms.Platform.WPF base.OnElementChanged(e); } + void OnElementScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + var scrollArgs = (ITemplatedItemsListScrollToRequestedEventArgs)e; + ScrollTo(scrollArgs.Group, scrollArgs.Item, e.Position, e.ShouldAnimate); + } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { @@ -234,5 +263,120 @@ namespace Xamarin.Forms.Platform.WPF { UpdateItemSource(); } + void ScrollTo(object group, object item, ScrollToPosition toPosition, bool shouldAnimate) + { + var viewer = Control.FindVisualChild(); + if (viewer == null) + { + RoutedEventHandler loadedHandler = null; + loadedHandler = (o, e) => + { + Control.Loaded -= loadedHandler; + Device.BeginInvokeOnMainThread(() => { ScrollTo(group, item, toPosition, shouldAnimate); }); + }; + Control.Loaded += loadedHandler; + return; + } + var templatedItems = TemplatedItemsView.TemplatedItems; + Tuple location = templatedItems.GetGroupAndIndexOfItem(group, item); + if (location.Item1 == -1 || location.Item2 == -1) + return; + + var t = templatedItems.GetGroup(location.Item1).ToArray(); + var c = t[location.Item2]; + + Device.BeginInvokeOnMainThread(() => + { + ScrollToPositionInView(Control, viewer, c, toPosition, shouldAnimate); + }); + } + static void ScrollToPositionInView(WList control, ScrollViewer sv, object item, ScrollToPosition position, bool animated) + { + // Scroll immediately if possible + if (!TryScrollToPositionInView(control, sv, item, position, animated)) + { + control.ScrollIntoView(item); + control.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => + { + TryScrollToPositionInView(control, sv, item, position, animated); + })); + } + } + + static bool TryScrollToPositionInView(ItemsControl itemsControl, ScrollViewer sv, object item, ScrollToPosition position, bool animated) + { + var cell = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement; + if (cell == null) + return false; + + var cellHeight = cell.RenderSize.Height; + var offsetInViewport = cell.TransformToAncestor(sv).Transform(new System.Windows.Point(0, 0)).Y; + + double newOffsetInViewport; + switch (position) + { + case ScrollToPosition.Start: + newOffsetInViewport = 0; + break; + case ScrollToPosition.Center: + newOffsetInViewport = (sv.ViewportHeight - cellHeight) / 2; + break; + case ScrollToPosition.End: + newOffsetInViewport = sv.ViewportHeight - cellHeight; + break; + case ScrollToPosition.MakeVisible: + { + var startOffset = 0; + var endOffset = sv.ViewportHeight - cellHeight; + var startDistance = Math.Abs(offsetInViewport - startOffset); + var endDistance = Math.Abs(offsetInViewport - endOffset); + // already in view, no action + if (endOffset >= offsetInViewport && startOffset <= offsetInViewport) + newOffsetInViewport = offsetInViewport; + else if (startDistance < endDistance) + newOffsetInViewport = startOffset; + else + newOffsetInViewport = endOffset; + } + break; + default: + throw new ArgumentOutOfRangeException(nameof(position), position, null); + } + + if (newOffsetInViewport != offsetInViewport) + { + var offset = sv.VerticalOffset + offsetInViewport - newOffsetInViewport; + ScrollToOffset(sv, offset, animated); + } + return true; + } + + static void ScrollToOffset(ScrollViewer sv, double offset, bool animated) + { + if (sv.CanContentScroll) + { + var maxPossibleValue = sv.ExtentHeight - sv.ViewportHeight; + offset = Math.Min(maxPossibleValue, Math.Max(0, offset)); + if (animated) + { + var animation = new DoubleAnimation + { + From = sv.VerticalOffset, + To = offset, + DecelerationRatio = 0.2, + Duration = new Duration(TimeSpan.FromMilliseconds(200)) + }; + var storyboard = new Storyboard(); + storyboard.Children.Add(animation); + Storyboard.SetTarget(animation, sv); + Storyboard.SetTargetProperty(animation, new PropertyPath(ScrollViewerBehavior.VerticalOffsetProperty)); + storyboard.Begin(); + } + else + { + sv.ScrollToVerticalOffset(offset); + } + } + } } } -- 2.7.4