diff options
Diffstat (limited to 'Xamarin.Forms.Platform.WP8/ListViewRenderer.cs')
-rw-r--r-- | Xamarin.Forms.Platform.WP8/ListViewRenderer.cs | 717 |
1 files changed, 717 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.WP8/ListViewRenderer.cs b/Xamarin.Forms.Platform.WP8/ListViewRenderer.cs new file mode 100644 index 00000000..362c993c --- /dev/null +++ b/Xamarin.Forms.Platform.WP8/ListViewRenderer.cs @@ -0,0 +1,717 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using Microsoft.Phone.Controls; +using GestureEventArgs = System.Windows.Input.GestureEventArgs; +using SLButton = System.Windows.Controls.Button; +using SLBinding = System.Windows.Data.Binding; + +namespace Xamarin.Forms.Platform.WinPhone +{ + // Fixes a weird crash, don't ask. + internal class FixedLongListSelector : LongListSelector + { + bool _isInPullToRefresh; + + System.Windows.Point _lastPosition; + double _pullToRefreshStatus; + + public FixedLongListSelector() + { + Loaded += OnLoaded; + } + + public bool IsInPullToRefresh + { + get { return _isInPullToRefresh; } + private set + { + if (_isInPullToRefresh == value) + return; + _isInPullToRefresh = value; + + if (_isInPullToRefresh) + { + EventHandler handler = PullToRefreshStarted; + if (handler != null) + handler(this, EventArgs.Empty); + } + else + { + EventHandler handler = PullToRefreshStatus >= 1 ? PullToRefreshCompleted : PullToRefreshCanceled; + if (handler != null) + handler(this, EventArgs.Empty); + _pullToRefreshStatus = 0; + } + } + } + + public double PullToRefreshStatus + { + get { return _pullToRefreshStatus; } + set + { + if (_pullToRefreshStatus == value) + return; + _pullToRefreshStatus = value; + EventHandler handler = PullToRefreshStatusUpdated; + if (handler != null) + handler(this, EventArgs.Empty); + } + } + + public ViewportControl ViewportControl { get; private set; } + + bool ViewportAtTop + { + get { return ViewportControl.Viewport.Top == 0; } + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (ViewportControl != null) + { + ViewportControl.ViewportChanged -= OnViewportChanged; + ViewportControl.ManipulationStateChanged -= OnManipulationStateChanged; + } + + ViewportControl = (ViewportControl)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(this, 0), 0), 0); + ViewportControl.ViewportChanged += OnViewportChanged; + ViewportControl.ManipulationStateChanged += OnManipulationStateChanged; + } + + public event EventHandler PullToRefreshCanceled; + + public event EventHandler PullToRefreshCompleted; + + public event EventHandler PullToRefreshStarted; + + public event EventHandler PullToRefreshStatusUpdated; + + protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize) + { + try + { + return base.MeasureOverride(availableSize); + } + catch (ArgumentException) + { + return base.MeasureOverride(availableSize); + } + } + + void OnFrameReported(object sender, TouchFrameEventArgs e) + { + TouchPoint touchPoint; + try + { + touchPoint = e.GetPrimaryTouchPoint(this); + } + catch (Exception) + { + return; + } + + if (touchPoint == null || touchPoint.Action != TouchAction.Move) + return; + + System.Windows.Point position = touchPoint.Position; + + if (IsInPullToRefresh) + { + double delta = position.Y - _lastPosition.Y; + PullToRefreshStatus += delta / 150.0; + } + + _lastPosition = position; + } + + void OnLoaded(object sender, RoutedEventArgs routedEventArgs) + { + Loaded -= OnLoaded; + Unloaded += OnUnloaded; + + Touch.FrameReported += OnFrameReported; + } + + void OnManipulationStateChanged(object o, ManipulationStateChangedEventArgs args) + { + switch (ViewportControl.ManipulationState) + { + case ManipulationState.Idle: + // thing is rested + IsInPullToRefresh = false; + break; + case ManipulationState.Manipulating: + // user interaction + IsInPullToRefresh = ViewportAtTop; + break; + case ManipulationState.Animating: + // user let go + IsInPullToRefresh = false; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) + { + Loaded += OnLoaded; + Unloaded -= OnUnloaded; + + Touch.FrameReported -= OnFrameReported; + } + + void OnViewportChanged(object o, ViewportChangedEventArgs args) + { + if (ViewportControl.ManipulationState == ManipulationState.Manipulating) + IsInPullToRefresh = ViewportAtTop; + } + } + + public class ListViewRenderer : ViewRenderer<ListView, LongListSelector> + { + public static readonly DependencyProperty HighlightWhenSelectedProperty = DependencyProperty.RegisterAttached("HighlightWhenSelected", typeof(bool), typeof(ListViewRenderer), + new PropertyMetadata(false)); + + readonly List<Tuple<FrameworkElement, SLBinding, Brush>> _previousHighlights = new List<Tuple<FrameworkElement, SLBinding, Brush>>(); + + Animatable _animatable; + object _fromNative; + bool _itemNeedsSelecting; + FixedLongListSelector _listBox; + System.Windows.Controls.ProgressBar _progressBar; + + ViewportControl _viewport; + + public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + SizeRequest result = base.GetDesiredSize(widthConstraint, heightConstraint); + result.Minimum = new Size(40, 40); + return result; + } + + public static bool GetHighlightWhenSelected(DependencyObject dependencyObject) + { + return (bool)dependencyObject.GetValue(HighlightWhenSelectedProperty); + } + + public static void SetHighlightWhenSelected(DependencyObject dependencyObject, bool value) + { + dependencyObject.SetValue(HighlightWhenSelectedProperty, value); + } + + protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) + { + System.Windows.Size result = base.ArrangeOverride(finalSize); + + _progressBar.Measure(finalSize); + _progressBar.Arrange(new Rect(0, 0, finalSize.Width, _progressBar.DesiredSize.Height)); + + return result; + } + + protected override void OnElementChanged(ElementChangedEventArgs<ListView> e) + { + base.OnElementChanged(e); + + Element.ScrollToRequested += OnScrollToRequested; + + if (Element.SelectedItem != null) + _itemNeedsSelecting = true; + + _listBox = new FixedLongListSelector + { + DataContext = Element, + ItemsSource = Element.TemplatedItems, + ItemTemplate = (System.Windows.DataTemplate)System.Windows.Application.Current.Resources["CellTemplate"], + GroupHeaderTemplate = (System.Windows.DataTemplate)System.Windows.Application.Current.Resources["ListViewHeader"], + ListHeaderTemplate = (System.Windows.DataTemplate)System.Windows.Application.Current.Resources["View"], + ListFooterTemplate = (System.Windows.DataTemplate)System.Windows.Application.Current.Resources["View"] + }; + _listBox.SetBinding(LongListSelector.IsGroupingEnabledProperty, new SLBinding("IsGroupingEnabled")); + + _listBox.SelectionChanged += OnNativeSelectionChanged; + _listBox.Tap += OnNativeItemTapped; + _listBox.ItemRealized += OnItemRealized; + + _listBox.PullToRefreshStarted += OnPullToRefreshStarted; + _listBox.PullToRefreshCompleted += OnPullToRefreshCompleted; + _listBox.PullToRefreshCanceled += OnPullToRefreshCanceled; + _listBox.PullToRefreshStatusUpdated += OnPullToRefreshStatusUpdated; + + SetNativeControl(_listBox); + + _progressBar = new System.Windows.Controls.ProgressBar { Maximum = 1, Visibility = Visibility.Collapsed }; + Children.Add(_progressBar); + + UpdateHeader(); + UpdateFooter(); + UpdateJumpList(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == ListView.SelectedItemProperty.PropertyName) + OnItemSelected(Element.SelectedItem); + else if (e.PropertyName == "HeaderElement") + UpdateHeader(); + else if (e.PropertyName == "FooterElement") + UpdateFooter(); + else if ((e.PropertyName == ListView.IsRefreshingProperty.PropertyName) || (e.PropertyName == ListView.IsPullToRefreshEnabledProperty.PropertyName) || (e.PropertyName == "CanRefresh")) + UpdateIsRefreshing(); + else if (e.PropertyName == "GroupShortNameBinding") + UpdateJumpList(); + } + + protected override void UpdateNativeWidget() + { + base.UpdateNativeWidget(); + + if (_progressBar != null) + _progressBar.Width = Element.Width; + } + + Cell FindCell(GestureEventArgs e, out FrameworkElement element) + { + Cell cell = null; + element = e.OriginalSource as FrameworkElement; + if (element != null) + cell = element.DataContext as Cell; + + if (cell == null) + { + System.Windows.Point pos = e.GetPosition(_listBox); + IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(pos, _listBox); + foreach (FrameworkElement frameworkElement in elements.OfType<FrameworkElement>()) + { + if ((cell = frameworkElement.DataContext as Cell) != null) + { + element = frameworkElement; + break; + } + } + } + + return cell; + } + + 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; + } + } + + FrameworkElement FindElement(Cell cell) + { + foreach (CellControl selector in FindDescendants<CellControl>(_listBox)) + { + if (ReferenceEquals(cell, selector.DataContext)) + return selector; + } + + return null; + } + + IEnumerable<FrameworkElement> FindHighlight(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 FindHighlightCore(parent); + } + + IEnumerable<FrameworkElement> FindHighlightCore(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 FindHighlightCore(childElement)) + yield return recursedElement; + } + } + + double GetHeight(Dictionary<System.Windows.DataTemplate, FrameworkElement> reusables, System.Windows.DataTemplate template, object bindingContext) + { + double width = Control.ActualWidth; + + FrameworkElement content; + if (!reusables.TryGetValue(template, out content)) + { + content = (FrameworkElement)template.LoadContent(); + + // Windows Phone refuses to properly bind things on a first pass or even a second pass unless it has different content + // so we'll force it to cycle here the first time for each template. + content.DataContext = bindingContext; + content.Measure(new System.Windows.Size(width, double.PositiveInfinity)); + content.DataContext = null; + content.Measure(new System.Windows.Size(width, double.PositiveInfinity)); + + var control = content as Control; + if (control != null) + { + // Since we're not adding to the visual tree, we need to inherit the font to measure correctly. + control.FontFamily = Control.FontFamily; + } + + reusables[template] = content; + } + + content.DataContext = bindingContext; + content.Measure(new System.Windows.Size(width, double.PositiveInfinity)); + return content.DesiredSize.Height; + } + + void OnItemRealized(object sender, ItemRealizationEventArgs e) + { + if (!_itemNeedsSelecting) + return; + + var cell = e.Container.DataContext as Cell; + if (cell == null || !Equals(cell.BindingContext, Element.SelectedItem)) + return; + + _itemNeedsSelecting = false; + OnItemSelected(Element.SelectedItem); + } + + void OnItemSelected(object selectedItem) + { + if (_fromNative != null && Equals(selectedItem, _fromNative)) + { + _fromNative = null; + return; + } + + RestorePreviousSelectedVisual(); + + if (selectedItem == null) + { + _listBox.SelectedItem = selectedItem; + return; + } + + IEnumerable<CellControl> items = FindDescendants<CellControl>(_listBox); + + CellControl item = items.FirstOrDefault(i => + { + var cell = (Cell)i.DataContext; + return Equals(cell.BindingContext, selectedItem); + }); + + if (item == null) + { + _itemNeedsSelecting = true; + return; + } + + SetSelectedVisual(item); + } + + void OnNativeItemTapped(object sender, GestureEventArgs e) + { + var cell = (Cell)Control.SelectedItem; + if (cell == null) + return; + + Cell parentCell = null; + + if (Element.IsGroupingEnabled) + { + TemplatedItemsList<ItemsView<Cell>, Cell> til = TemplatedItemsList<ItemsView<Cell>, Cell>.GetGroup(cell); + parentCell = til.HeaderContent; + } + + _fromNative = cell.BindingContext; + + if (Element.IsGroupingEnabled) + { + Element.NotifyRowTapped(TemplatedItemsList<ItemsView<Cell>, Cell>.GetIndex(parentCell), TemplatedItemsList<ItemsView<Cell>, Cell>.GetIndex(cell)); + } + else + Element.NotifyRowTapped(TemplatedItemsList<ItemsView<Cell>, Cell>.GetIndex(cell)); + } + + void OnNativeSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.AddedItems.Count == 0) + return; + + var cell = (Cell)e.AddedItems[0]; + Cell parentCell = null; + + if (cell == null) + { + RestorePreviousSelectedVisual(); + return; + } + + RestorePreviousSelectedVisual(); + FrameworkElement element = FindElement(cell); + if (element != null) + SetSelectedVisual(element); + } + + void OnPullToRefreshCanceled(object sender, EventArgs args) + { + if (Element.IsPullToRefreshEnabled && ((IListViewController)Element).RefreshAllowed) + _progressBar.Visibility = Visibility.Collapsed; + } + + void OnPullToRefreshCompleted(object sender, EventArgs args) + { + if (Element.IsPullToRefreshEnabled && ((IListViewController)Element).RefreshAllowed) + { + _progressBar.IsIndeterminate = true; + ((IListViewController)Element).SendRefreshing(); + } + } + + void OnPullToRefreshStarted(object sender, EventArgs args) + { + if (Element.IsPullToRefreshEnabled && ((IListViewController)Element).RefreshAllowed) + { + _progressBar.Visibility = Visibility.Visible; + _progressBar.IsIndeterminate = false; + _progressBar.Value = Math.Max(0, Math.Min(1, _listBox.PullToRefreshStatus)); + } + } + + void OnPullToRefreshStatusUpdated(object sender, EventArgs eventArgs) + { + if (Element.IsPullToRefreshEnabled && ((IListViewController)Element).RefreshAllowed) + _progressBar.Value = Math.Max(0, Math.Min(1, _listBox.PullToRefreshStatus)); + } + + void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + if (_animatable == null && e.ShouldAnimate) + _animatable = new Animatable(); + + if (_viewport == null) + { + // Making sure we're actually loaded + if (VisualTreeHelper.GetChildrenCount(_listBox) == 0) + { + RoutedEventHandler handler = null; + handler = (o, args) => + { + Control.Loaded -= handler; + OnScrollToRequested(sender, e); + }; + Control.Loaded += handler; + + return; + } + _viewport = (ViewportControl)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(_listBox, 0), 0), 0); + if (_viewport.Viewport.Bottom == 0) + { + EventHandler<ViewportChangedEventArgs> viewportChanged = null; + viewportChanged = (o, args) => + { + if (_viewport.Viewport.Bottom == 0) + return; + + _viewport.ViewportChanged -= viewportChanged; + OnScrollToRequested(sender, e); + }; + _viewport.ViewportChanged += viewportChanged; + return; + } + } + + double y = 0; + double targetHeight = 0; + double targetHeaderHeight = 0; + + var templateReusables = new Dictionary<System.Windows.DataTemplate, FrameworkElement>(); + + var found = false; + + if (Element.IsGroupingEnabled) + { + for (var g = 0; g < Element.TemplatedItems.Count; g++) + { + if (found) + break; + + TemplatedItemsList<ItemsView<Cell>, Cell> til = Element.TemplatedItems.GetGroup(g); + + double headerHeight = GetHeight(templateReusables, Control.GroupHeaderTemplate, til); + y += headerHeight; + + for (var i = 0; i < til.Count; i++) + { + Cell cell = til[i]; + + double contentHeight = GetHeight(templateReusables, Control.ItemTemplate, cell); + + if ((ReferenceEquals(til.BindingContext, e.Group) || e.Group == null) && ReferenceEquals(cell.BindingContext, e.Item)) + { + targetHeaderHeight = headerHeight; + targetHeight = contentHeight; + found = true; + break; + } + + y += contentHeight; + } + } + } + else + { + for (var i = 0; i < Element.TemplatedItems.Count; i++) + { + Cell cell = Element.TemplatedItems[i]; + + double height = GetHeight(templateReusables, Control.ItemTemplate, cell); + + if (ReferenceEquals(cell.BindingContext, e.Item)) + { + found = true; + targetHeight = height; + break; + } + + y += height; + } + } + + if (!found) + return; + + ScrollToPosition position = e.Position; + if (position == ScrollToPosition.MakeVisible) + { + if (y >= _viewport.Viewport.Top && y <= _viewport.Viewport.Bottom) + return; + if (y > _viewport.Viewport.Bottom) + position = ScrollToPosition.End; + else + position = ScrollToPosition.Start; + } + + if (position == ScrollToPosition.Start && Element.IsGroupingEnabled) + y = y - targetHeaderHeight; + else if (position == ScrollToPosition.Center) + y = y - (_viewport.ActualHeight / 2 + targetHeight / 2); + else if (position == ScrollToPosition.End) + y = y - _viewport.ActualHeight + targetHeight; + + double startY = _viewport.Viewport.Y; + double distance = y - startY; + + if (e.ShouldAnimate) + { + var animation = new Animation(v => { _viewport.SetViewportOrigin(new System.Windows.Point(0, startY + distance * v)); }); + + animation.Commit(_animatable, "ScrollTo", length: 500, easing: Easing.CubicInOut); + } + else + _viewport.SetViewportOrigin(new System.Windows.Point(0, y)); + } + + void RestorePreviousSelectedVisual() + { + foreach (Tuple<FrameworkElement, SLBinding, Brush> highlight in _previousHighlights) + { + if (highlight.Item2 != null) + highlight.Item1.SetForeground(highlight.Item2); + else + highlight.Item1.SetForeground(highlight.Item3); + } + + _previousHighlights.Clear(); + } + + void SetSelectedVisual(FrameworkElement element) + { + IEnumerable<FrameworkElement> highlightMes = FindHighlight(element); + foreach (FrameworkElement toHighlight in highlightMes) + { + Brush brush = null; + SLBinding binding = toHighlight.GetForegroundBinding(); + if (binding == null) + brush = toHighlight.GetForeground(); + + _previousHighlights.Add(new Tuple<FrameworkElement, SLBinding, Brush>(toHighlight, binding, brush)); + toHighlight.SetForeground((Brush)System.Windows.Application.Current.Resources["PhoneAccentBrush"]); + } + } + + void UpdateFooter() + { + Control.ListFooter = ((IListViewController)Element).FooterElement; + } + + void UpdateHeader() + { + Control.ListHeader = ((IListViewController)Element).HeaderElement; + } + + void UpdateIsRefreshing() + { + if (Element.IsRefreshing) + { + _progressBar.Visibility = Visibility.Visible; + _progressBar.IsIndeterminate = true; + } + else + { + _progressBar.IsIndeterminate = false; + _progressBar.Visibility = _listBox.IsInPullToRefresh && Element.IsPullToRefreshEnabled && ((IListViewController)Element).RefreshAllowed ? Visibility.Visible : Visibility.Collapsed; + } + } + + void UpdateJumpList() + { + if (_listBox.IsGroupingEnabled && Element.GroupShortNameBinding == null) + _listBox.JumpListStyle = null; + else + _listBox.JumpListStyle = (System.Windows.Style)System.Windows.Application.Current.Resources["HeaderJumpStyle"]; + } + } +}
\ No newline at end of file |