Skip to content

Instantly share code, notes, and snippets.

@atomaras
Last active April 15, 2016 07:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atomaras/c4d4d3dcae8a262c967fc732efbf3133 to your computer and use it in GitHub Desktop.
Save atomaras/c4d4d3dcae8a262c967fc732efbf3133 to your computer and use it in GitHub Desktop.
PullToRefreshListView Control for UWP
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);
}
}
}
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PullToRefreshListView">
<Style TargetType="controls:PullToRefreshListView">
<Setter Property="IsTabStop"
Value="False" />
<Setter Property="TabNavigation"
Value="Once" />
<Setter Property="IsSwipeEnabled"
Value="True" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility"
Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollMode"
Value="Disabled" />
<Setter Property="ScrollViewer.IsHorizontalRailEnabled"
Value="False" />
<Setter Property="ScrollViewer.VerticalScrollMode"
Value="Enabled" />
<Setter Property="ScrollViewer.IsVerticalRailEnabled"
Value="True" />
<Setter Property="ScrollViewer.ZoomMode"
Value="Disabled" />
<Setter Property="ScrollViewer.IsDeferredScrollingEnabled"
Value="False" />
<Setter Property="ScrollViewer.BringIntoViewOnFocusChange"
Value="True" />
<Setter Property="PullIndicatorForeground"
Value="Gray"/>
<Setter Property="ItemContainerTransitions">
<Setter.Value>
<TransitionCollection>
<AddDeleteThemeTransition />
<ContentThemeTransition />
<ReorderThemeTransition />
<EntranceThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:PullToRefreshListView">
<Border BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PullStates">
<VisualState x:Name="Pulling">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="pulling"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="notPulling"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetName="rotArrow"
Storyboard.TargetProperty="Angle"
To="0"
Duration="0:0:.25" />
</Storyboard>
</VisualState>
<VisualState x:Name="NotPulling">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="rotArrow"
Storyboard.TargetProperty="Angle"
To="-180"
Duration="0:0:.25" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<controls:PullToRefreshOuterPanel x:Name="OuterPanel"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ScrollViewer x:Name="ScrollViewer"
TabNavigation="{TemplateBinding TabNavigation}"
HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}"
VerticalScrollMode="Enabled"
VerticalScrollBarVisibility="Hidden"
IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}"
IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"
IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}"
VerticalSnapPointsType="MandatorySingle"
VerticalSnapPointsAlignment="Near"
AutomationProperties.AccessibilityView="Raw">
<controls:PullToRefreshInnerPanel x:Name="InnerPanel"
VerticalAlignment="Stretch">
<Border x:Name="PullToRefreshIndicator"
Height="60"
HorizontalAlignment="Stretch"
Opacity="0">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<FontIcon RenderTransformOrigin=".5,.5"
Margin="0,0,4,0"
FontSize="18"
Glyph="&#xE110;"
VerticalAlignment="Center"
Foreground="{TemplateBinding PullIndicatorForeground}"
UseLayoutRounding="False">
<FontIcon.RenderTransform>
<RotateTransform x:Name="rotArrow" />
</FontIcon.RenderTransform>
</FontIcon>
<StackPanel Orientation="Vertical">
<Grid>
<TextBlock Text="{TemplateBinding RefreshText}"
x:Name="notPulling"
FontSize="16"
HorizontalAlignment="Center"
Foreground="{TemplateBinding PullIndicatorForeground}" />
<TextBlock Text="{TemplateBinding ReleaseText}"
Visibility="Collapsed"
x:Name="pulling"
FontSize="16"
HorizontalAlignment="Center"
Foreground="{TemplateBinding PullIndicatorForeground}" />
</Grid>
<TextBlock Text="{TemplateBinding PullSubtext}"
HorizontalAlignment="Center"
Visibility="Collapsed"
FontSize="12"
Foreground="{TemplateBinding PullIndicatorForeground}" />
</StackPanel>
</StackPanel>
</Border>
<ScrollViewer x:Name="ItemsScrollViewer"
VerticalScrollMode="Enabled"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter Header="{TemplateBinding Header}"
HeaderTemplate="{TemplateBinding HeaderTemplate}"
HeaderTransitions="{TemplateBinding HeaderTransitions}"
Footer="{TemplateBinding Footer}"
FooterTemplate="{TemplateBinding FooterTemplate}"
FooterTransitions="{TemplateBinding FooterTransitions}"
Padding="{TemplateBinding Padding}" />
</ScrollViewer>
</controls:PullToRefreshInnerPanel>
</ScrollViewer>
</controls:PullToRefreshOuterPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment