Last active
April 15, 2016 07:01
-
-
Save atomaras/c4d4d3dcae8a262c967fc732efbf3133 to your computer and use it in GitHub Desktop.
PullToRefreshListView Control for UWP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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="" | |
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