diff options
author | Jason Smith <jason.smith@xamarin.com> | 2016-03-22 13:02:25 -0700 |
---|---|---|
committer | Jason Smith <jason.smith@xamarin.com> | 2016-03-22 16:13:41 -0700 |
commit | 17fdde66d94155fc62a034fa6658995bef6fd6e5 (patch) | |
tree | b5e5073a2a7b15cdbe826faa5c763e270a505729 /Xamarin.Forms.Platform.WinRT/ListViewRenderer.cs | |
download | xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.gz xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.bz2 xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.zip |
Initial import
Diffstat (limited to 'Xamarin.Forms.Platform.WinRT/ListViewRenderer.cs')
-rw-r--r-- | Xamarin.Forms.Platform.WinRT/ListViewRenderer.cs | 634 |
1 files changed, 634 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.WinRT/ListViewRenderer.cs b/Xamarin.Forms.Platform.WinRT/ListViewRenderer.cs new file mode 100644 index 00000000..5c231738 --- /dev/null +++ b/Xamarin.Forms.Platform.WinRT/ListViewRenderer.cs @@ -0,0 +1,634 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using WListView = Windows.UI.Xaml.Controls.ListView; +using WBinding = Windows.UI.Xaml.Data.Binding; +using WApp = Windows.UI.Xaml.Application; + +#if WINDOWS_UWP + +namespace Xamarin.Forms.Platform.UWP +#else + +namespace Xamarin.Forms.Platform.WinRT +#endif +{ + public class ListViewRenderer : ViewRenderer<ListView, FrameworkElement> + { +#if !WINDOWS_UWP + public static readonly DependencyProperty HighlightWhenSelectedProperty = DependencyProperty.RegisterAttached("HighlightWhenSelected", typeof(bool), typeof(ListViewRenderer), + new PropertyMetadata(false)); + + public static bool GetHighlightWhenSelected(DependencyObject dependencyObject) + { + return (bool)dependencyObject.GetValue(HighlightWhenSelectedProperty); + } + + public static void SetHighlightWhenSelected(DependencyObject dependencyObject, bool value) + { + dependencyObject.SetValue(HighlightWhenSelectedProperty, value); + } +#endif + + protected WListView List { get; private set; } + + protected override void OnElementChanged(ElementChangedEventArgs<ListView> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + { + e.OldElement.ItemSelected -= OnElementItemSelected; + e.OldElement.ScrollToRequested -= OnElementScrollToRequested; + } + + if (e.NewElement != null) + { + e.NewElement.ItemSelected += OnElementItemSelected; + e.NewElement.ScrollToRequested += OnElementScrollToRequested; + + if (List == null) + { + List = new WListView + { + IsSynchronizedWithCurrentItem = false, + ItemTemplate = (Windows.UI.Xaml.DataTemplate)WApp.Current.Resources["CellTemplate"], + HeaderTemplate = (Windows.UI.Xaml.DataTemplate)WApp.Current.Resources["View"], + FooterTemplate = (Windows.UI.Xaml.DataTemplate)WApp.Current.Resources["View"], + ItemContainerStyle = (Windows.UI.Xaml.Style)WApp.Current.Resources["FormsListViewItem"], + GroupStyleSelector = (GroupStyleSelector)WApp.Current.Resources["ListViewGroupSelector"] + }; + + // In order to support tapping on elements within a list item, we handle + // ListView.Tapped (which can be handled by child elements in the list items + // and prevented from bubbling up) rather than ListView.ItemClick + List.Tapped += ListOnTapped; + + if (ShouldCustomHighlight) + { + List.SelectionChanged += OnControlSelectionChanged; + } + + List.SetBinding(ItemsControl.ItemsSourceProperty, ""); + } + + // WinRT throws an exception if you set ItemsSource directly to a CVS, so bind it. + List.DataContext = new CollectionViewSource { Source = Element.ItemsSource, IsSourceGrouped = Element.IsGroupingEnabled }; + + UpdateGrouping(); + UpdateHeader(); + UpdateFooter(); + ClearSizeEstimate(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == ListView.IsGroupingEnabledProperty.PropertyName) + { + UpdateGrouping(); + } + else if (e.PropertyName == ListView.HeaderProperty.PropertyName) + { + UpdateHeader(); + } + else if (e.PropertyName == ListView.FooterProperty.PropertyName) + { + UpdateFooter(); + } + else if (e.PropertyName == ListView.RowHeightProperty.PropertyName) + { + ClearSizeEstimate(); + } + else if (e.PropertyName == ListView.HasUnevenRowsProperty.PropertyName) + { + ClearSizeEstimate(); + } + else if (e.PropertyName == ListView.ItemTemplateProperty.PropertyName) + { + ClearSizeEstimate(); + } + else if (e.PropertyName == ListView.ItemsSourceProperty.PropertyName) + { + ClearSizeEstimate(); + ((CollectionViewSource)List.DataContext).Source = Element.ItemsSource; + } + } + + protected override void Dispose(bool disposing) + { + if (List != null) + { + List.Tapped -= ListOnTapped; + + if (ShouldCustomHighlight) + { + List.SelectionChanged -= OnControlSelectionChanged; + } + + List.DataContext = null; + List = null; + } + + if (_zoom != null) + { + _zoom.ViewChangeCompleted -= OnViewChangeCompleted; + _zoom = null; + } + + base.Dispose(disposing); + } + + static IEnumerable<T> FindDescendants<T>(DependencyObject dobj) where T : DependencyObject + { + int count = VisualTreeHelper.GetChildrenCount(dobj); + for (var i = 0; i < count; i++) + { + DependencyObject element = VisualTreeHelper.GetChild(dobj, i); + if (element is T) + yield return (T)element; + + foreach (T descendant in FindDescendants<T>(element)) + yield return descendant; + } + } + + sealed class BrushedElement + { + bool _isBound; + + public BrushedElement(FrameworkElement element, WBinding brushBinding = null, Brush brush = null) + { + Element = element; + BrushBinding = brushBinding; + Brush = brush; + } + + public Brush Brush { get; } + + public WBinding BrushBinding { get; } + + public FrameworkElement Element { get; } + + public bool IsBound + { + get { return BrushBinding != null; } + } + } + + SemanticZoom _zoom; + ScrollViewer _scrollViewer; + ContentControl _headerControl; + readonly List<BrushedElement> _highlightedElements = new List<BrushedElement>(); + + bool ShouldCustomHighlight + { + get + { +#if WINDOWS_UWP + return false; +#else + return Device.Idiom == TargetIdiom.Phone; +#endif + } + } + + void ClearSizeEstimate() + { + Element.ClearValue(CellControl.MeasuredEstimateProperty); + } + + void UpdateFooter() + { + List.Footer = ((IListViewController)Element).FooterElement; + } + + void UpdateHeader() + { + List.Header = ((IListViewController)Element).HeaderElement; + } + + void UpdateGrouping() + { + bool grouping = Element.IsGroupingEnabled; + + ((CollectionViewSource)List.DataContext).IsSourceGrouped = grouping; + + if (grouping && Element.TemplatedItems.ShortNames != null) + { + if (_zoom == null) + { + ScrollViewer.SetIsVerticalScrollChainingEnabled(List, false); + + var grid = new GridView { ItemsSource = Element.TemplatedItems.ShortNames, Style = (Windows.UI.Xaml.Style)WApp.Current.Resources["JumpListGrid"] }; + + ScrollViewer.SetIsHorizontalScrollChainingEnabled(grid, false); + + _zoom = new SemanticZoom { IsZoomOutButtonEnabled = false, ZoomedOutView = grid }; + + // Since we reuse our ScrollTo, we have to wait until the change completes or ChangeView has odd behavior. + _zoom.ViewChangeCompleted += OnViewChangeCompleted; + + // Specific order to let SNC unparent the ListView for us + SetNativeControl(_zoom); + _zoom.ZoomedInView = List; + } + else + { + _zoom.CanChangeViews = true; + } + } + else + { + if (_zoom != null) + _zoom.CanChangeViews = false; + else if (List != Control) + SetNativeControl(List); + } + } + + async void OnViewChangeCompleted(object sender, SemanticZoomViewChangedEventArgs e) + { + if (e.IsSourceZoomedInView) + return; + + // HACK: Technically more than one short name could be the same, this will potentially find the wrong one in that case + var item = (string)e.SourceItem.Item; + + int index = Element.TemplatedItems.ShortNames.IndexOf(item); + if (index == -1) + return; + + TemplatedItemsList<ItemsView<Cell>, Cell> til = Element.TemplatedItems.GetGroup(index); + if (til.Count == 0) + return; // FIXME + + // Delay until after the SemanticZoom change _actually_ finishes, fixes tons of odd issues on Phone w/ virtualization. + if (Device.Idiom == TargetIdiom.Phone) + await Task.Delay(1); + + ScrollTo(til.ListProxy.ProxiedEnumerable, til.ListProxy[0], ScrollToPosition.Start, true, true); + } + + async void ScrollTo(object group, object item, ScrollToPosition toPosition, bool shouldAnimate, bool includeGroup = false, bool previouslyFailed = false) + { + ScrollViewer viewer = GetScrollViewer(); + if (viewer == null) + { + RoutedEventHandler loadedHandler = null; + loadedHandler = async (o, e) => + { + List.Loaded -= loadedHandler; + + // Here we try to avoid an exception, see explanation at bottom + await Dispatcher.RunIdleAsync(args => { ScrollTo(group, item, toPosition, shouldAnimate, includeGroup); }); + }; + List.Loaded += loadedHandler; + return; + } + + Tuple<int, int> location = Element.TemplatedItems.GetGroupAndIndexOfItem(group, item); + if (location.Item1 == -1 || location.Item2 == -1) + return; + + object[] t = Element.TemplatedItems.GetGroup(location.Item1).ItemsSource.Cast<object>().ToArray(); + object c = t[location.Item2]; + + double viewportHeight = viewer.ViewportHeight; + + var semanticLocation = new SemanticZoomLocation { Item = c }; + + switch (toPosition) + { + case ScrollToPosition.Start: + { + List.ScrollIntoView(c, ScrollIntoViewAlignment.Leading); + return; + } + + case ScrollToPosition.MakeVisible: + { + List.ScrollIntoView(c, ScrollIntoViewAlignment.Default); + return; + } + + case ScrollToPosition.End: + case ScrollToPosition.Center: + { + var content = (FrameworkElement)List.ItemTemplate.LoadContent(); + content.DataContext = c; + content.Measure(new Windows.Foundation.Size(viewer.ActualWidth, double.PositiveInfinity)); + + double tHeight = content.DesiredSize.Height; + + if (toPosition == ScrollToPosition.Center) + semanticLocation.Bounds = new Rect(0, viewportHeight / 2 - tHeight / 2, 0, 0); + else + semanticLocation.Bounds = new Rect(0, viewportHeight - tHeight, 0, 0); + + break; + } + } + + // Waiting for loaded doesn't seem to be enough anymore; the ScrollViewer does not appear until after Loaded. + // Even if the ScrollViewer is present, an invoke at low priority fails (E_FAIL) presumably because the items are + // still loading. An invoke at idle sometimes work, but isn't reliable enough, so we'll just have to commit + // treason and use a blanket catch for the E_FAIL and try again. + try + { + List.MakeVisible(semanticLocation); + } + catch (Exception) + { + if (previouslyFailed) + return; + + Task.Delay(1).ContinueWith(ct => { ScrollTo(group, item, toPosition, shouldAnimate, includeGroup, true); }, TaskScheduler.FromCurrentSynchronizationContext()).WatchForError(); + } + } + + void OnElementScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + ScrollTo(e.Group, e.Item, e.Position, e.ShouldAnimate); + } + + T GetFirstDescendant<T>(DependencyObject element) where T : FrameworkElement + { + int count = VisualTreeHelper.GetChildrenCount(element); + for (var i = 0; i < count; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + T target = child as T ?? GetFirstDescendant<T>(child); + if (target != null) + return target; + } + + return null; + } + + ContentControl GetHeaderControl() + { + if (_headerControl == null) + { + ScrollViewer viewer = GetScrollViewer(); + if (viewer == null) + return null; + + var presenter = GetFirstDescendant<ItemsPresenter>(viewer); + if (presenter == null) + return null; + + _headerControl = GetFirstDescendant<ContentControl>(presenter); + } + + return _headerControl; + } + + ScrollViewer GetScrollViewer() + { + if (_scrollViewer == null) + { + _scrollViewer = List.GetFirstDescendant<ScrollViewer>(); + } + + return _scrollViewer; + } + + void OnElementItemSelected(object sender, SelectedItemChangedEventArgs e) + { + if (Element == null) + return; + + if (_deferSelection) + { + // If we get more than one of these, that's okay; we only want the latest one + _deferredSelectedItemChangedEvent = new Tuple<object, SelectedItemChangedEventArgs>(sender, e); + return; + } + + if (e.SelectedItem == null) + { + List.SelectedIndex = -1; + return; + } + + var index = 0; + if (Element.IsGroupingEnabled) + { + int selectedItemIndex = Element.TemplatedItems.GetGlobalIndexOfItem(e.SelectedItem); + var leftOver = 0; + int groupIndex = Element.TemplatedItems.GetGroupIndexFromGlobal(selectedItemIndex, out leftOver); + + index = selectedItemIndex - (groupIndex + 1); + } + else + { + index = Element.TemplatedItems.GetGlobalIndexOfItem(e.SelectedItem); + } + + List.SelectedIndex = index; + } + + void ListOnTapped(object sender, TappedRoutedEventArgs args) + { + var orig = args.OriginalSource as DependencyObject; + int index = -1; + + // Work our way up the tree until we find the actual list item + // the user tapped on + while (orig != null && orig != List) + { + var lv = orig as ListViewItem; + + if (lv != null) + { + index = Element.TemplatedItems.GetGlobalIndexOfItem(lv.Content); + break; + } + + orig = VisualTreeHelper.GetParent(orig); + } + + if (index > -1) + { + OnListItemClicked(index); + } + } + + void OnListItemClicked(int index) + { +#if !WINDOWS_UWP + // If we're on the phone , we need to cache the selected item in case the handler + // we're about to call changes any item indexes; + // in some cases, those index changes will throw an exception we can't catch if + // the listview has an item selected + object selectedItem = null; + if (Device.Idiom == TargetIdiom.Phone) + { + selectedItem = List.SelectedItem; + List.SelectedIndex = -1; + _deferSelection = true; + } +#endif + + Element.NotifyRowTapped(index); + +#if !WINDOWS_UWP + + if (Device.Idiom != TargetIdiom.Phone || List == null) + { + return; + } + + _deferSelection = false; + + if (_deferredSelectedItemChangedEvent != null) + { + // If there was a selection change attempt while RowTapped was being handled, replay it + OnElementItemSelected(_deferredSelectedItemChangedEvent.Item1, _deferredSelectedItemChangedEvent.Item2); + _deferredSelectedItemChangedEvent = null; + } + else if (List?.SelectedIndex == -1 && selectedItem != null) + { + // Otherwise, set the selection back to whatever it was before all this started + List.SelectedItem = selectedItem; + } +#endif + } + + void OnControlSelectionChanged(object sender, SelectionChangedEventArgs e) + { + RestorePreviousSelectedVisual(); + + if (e.AddedItems.Count == 0) + return; + + object cell = e.AddedItems[0]; + if (cell == null) + return; + + if (ShouldCustomHighlight) + { + FrameworkElement element = FindElement(cell); + if (element != null) + { + SetSelectedVisual(element); + } + } + } + + FrameworkElement FindElement(object cell) + { + foreach (CellControl selector in FindDescendants<CellControl>(List)) + { + if (ReferenceEquals(cell, selector.DataContext)) + return selector; + } + + return null; + } + +#if WINDOWS_UWP + void RestorePreviousSelectedVisual() + { + } + + void SetSelectedVisual(FrameworkElement element) + { + } +#else + void RestorePreviousSelectedVisual() + { + foreach (BrushedElement highlight in _highlightedElements) + { + if (highlight.IsBound) + { + highlight.Element.SetForeground(highlight.BrushBinding); + } + else + { + highlight.Element.SetForeground(highlight.Brush); + } + } + + _highlightedElements.Clear(); + } + + void SetSelectedVisual(FrameworkElement element) + { + // Find all labels in children and set their foreground color to accent color + IEnumerable<FrameworkElement> elementsToHighlight = FindPhoneHighlights(element); + var systemAccentBrush = (Brush)WApp.Current.Resources["SystemColorControlAccentBrush"]; + + foreach (FrameworkElement toHighlight in elementsToHighlight) + { + Brush brush = null; + WBinding binding = toHighlight.GetForegroundBinding(); + if (binding == null) + brush = toHighlight.GetForeground(); + + var brushedElement = new BrushedElement(toHighlight, binding, brush); + _highlightedElements.Add(brushedElement); + + toHighlight.SetForeground(systemAccentBrush); + } + } + + IEnumerable<FrameworkElement> FindPhoneHighlights(FrameworkElement element) + { + FrameworkElement parent = element; + while (true) + { + element = parent; + if (element is CellControl) + break; + + parent = VisualTreeHelper.GetParent(element) as FrameworkElement; + if (parent == null) + { + parent = element; + break; + } + } + + return FindPhoneHighlightCore(parent); + } + + IEnumerable<FrameworkElement> FindPhoneHighlightCore(DependencyObject element) + { + int children = VisualTreeHelper.GetChildrenCount(element); + for (var i = 0; i < children; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + var label = child as LabelRenderer; + var childElement = child as FrameworkElement; + if (childElement != null && (GetHighlightWhenSelected(childElement) || label != null)) + { + if (label != null) + yield return label.Control; + else + yield return childElement; + } + + foreach (FrameworkElement recursedElement in FindPhoneHighlightCore(childElement)) + yield return recursedElement; + } + } +#endif + + bool _deferSelection; + Tuple<object, SelectedItemChangedEventArgs> _deferredSelectedItemChangedEvent; + } +}
\ No newline at end of file |