Реализация pull-to-refresh для WP8
Начать хочется с того, что всё тоже самое справедливо и для WP7, если использовать LongListSelector for Windows Phone 7.5 1.0.1, хотя в нём и есть несколько неприятных моментов. Что касается LongListSelector из состава WPToolkit или ListBox, то тут всё совсем просто, достаточно получить ScrollViewer и грамотно воспользоваться VerticalOffset. В последнем случае все тропы уже изучены и шишки набиты, можно найти красивое готовое решение. А вот с LLS под WP8 ситуация иная.
Для начала, если вы будете пользоваться наработками других товарищей, то скорее всего в итоге получите BindingExpression path error или, что ещё хуже, таинственный и загадочный -1073741819 (0xc0000005) ‘Access violation’. Возникает это при резком скролле из-за виртуализации данных, в различных ситуациях по-разному. Основная причина в том, что мы либо делаем binding элементов, которые уже уничтожены, либо биндим в ту область памяти, которая уже освобождена. Повинны в этом в любом случае разработчики LongListSelector, на мой взгляд, т.к. подобный вариант можно было бы и продумать или хотя бы кидать адекватный exception.
Чтобы этого избежать, надо после каждой дозагрузки списка делать _longlist.UpdateLayout();
Что касается самого pull-to-refresh, то есть полно готовых кривых реализаций, одна из них вот:
using Microsoft.Phone.Controls; using System; using System.Windows.Controls.Primitives; /// <summary> /// This class detects the pull gesture on a LongListSelector. How does it work? /// /// This class listens to the change of manipulation state of the LLS, to the MouseMove event /// (in WP, this event is triggered when the user moves the finger through the screen) /// and to the ItemRealized/Unrealized events. /// /// Listening to MouseMove, we can calculate the amount of finger movement. That is, we can /// detect when the user has scrolled the list. /// /// Then, when the ManipulationState changes from Manipulating to Animating (from user /// triggered movement to inertia movement), we check the viewport changes. The viewport is /// only constant when the user scrolls beyond the end of the list, either at the top or at the bottom. /// If no items were added, check the direction of the scroll movement and fire the corresponding event. /// </summary> public class WP8PullDetector { LongListSelector listbox; bool viewportChanged = false; bool isMoving = false; double manipulationStart = 0; double manipulationEnd = 0; public bool Bound { get; private set; } public void Bind(LongListSelector listbox) { Bound = true; this.listbox = listbox; listbox.ManipulationStateChanged += listbox_ManipulationStateChanged; listbox.MouseMove += listbox_MouseMove; listbox.ItemRealized += OnViewportChanged; listbox.ItemUnrealized += OnViewportChanged; } public void Unbind() { Bound = false; if (listbox != null) { listbox.ManipulationStateChanged -= listbox_ManipulationStateChanged; listbox.MouseMove -= listbox_MouseMove; listbox.ItemRealized -= OnViewportChanged; listbox.ItemUnrealized -= OnViewportChanged; } } void OnViewportChanged(object sender, Microsoft.Phone.Controls.ItemRealizationEventArgs e) { viewportChanged = true; } void listbox_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) { var pos = e.GetPosition(null); if (!isMoving) manipulationStart = pos.Y; else manipulationEnd = pos.Y; isMoving = true; } void listbox_ManipulationStateChanged(object sender, EventArgs e) { if (listbox.ManipulationState == ManipulationState.Idle) { isMoving = false; viewportChanged = false; } else if (listbox.ManipulationState == ManipulationState.Manipulating) { viewportChanged = false; } else if (listbox.ManipulationState == ManipulationState.Animating) { var total = manipulationStart - manipulationEnd; if (!viewportChanged && Compression != null) { if (total < 0) Compression(this, new CompressionEventArgs(CompressionType.Top)); else if(total > 0) // Explicitly exclude total == 0 case Compression(this, new CompressionEventArgs(CompressionType.Bottom)); } } } public event OnCompression Compression; } public class CompressionEventArgs : EventArgs { public CompressionType Type { get; protected set; } public CompressionEventArgs(CompressionType type) { Type = type; } } public enum CompressionType { Top, Bottom, Left, Right }; public delegate void OnCompression(object sender, CompressionEventArgs e);
Использование:
public Page1() { InitializeComponent(); var objWP8PullDetector = new WP8PullDetector(); objWP8PullDetector.Bind(objLongListSelector); //objWP8PullDetector.Unbind(); To unbind from compression detection objWP8PullDetector.Compression += objWP8PullDetector_Compression; } void objWP8PullDetector_Compression(object sender, CompressionEventArgs e) { //TODO: Your logic here }
Срабатывать это чудо научной жизни будет не при завершении (или начале) списка, а каждый раз при прокрутке вверх или вниз в пределах одного viewport
Посему, сразу хотелось бы обратить внимание на немного более здравую задумку
И в сумме получаем что-то вроде этого:
public class WP8PullDetector { LongListSelector listbox; bool viewportChanged = false; bool isMoving = false; double manipulationStart = 0; double manipulationEnd = 0; public bool Bound { get; private set; } private ViewportControl _viewport = null; public void Bind(LongListSelector listbox) { Bound = true; this.listbox = listbox; listbox.ManipulationStateChanged += listbox_ManipulationStateChanged; listbox.MouseMove += listbox_MouseMove; listbox.ItemRealized += OnViewportChanged; listbox.ItemUnrealized += OnViewportChanged; _viewport = FindVisualChild<ViewportControl>(listbox); } public void Unbind() { Bound = false; if (listbox != null) { listbox.ManipulationStateChanged -= listbox_ManipulationStateChanged; listbox.MouseMove -= listbox_MouseMove; listbox.ItemRealized -= OnViewportChanged; listbox.ItemUnrealized -= OnViewportChanged; _viewport=null; } } public static T FindVisualChild<T>(DependencyObject depObj) where T : DependencyObject { if (depObj != null) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) { DependencyObject child = VisualTreeHelper.GetChild(depObj, i); if (child != null && child is T) { return (T)child; } T childItem = FindVisualChild<T>(child); if (childItem != null) return childItem; } } return null; } void OnViewportChanged(object sender, Microsoft.Phone.Controls.ItemRealizationEventArgs e) { viewportChanged = true; } void listbox_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) { var pos = e.GetPosition(null); if (!isMoving) manipulationStart = pos.Y; else manipulationEnd = pos.Y; isMoving = true; } void listbox_ManipulationStateChanged(object sender, EventArgs e) { if (listbox.ManipulationState == ManipulationState.Idle) { isMoving = false; viewportChanged = false; } else if (listbox.ManipulationState == ManipulationState.Manipulating) { viewportChanged = false; } else if (listbox.ManipulationState == ManipulationState.Animating) { var total = manipulationStart - manipulationEnd; if (!viewportChanged && Compression != null) { if (total < 0 && (_viewport.Viewport.Top == _viewport.Bounds.Top)) Compression(this, new CompressionEventArgs(CompressionType.Top)); else if(total > 0 && (_viewport.Bounds.Bottom == _viewport.Viewport.Bottom)) Compression(this, new CompressionEventArgs(CompressionType.Bottom)); } } } public event OnCompression Compression; } public class CompressionEventArgs : EventArgs { public CompressionType Type { get; protected set; } public CompressionEventArgs(CompressionType type) { Type = type; } } public enum CompressionType { Bottom, Top }; public delegate void OnCompression(object sender, CompressionEventArgs e);
Работоспособность в боевых условиях кода выше я лично не проверял, т.к. под себя переписывал полностью, но как идеи, этого вполне достаточно. В данном случае мы точно не будем лишний раз сигналить о приближении к концу (или к началу), именно с этой целью мы и получали _viewport, чтобы затем сравнить _viewport.Bounds.Bottom == _viewport.Viewport.Bottom, к слову, второе сравнение (total > 0) можно и вовсе убрать в данном случае, как и всё что с ним связано.
А можно и не убирать, к примеру, если мы будем делать так:
if(total > 0 && ((_viewport.Bounds.Bottom - (_viewport.Viewport.Height + _viewport.Viewport.Bottom))) Compression(this, new CompressionEventArgs(CompressionType.Bottom));
То событие будет происходить ещё до того, как мы потянем вниз, это очень удобно, если пользователь не догадывается, что теоретически можно тянуть, а так же и просто удобнее, чем тянуть. Но тогда стоит добавить дополнительный флаг, что мы уже отправляли event, и не забывать его сбрасывать, а так же, отправлять событие и при ManipulationState.Idle
Впрочем, это уже совсем другая история…