|
using System; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.Runtime.InteropServices.WindowsRuntime; |
|
using System.Windows.Input; |
|
using Windows.Foundation; |
|
using Windows.UI.Xaml; |
|
using Windows.UI.Xaml.Controls; |
|
using Windows.UI.Xaml.Controls.Primitives; |
|
using Windows.UI.Xaml.Media; |
|
|
|
namespace PullToRefreshListView |
|
{ |
|
/// <summary> |
|
/// Refresh box. Pull down beyond the top limit on the listview to |
|
/// trigger a refresh, similar to iPhone's lists. |
|
/// </summary> |
|
public class PullToRefreshListView : ListView |
|
{ |
|
ScrollViewer _elementScrollViewer; |
|
PullToRefreshOuterPanel _pullToRefreshOuterPanel; |
|
PullToRefreshInnerPanel _pullToRefreshInnerPanel; |
|
|
|
public PullToRefreshListView() |
|
{ |
|
DefaultStyleKey = typeof(PullToRefreshListView); |
|
} |
|
|
|
ScrollViewer ElementScrollViewer |
|
{ |
|
get { return _elementScrollViewer; } |
|
set |
|
{ |
|
if (_elementScrollViewer != null) |
|
{ |
|
_elementScrollViewer.ViewChanging -= OnElementScrollViewerViewChanging; |
|
_elementScrollViewer.DirectManipulationStarted -= OnElementScrollViewerManipulationStarted; |
|
} |
|
_elementScrollViewer = value; |
|
if (_elementScrollViewer != null) |
|
{ |
|
_elementScrollViewer.ViewChanging += OnElementScrollViewerViewChanging; |
|
_elementScrollViewer.DirectManipulationStarted += OnElementScrollViewerManipulationStarted; |
|
} |
|
} |
|
} |
|
|
|
PullToRefreshInnerPanel PullToRefreshInnerPanel |
|
{ |
|
get { return _pullToRefreshInnerPanel; } |
|
set |
|
{ |
|
if (_pullToRefreshInnerPanel != null) |
|
_pullToRefreshInnerPanel.SizeChanged -= OnPullToRefreshInnerPanelSizeChanged; |
|
_pullToRefreshInnerPanel = value; |
|
if (_pullToRefreshInnerPanel != null) |
|
_pullToRefreshInnerPanel.SizeChanged += OnPullToRefreshInnerPanelSizeChanged; |
|
} |
|
} |
|
|
|
PullToRefreshOuterPanel PullToRefreshOuterPanel |
|
{ |
|
get { return _pullToRefreshOuterPanel; } |
|
set |
|
{ |
|
if (_pullToRefreshOuterPanel != null) |
|
_pullToRefreshOuterPanel.SizeChanged -= OnPullToRefreshOuterPanelSizeChanged; |
|
_pullToRefreshOuterPanel = value; |
|
if (_pullToRefreshOuterPanel != null) |
|
_pullToRefreshOuterPanel.SizeChanged += OnPullToRefreshOuterPanelSizeChanged; |
|
} |
|
} |
|
|
|
FrameworkElement PullToRefreshIndicator { get; set; } |
|
|
|
/// <summary> |
|
/// Is the scrollviewer currently being pulled? |
|
/// </summary> |
|
public bool IsPulling { get; private set; } |
|
|
|
void OnPullToRefreshInnerPanelSizeChanged(object sender, SizeChangedEventArgs e) |
|
{ |
|
PullToRefreshIndicator.Width = PullToRefreshInnerPanel.ActualWidth; |
|
|
|
// Hide the refresh indicator |
|
ElementScrollViewer.ChangeView(null, PullToRefreshIndicator.Height, null, true); |
|
} |
|
|
|
void OnPullToRefreshOuterPanelSizeChanged(object sender, SizeChangedEventArgs e) |
|
{ |
|
PullToRefreshInnerPanel.InvalidateMeasure(); |
|
} |
|
|
|
/// <summary> |
|
/// Called when the scroll position of the scrollviewer has changed. |
|
/// Used to figure out if the user has over-panned the scrollviewer and if so |
|
/// schedule a refresh when the user releases their finger. |
|
/// </summary> |
|
void OnElementScrollViewerViewChanging(object sender, ScrollViewerViewChangingEventArgs e) |
|
{ |
|
if (e.NextView.VerticalOffset == 0) |
|
{ |
|
IsPulling = true; |
|
ChangeVisualState(true); |
|
} |
|
else |
|
{ |
|
IsPulling = false; |
|
ChangeVisualState(true); |
|
} |
|
|
|
// check whether the user released their finger when VerticalOffset=0 |
|
// and the direction is up. |
|
if (e.IsInertial && |
|
e.FinalView.VerticalOffset == PullToRefreshIndicator.Height && |
|
ElementScrollViewer.VerticalOffset == 0) |
|
{ |
|
// wait for the manipulation to end before firing the Refresh event. |
|
ElementScrollViewer.DirectManipulationCompleted += OnElementScrollViewerManipulationCompleted; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Called when a user manipulation has started. Makes the pull indicator visible. |
|
/// </summary> |
|
void OnElementScrollViewerManipulationStarted(object sender, object e) |
|
{ |
|
ElementScrollViewer.DirectManipulationStarted -= OnElementScrollViewerManipulationStarted; |
|
|
|
// Hide the indicator until the user starts touching the scrollviewer for the first time. |
|
PullToRefreshIndicator.Opacity = 1; |
|
} |
|
|
|
/// <summary> |
|
/// Called when the user releases their finger from the screen after having overpanned the scrollviewer |
|
/// </summary> |
|
void OnElementScrollViewerManipulationCompleted(object sender, object args) |
|
{ |
|
ElementScrollViewer.DirectManipulationCompleted -= OnElementScrollViewerManipulationCompleted; |
|
OnRefreshed(); |
|
} |
|
|
|
private void OnRefreshed() |
|
{ |
|
ElementScrollViewer.ChangeView(null, 0, null, true); |
|
IsPulling = false; |
|
ChangeVisualState(true); |
|
|
|
PullRefresh?.Invoke(this, EventArgs.Empty); |
|
if (PullRefreshCommand?.CanExecute(null) == true) |
|
PullRefreshCommand.Execute(null); |
|
} |
|
|
|
protected override void OnApplyTemplate() |
|
{ |
|
base.OnApplyTemplate(); |
|
|
|
ElementScrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer; |
|
PullToRefreshIndicator = GetTemplateChild("PullToRefreshIndicator") as FrameworkElement; |
|
PullToRefreshInnerPanel = GetTemplateChild("InnerPanel") as PullToRefreshInnerPanel; |
|
PullToRefreshOuterPanel = GetTemplateChild("OuterPanel") as PullToRefreshOuterPanel; |
|
|
|
ChangeVisualState(false); |
|
|
|
} |
|
|
|
private void ChangeVisualState(bool useTransitions) |
|
{ |
|
if (IsPulling) |
|
{ |
|
GoToState(useTransitions, "Pulling"); |
|
} |
|
else |
|
{ |
|
GoToState(useTransitions, "NotPulling"); |
|
} |
|
} |
|
|
|
private bool GoToState(bool useTransitions, string stateName) |
|
{ |
|
return VisualStateManager.GoToState(this, stateName, useTransitions); |
|
} |
|
|
|
/// <summary> |
|
/// Gets or sets the refresh text. Ie "Pull down to refresh". |
|
/// </summary> |
|
public string RefreshText |
|
{ |
|
get { return (string)GetValue(RefreshTextProperty); } |
|
set { SetValue(RefreshTextProperty, value); } |
|
} |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="RefreshText"/> property |
|
/// </summary> |
|
public static readonly DependencyProperty RefreshTextProperty = |
|
DependencyProperty.Register("RefreshText", typeof(string), typeof(PullToRefreshListView), new PropertyMetadata("Pull down to refresh…")); |
|
|
|
/// <summary> |
|
/// Gets or sets the release text. Ie "Release to refresh". |
|
/// </summary> |
|
public string ReleaseText |
|
{ |
|
get { return (string)GetValue(ReleaseTextProperty); } |
|
set { SetValue(ReleaseTextProperty, value); } |
|
} |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="ReleaseText"/> property |
|
/// </summary> |
|
public static readonly DependencyProperty ReleaseTextProperty = |
|
DependencyProperty.Register("ReleaseText", typeof(string), typeof(PullToRefreshListView), new PropertyMetadata("Release to refresh…")); |
|
|
|
/// <summary> |
|
/// Sub text below Release/Refresh text. For example: Updated last: 12:34pm |
|
/// </summary> |
|
public string PullSubtext |
|
{ |
|
get { return (string)GetValue(PullSubtextProperty); } |
|
set { SetValue(PullSubtextProperty, value); } |
|
} |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="PullSubtext"/> property |
|
/// </summary> |
|
public static readonly DependencyProperty PullSubtextProperty = |
|
DependencyProperty.Register("PullSubtext", typeof(string), typeof(PullToRefreshListView), null); |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="PullIndicatorForeground"/> property |
|
/// </summary> |
|
public Brush PullIndicatorForeground |
|
{ |
|
get { return (Brush)GetValue(PullIndicatorForegroundProperty); } |
|
set { SetValue(PullIndicatorForegroundProperty, value); } |
|
} |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="PullIndicatorForeground"/> property |
|
/// </summary> |
|
public static readonly DependencyProperty PullIndicatorForegroundProperty = |
|
DependencyProperty.Register("PullIndicatorForeground", typeof(Brush), typeof(PullToRefreshListView), null); |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="PullRefreshCommand"/> property |
|
/// </summary> |
|
public ICommand PullRefreshCommand |
|
{ |
|
get { return (ICommand)GetValue(PullRefreshCommandProperty); } |
|
set { SetValue(PullRefreshCommandProperty, value); } |
|
} |
|
|
|
/// <summary> |
|
/// Identifies the <see cref="PullRefreshCommand"/> property |
|
/// </summary> |
|
public static readonly DependencyProperty PullRefreshCommandProperty = |
|
DependencyProperty.Register("PullRefreshCommand", typeof(ICommand), typeof(PullToRefreshListView), null); |
|
|
|
/// <summary> |
|
/// Triggered when the user requested a refresh. |
|
/// </summary> |
|
public event EventHandler PullRefresh; |
|
} |
|
|
|
/// <summary> |
|
/// The PullToRefreshOuterPanel works together with the <see cref="PullToRefreshInnerPanel"/> |
|
/// to workaround the problem where List Virtualization is disabled when ListView is hosted |
|
/// inside a ScrollViewer. |
|
/// Specifically the InnerPanel is able to report to the ListView as available height the OuterPanel's height |
|
/// instead of infinity which would be the case when a ScrollViewer is hosted inside another ScrollViewer. |
|
/// </summary> |
|
internal class PullToRefreshOuterPanel : Panel |
|
{ |
|
public Size AvailableSize { get; private set; } |
|
|
|
public Size FinalSize { get; private set; } |
|
|
|
protected override Size MeasureOverride(Size availableSize) |
|
{ |
|
AvailableSize = availableSize; |
|
// Children[0] is the outer ScrollViewer |
|
this.Children[0].Measure(availableSize); |
|
return this.Children[0].DesiredSize; |
|
} |
|
|
|
protected override Size ArrangeOverride(Size finalSize) |
|
{ |
|
FinalSize = finalSize; |
|
// Children[0] is the outer ScrollViewer |
|
this.Children[0].Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height)); |
|
return finalSize; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// The PullToRefreshOuterPanel works together with the <see cref="PullToRefreshInnerPanel"/> |
|
/// to workaround the problem where List Virtualization is disabled when ListView is hosted |
|
/// inside a ScrollViewer. |
|
/// Specifically the InnerPanel is able to report to the ListView as available height the OuterPanel's height |
|
/// instead of infinity which would be the case when a ScrollViewer is hosted inside another ScrollViewer. |
|
/// </summary> |
|
internal class PullToRefreshInnerPanel : Panel, IScrollSnapPointsInfo |
|
{ |
|
EventRegistrationTokenTable<EventHandler<object>> _verticaltable = new EventRegistrationTokenTable<EventHandler<object>>(); |
|
EventRegistrationTokenTable<EventHandler<object>> _horizontaltable = new EventRegistrationTokenTable<EventHandler<object>>(); |
|
|
|
protected override Size MeasureOverride(Size availableSize) |
|
{ |
|
// need to get away from infinity |
|
var parent = this.Parent as FrameworkElement; |
|
while (!(parent is PullToRefreshOuterPanel)) |
|
{ |
|
parent = parent.Parent as FrameworkElement; |
|
} |
|
|
|
var pullToRefreshOuterPanel = parent as PullToRefreshOuterPanel; |
|
|
|
// Children[0] is the Border that comprises the refresh UI |
|
this.Children[0].Measure(pullToRefreshOuterPanel.AvailableSize); |
|
// Children[1] is the ListView |
|
this.Children[1].Measure(new Size(pullToRefreshOuterPanel.AvailableSize.Width, pullToRefreshOuterPanel.AvailableSize.Height)); |
|
return new Size(this.Children[1].DesiredSize.Width, this.Children[0].DesiredSize.Height + pullToRefreshOuterPanel.AvailableSize.Height); |
|
} |
|
|
|
protected override Size ArrangeOverride(Size finalSize) |
|
{ |
|
// need to get away from infinity |
|
var parent = this.Parent as FrameworkElement; |
|
while (!(parent is PullToRefreshOuterPanel)) |
|
{ |
|
parent = parent.Parent as FrameworkElement; |
|
} |
|
|
|
var pullToRefreshOuterPanel = parent as PullToRefreshOuterPanel; |
|
|
|
// Children[0] is the PullToRefreshIndicator |
|
this.Children[0].Arrange(new Rect(0, 0, this.Children[0].DesiredSize.Width, this.Children[0].DesiredSize.Height)); |
|
// Children[1] is the ItemsScrollViewer |
|
this.Children[1].Arrange(new Rect(0, this.Children[0].DesiredSize.Height, pullToRefreshOuterPanel.FinalSize.Width, pullToRefreshOuterPanel.FinalSize.Height)); |
|
return finalSize; |
|
} |
|
|
|
bool IScrollSnapPointsInfo.AreHorizontalSnapPointsRegular => false; |
|
|
|
bool IScrollSnapPointsInfo.AreVerticalSnapPointsRegular => false; |
|
|
|
IReadOnlyList<float> IScrollSnapPointsInfo.GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment alignment) |
|
{ |
|
if (orientation == Orientation.Vertical) |
|
{ |
|
var l = new List<float>(); |
|
l.Add((float)this.Children[0].DesiredSize.Height); |
|
return l; |
|
} |
|
else |
|
{ |
|
return new List<float>(); |
|
} |
|
} |
|
|
|
float IScrollSnapPointsInfo.GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment alignment, out float offset) |
|
{ |
|
throw new NotImplementedException(); |
|
} |
|
|
|
event EventHandler<object> IScrollSnapPointsInfo.HorizontalSnapPointsChanged |
|
{ |
|
add |
|
{ |
|
var table = EventRegistrationTokenTable<EventHandler<object>> |
|
.GetOrCreateEventRegistrationTokenTable(ref this._horizontaltable); |
|
return table.AddEventHandler(value); |
|
} |
|
remove |
|
{ |
|
EventRegistrationTokenTable<EventHandler<object>> |
|
.GetOrCreateEventRegistrationTokenTable(ref this._horizontaltable) |
|
.RemoveEventHandler(value); |
|
} |
|
} |
|
|
|
event EventHandler<object> IScrollSnapPointsInfo.VerticalSnapPointsChanged |
|
{ |
|
add |
|
{ |
|
var table = EventRegistrationTokenTable<EventHandler<object>> |
|
.GetOrCreateEventRegistrationTokenTable(ref this._verticaltable); |
|
return table.AddEventHandler(value); |
|
|
|
} |
|
remove |
|
{ |
|
EventRegistrationTokenTable<EventHandler<object>> |
|
.GetOrCreateEventRegistrationTokenTable(ref this._verticaltable) |
|
.RemoveEventHandler(value); |
|
} |
|
} |
|
} |
|
} |