From e00e4e7e75d53af5fcf06a90421f5ca69d35ccf3 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sat, 2 Mar 2013 20:28:59 +0400 Subject: LowProfileImageLoader --- Juick/Classes/DeferredLoadListBox.cs | 275 +++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 Juick/Classes/DeferredLoadListBox.cs (limited to 'Juick/Classes/DeferredLoadListBox.cs') diff --git a/Juick/Classes/DeferredLoadListBox.cs b/Juick/Classes/DeferredLoadListBox.cs new file mode 100644 index 0000000..165a395 --- /dev/null +++ b/Juick/Classes/DeferredLoadListBox.cs @@ -0,0 +1,275 @@ +// Copyright (C) Microsoft Corporation. All Rights Reserved. +// This code released under the terms of the Microsoft Public License +// (Ms-PL, http://opensource.org/licenses/ms-pl.html). + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Media; + +namespace Juick.Classes +{ + /// + /// Implements a subclass of ListBox based on a StackPanel that defers the + /// loading of off-screen items until necessary in order to minimize impact + /// to the UI thread. + /// + public class DeferredLoadListBox : ListBox + { + private enum OverlapKind { Overlap, ChildAbove, ChildBelow }; + + private ScrollViewer _scrollViewer; + private ItemContainerGenerator _generator; + private bool _queuedUnmaskVisibleContent; + private bool _inOnApplyTemplate; + + /// + /// Handles the application of the Control's Template. + /// + public override void OnApplyTemplate() + { + // Unhook from old Template elements + _inOnApplyTemplate = true; + ClearValue(VerticalOffsetShadowProperty); + _scrollViewer = null; + _generator = null; + + // Apply new Template + base.OnApplyTemplate(); + + // Hook up to new Template elements + _scrollViewer = FindFirstChildOfType(this); + if (null == _scrollViewer) + { + throw new NotSupportedException("Control Template must include a ScrollViewer (wrapping ItemsHost)."); + } + _generator = ItemContainerGenerator; + SetBinding(VerticalOffsetShadowProperty, new Binding { Source = _scrollViewer, Path = new PropertyPath("VerticalOffset") }); + _inOnApplyTemplate = false; + } + + /// + /// Determines if the specified item is (or is eligible to be) its own item container. + /// + /// The specified item. + /// true if the item is its own item container; otherwise, false. + protected override bool IsItemItsOwnContainerOverride(object item) + { + // Check container type + return item is DeferredLoadListBoxItem; + } + + /// + /// Creates or identifies the element used to display a specified item. + /// + /// A DeferredLoadListBoxItem corresponding to a specified item. + protected override DependencyObject GetContainerForItemOverride() + { + // Create container (matches ListBox implementation) + var item = new DeferredLoadListBoxItem(); + if (ItemContainerStyle != null) + { + item.Style = ItemContainerStyle; + } + return item; + } + + /// + /// Prepares the specified element to display the specified item. + /// + /// The element used to display the specified item. + /// The item to display. + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + // Perform base class preparation + base.PrepareContainerForItemOverride(element, item); + + // Mask the container's content + var container = (DeferredLoadListBoxItem)element; + if (!DesignerProperties.IsInDesignTool) + { + container.MaskContent(); + } + + // Queue a (single) pass to unmask newly visible content on the next tick + if (!_queuedUnmaskVisibleContent) + { + _queuedUnmaskVisibleContent = true; + Dispatcher.BeginInvoke(() => + { + _queuedUnmaskVisibleContent = false; + UnmaskVisibleContent(); + }); + } + } + + private static readonly DependencyProperty VerticalOffsetShadowProperty = + DependencyProperty.Register("VerticalOffsetShadow", typeof(double), typeof(DeferredLoadListBox), new PropertyMetadata(-1.0, OnVerticalOffsetShadowChanged)); + private static void OnVerticalOffsetShadowChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + // Handle ScrollViewer VerticalOffset change by unmasking newly visible content + ((DeferredLoadListBox)o).UnmaskVisibleContent(); + } + + private void UnmaskVisibleContent() + { + // Capture variables + var count = Items.Count; + + // Find index of any container within view using (1-indexed) binary search + var index = -1; + var l = 0; + var r = count + 1; + while (-1 == index) + { + var p = (r - l) / 2; + if (0 == p) + { + break; + } + p += l; + var c = (DeferredLoadListBoxItem)_generator.ContainerFromIndex(p - 1); + if (null == c) + { + if (_inOnApplyTemplate) + { + // Applying template; don't expect to have containers at this point + return; + } + // Should always be able to get the container + var presenter = FindFirstChildOfType(_scrollViewer); + var panel = (null == presenter) ? null : FindFirstChildOfType(presenter); + if (panel is VirtualizingStackPanel) + { + throw new NotSupportedException("Must change ItemsPanel to be a StackPanel (via the ItemsPanel property)."); + } + else + { + throw new NotSupportedException("Couldn't find container for item (ItemsPanel should be a StackPanel)."); + } + } + switch (Overlap(_scrollViewer, c, 0)) + { + case OverlapKind.Overlap: + index = p - 1; + break; + case OverlapKind.ChildAbove: + l = p; + break; + case OverlapKind.ChildBelow: + r = p; + break; + } + } + + if (-1 != index) + { + // Unmask visible items below the current item + for (var i = index; i < count; i++) + { + if (!UnmaskItemContent(i)) + { + break; + } + } + + // Unmask visible items above the current item + for (var i = index - 1; 0 <= i; i--) + { + if (!UnmaskItemContent(i)) + { + break; + } + } + } + } + + private bool UnmaskItemContent(int index) + { + var container = (DeferredLoadListBoxItem)_generator.ContainerFromIndex(index); + if (null != container) + { + // Return quickly if not masked (but periodically check visibility anyway so we can stop once we're out of range) + if (!container.Masked && (0 != (index % 16))) + { + return true; + } + // Check necessary conditions + if (0 == container.ActualHeight) + { + // In some cases, ActualHeight will be 0 here, but can be "fixed" with an explicit call to UpdateLayout + container.UpdateLayout(); + if (0 == container.ActualHeight) + { + throw new NotSupportedException("All containers must have a Height set (ex: via ItemContainerStyle), though the heights do not all need to be the same."); + } + } + // If container overlaps the "visible" area (i.e. on or near the screen), unmask it + if (OverlapKind.Overlap == Overlap(_scrollViewer, container, 2 * _scrollViewer.ActualHeight)) + { + container.UnmaskContent(); + return true; + } + } + return false; + } + + private static bool Overlap(double startA, double endA, double startB, double endB) + { + return (((startA <= startB) && (startB <= endA)) || + ((startB <= startA) && (startA <= endB))); + } + + private static OverlapKind Overlap(ScrollViewer parent, FrameworkElement child, double padding) + { + // Get child transform relative to parent + //var transform = child.TransformToVisual(parent); // Unreliable on Windows Phone 7; throws ArgumentException sometimes + var layoutSlot = LayoutInformation.GetLayoutSlot(child); + var transform = new TranslateTransform { /*X = layoutSlot.Left - parent.HorizontalOffset,*/ Y = layoutSlot.Top - parent.VerticalOffset }; + // Get child bounds relative to parent + var bounds = new Rect(transform.Transform(new Point()), transform.Transform(new Point(/*child.ActualWidth*/ 0, child.ActualHeight))); + // Return kind of overlap + if (Overlap(0 - padding, parent.ActualHeight + padding, bounds.Top, bounds.Bottom)) + { + return OverlapKind.Overlap; + } + else if (bounds.Top < 0) + { + return OverlapKind.ChildAbove; + } + else + { + return OverlapKind.ChildBelow; + } + } + + private static T FindFirstChildOfType(DependencyObject root) where T : class + { + // Enqueue root node + var queue = new Queue(); + queue.Enqueue(root); + while (0 < queue.Count) + { + // Dequeue next node and check its children + var current = queue.Dequeue(); + for (var i = VisualTreeHelper.GetChildrenCount(current) - 1; 0 <= i; i--) + { + var child = VisualTreeHelper.GetChild(current, i); + var typedChild = child as T; + if (null != typedChild) + { + return typedChild; + } + // Enqueue child + queue.Enqueue(child); + } + } + // No children match + return null; + } + } +} -- cgit v1.2.3