├── PullToRefreshXaml ├── Assets │ ├── StoreLogo.png │ ├── SplashScreen.scale-200.png │ ├── LockScreenLogo.scale-200.png │ ├── Square44x44Logo.scale-200.png │ ├── Wide310x150Logo.scale-200.png │ ├── Square150x150Logo.scale-200.png │ └── Square44x44Logo.targetsize-24_altform-unplated.png ├── RefreshRequestedEventHandler.cs ├── Model │ ├── GroupInfoList.cs │ ├── Item.cs │ └── Contact.cs ├── App.xaml ├── project.json ├── RefreshRequestedEventArgs.cs ├── MainPageBottomUp.xaml.cs ├── Extensions.cs ├── Properties │ ├── AssemblyInfo.cs │ └── Default.rd.xml ├── PullToRefreshXaml.nuget.targets ├── Package.appxmanifest ├── MainPage.xaml ├── PullToRefreshXaml.nuget.props ├── MainPageTopDown.xaml.cs ├── App.xaml.cs ├── MainPageTopDown.xaml ├── AsyncDelegateCommand.cs ├── MainPageBottomUp.xaml ├── PullToRefreshXaml.csproj ├── MainPage.xaml.cs └── PullToRefreshBehavior.cs ├── README.md ├── LICENSE.md ├── PullToRefreshXaml.sln ├── .gitattributes └── .gitignore /PullToRefreshXaml/Assets/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/StoreLogo.png -------------------------------------------------------------------------------- /PullToRefreshXaml/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PullToRefreshXaml 2 | Pull to refresh with the new Windows Composition API. 3 | 4 | ![Pull to Refresh](http://ww4.sinaimg.cn/large/6376e69egw1ezly7u4qrpg20p90uonpf.gif). 5 | -------------------------------------------------------------------------------- /PullToRefreshXaml/Assets/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /PullToRefreshXaml/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /PullToRefreshXaml/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /PullToRefreshXaml/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /PullToRefreshXaml/RefreshRequestedEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace PullToRefreshXaml 2 | { 3 | public delegate void RefreshRequestedEventHandler(object sender, RefreshRequestedEventArgs args); 4 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinXinLiu/PullToRefreshUWP_WindowsComposition/HEAD/PullToRefreshXaml/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /PullToRefreshXaml/Model/GroupInfoList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PullToRefreshXaml.Model 4 | { 5 | public class GroupInfoList : List 6 | { 7 | public object Key { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PullToRefreshXaml/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PullToRefreshXaml/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.NETCore.UniversalWindowsPlatform": "5.3.0", 4 | "Microsoft.Xaml.Behaviors.Uwp.Managed": "2.0.0" 5 | }, 6 | "frameworks": { 7 | "uap10.0": {} 8 | }, 9 | "runtimes": { 10 | "win10-arm": {}, 11 | "win10-arm-aot": {}, 12 | "win10-x86": {}, 13 | "win10-x86-aot": {}, 14 | "win10-x64": {}, 15 | "win10-x64-aot": {} 16 | } 17 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/RefreshRequestedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Windows.Foundation; 2 | 3 | namespace PullToRefreshXaml 4 | { 5 | public class RefreshRequestedEventArgs 6 | { 7 | internal RefreshRequestedEventArgs(DeferralCompletedHandler handler) 8 | { 9 | Handler = handler; 10 | } 11 | 12 | private DeferralCompletedHandler Handler { get; } 13 | private Deferral Deferral { get; set; } 14 | 15 | public Deferral GetDeferral() => 16 | Deferral ?? (Deferral = new Deferral(Handler)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PullToRefreshXaml/MainPageBottomUp.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PullToRefreshXaml.Model; 3 | using System.Collections.ObjectModel; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Windows.UI.Xaml.Controls; 7 | 8 | namespace PullToRefreshXaml 9 | { 10 | public sealed partial class MainPageBottomUp : Page 11 | { 12 | private readonly ObservableCollection _list = Contact.GetContactsGrouped(250); 13 | 14 | public MainPageBottomUp() 15 | { 16 | InitializeComponent(); 17 | ContactsCVS.Source = _list; 18 | 19 | RefreshCommand = new AsyncDelegateCommand(async token => 20 | { 21 | await Task.Delay(2000, token); 22 | 23 | _list.Add(Contact.GetContactsGrouped(1)[0]); 24 | }); 25 | } 26 | 27 | public AsyncDelegateCommand RefreshCommand { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.UI.Xaml; 3 | using Windows.UI.Xaml.Controls; 4 | using Windows.UI.Xaml.Media; 5 | 6 | namespace PullToRefreshXaml 7 | { 8 | public static class Extensions 9 | { 10 | public static ScrollViewer GetScrollViewer(this DependencyObject element) 11 | { 12 | if (element is ScrollViewer scrollViewer) 13 | { 14 | return scrollViewer; 15 | } 16 | 17 | for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) 18 | { 19 | var child = VisualTreeHelper.GetChild(element, i); 20 | 21 | var result = GetScrollViewer(child); 22 | if (result == null) continue; 23 | 24 | return result; 25 | } 26 | 27 | return null; 28 | } 29 | 30 | public static bool AlmostEqual(this float x, float y, float tolerance = 0.01f) => 31 | Math.Abs(x - y) < tolerance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Justin Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PullToRefreshXaml/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PullToRefreshXaml")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PullToRefreshXaml")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Version information for an assembly consists of the following four values: 18 | // 19 | // Major Version 20 | // Minor Version 21 | // Build Number 22 | // Revision 23 | // 24 | // You can specify all the values or you can default the Build and Revision Numbers 25 | // by using the '*' as shown below: 26 | // [assembly: AssemblyVersion("1.0.*")] 27 | [assembly: AssemblyVersion("1.0.0.0")] 28 | [assembly: AssemblyFileVersion("1.0.0.0")] 29 | [assembly: ComVisible(false)] -------------------------------------------------------------------------------- /PullToRefreshXaml/Properties/Default.rd.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /PullToRefreshXaml/PullToRefreshXaml.nuget.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PullToRefreshXaml/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | PullToRefreshXaml 18 | Justin 19 | Assets\StoreLogo.png 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /PullToRefreshXaml.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.24627.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PullToRefreshXaml", "PullToRefreshXaml\PullToRefreshXaml.csproj", "{DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|ARM = Debug|ARM 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|ARM = Release|ARM 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|ARM.ActiveCfg = Debug|ARM 19 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|ARM.Build.0 = Debug|ARM 20 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|ARM.Deploy.0 = Debug|ARM 21 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|x64.ActiveCfg = Debug|x64 22 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|x64.Build.0 = Debug|x64 23 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|x64.Deploy.0 = Debug|x64 24 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|x86.ActiveCfg = Debug|x86 25 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|x86.Build.0 = Debug|x86 26 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Debug|x86.Deploy.0 = Debug|x86 27 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|ARM.ActiveCfg = Release|ARM 28 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|ARM.Build.0 = Release|ARM 29 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|ARM.Deploy.0 = Release|ARM 30 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|x64.ActiveCfg = Release|x64 31 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|x64.Build.0 = Release|x64 32 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|x64.Deploy.0 = Release|x64 33 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|x86.ActiveCfg = Release|x86 34 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|x86.Build.0 = Release|x86 35 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4}.Release|x86.Deploy.0 = Release|x86 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /PullToRefreshXaml/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /PullToRefreshXaml/PullToRefreshXaml.nuget.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | NuGet 6 | C:\Users\JustinLiu\Documents\GitHub\PullToRefreshUWP_WindowsComposition\PullToRefreshXaml\project.lock.json 7 | $(UserProfile)\.nuget\packages\ 8 | C:\Users\JustinLiu\.nuget\packages\ 9 | ProjectJson 10 | 4.0.0 11 | 12 | 13 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /PullToRefreshXaml/Model/Item.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using Windows.UI.Xaml.Media.Imaging; 5 | 6 | namespace PullToRefreshXaml.Model 7 | { 8 | public class Item 9 | { 10 | #region Properties 11 | private BitmapImage imageSource; 12 | private static List sources; 13 | private string id; 14 | 15 | public BitmapImage ImageSource 16 | { 17 | get 18 | { 19 | return imageSource; 20 | } 21 | 22 | set 23 | { 24 | imageSource = value; 25 | } 26 | } 27 | 28 | public string Id 29 | { 30 | get 31 | { 32 | return id; 33 | } 34 | 35 | set 36 | { 37 | id = value; 38 | } 39 | } 40 | 41 | public Item() 42 | { 43 | imageSource = new BitmapImage(); 44 | id = string.Empty; 45 | 46 | sources = new List() 47 | { 48 | new BitmapImage(new Uri("ms-appx:///Assets/cliff.jpg")), 49 | new BitmapImage(new Uri("ms-appx:///Assets/grapes_background.jpg")), 50 | new BitmapImage(new Uri("ms-appx:///Assets/LandscapeImage13.jpg")), 51 | new BitmapImage(new Uri("ms-appx:///Assets/LandscapeImage2.jpg")), 52 | new BitmapImage(new Uri("ms-appx:///Assets/LandscapeImage24.jpg")), 53 | new BitmapImage(new Uri("ms-appx:///Assets/LandscapeImage3.jpg")), 54 | new BitmapImage(new Uri("ms-appx:///Assets/LandscapeImage6.jpg")), 55 | new BitmapImage(new Uri("ms-appx:///Assets/LandscapeImage7.jpg")), 56 | new BitmapImage(new Uri("ms-appx:///Assets/Ring01.jpg")), 57 | new BitmapImage(new Uri("ms-appx:///Assets/Ring03Part1.jpg")), 58 | new BitmapImage(new Uri("ms-appx:///Assets/Ring03Part3.jpg")), 59 | new BitmapImage(new Uri("ms-appx:///Assets/sunset.jpg")) 60 | }; 61 | } 62 | 63 | public static Item GetNewItem(int i, int j) 64 | { 65 | return new Item() 66 | { 67 | imageSource = GenerateImageSource(j), 68 | id = i.ToString() 69 | }; 70 | } 71 | 72 | public static ObservableCollection GetItems(int numberOfContacts) 73 | { 74 | ObservableCollection items = new ObservableCollection(); 75 | 76 | for (int i = 0; i < numberOfContacts; i++) 77 | { 78 | items.Add(GetNewItem(i, i % 11)); 79 | } 80 | return items; 81 | } 82 | 83 | private static BitmapImage GenerateImageSource(int i) 84 | { 85 | return sources[i]; 86 | } 87 | #endregion 88 | } 89 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/MainPageTopDown.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PullToRefreshXaml.Model; 3 | using System.Collections.ObjectModel; 4 | using System.Threading.Tasks; 5 | using Windows.UI; 6 | using Windows.UI.Xaml; 7 | using Windows.UI.Xaml.Controls; 8 | using Windows.UI.Xaml.Media; 9 | using Windows.UI.Xaml.Media.Animation; 10 | 11 | namespace PullToRefreshXaml 12 | { 13 | public sealed partial class MainPageTopDown : Page 14 | { 15 | private readonly ObservableCollection _list = Contact.GetContactsGrouped(250); 16 | 17 | public MainPageTopDown() 18 | { 19 | InitializeComponent(); 20 | ContactsCVS.Source = _list; 21 | 22 | Loaded += OnLoaded; 23 | } 24 | 25 | private async void OnLoaded(object sender, RoutedEventArgs e) 26 | { 27 | await Task.Delay(1000); 28 | ListView.ContainerContentChanging += OnListViewContainerContentChanging; 29 | } 30 | 31 | private async void OnRefreshRequested(object sender, RefreshRequestedEventArgs args) 32 | { 33 | using (args.GetDeferral()) 34 | { 35 | await Task.Delay(2000); 36 | _list.Insert(0, Contact.GetContactsGrouped(1)[0]); 37 | } 38 | } 39 | 40 | private void OnListViewContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) 41 | { 42 | if (args.ItemContainer != null && !args.InRecycleQueue && args.Phase == 0) 43 | { 44 | var colorAnimation = new ColorAnimationUsingKeyFrames 45 | { 46 | // 'cause the new item comes in with an animation of which duration is about 300s, we add a little delay here to only 47 | // animate the color after it appears. 48 | BeginTime = TimeSpan.FromMilliseconds(300) 49 | }; 50 | var keyFrame1 = new LinearColorKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)), Value = Colors.White }; 51 | var keyFrame2 = new LinearColorKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(400)), Value = Colors.LightGray }; 52 | var keyFrame3 = new LinearColorKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1200)), Value = Colors.White }; 53 | colorAnimation.KeyFrames.Add(keyFrame1); 54 | colorAnimation.KeyFrames.Add(keyFrame2); 55 | colorAnimation.KeyFrames.Add(keyFrame3); 56 | 57 | ListView.Background = new SolidColorBrush(Colors.Transparent); 58 | Storyboard.SetTarget(colorAnimation, args.ItemContainer); 59 | Storyboard.SetTargetProperty(colorAnimation, "(Control.Background).(SolidColorBrush.Color)"); 60 | 61 | var storyboard = new Storyboard(); 62 | storyboard.Children.Add(colorAnimation); 63 | storyboard.Begin(); 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.ApplicationModel; 3 | using Windows.ApplicationModel.Activation; 4 | using Windows.UI.Xaml; 5 | using Windows.UI.Xaml.Controls; 6 | using Windows.UI.Xaml.Navigation; 7 | 8 | namespace PullToRefreshXaml 9 | { 10 | /// 11 | /// Provides application-specific behavior to supplement the default Application class. 12 | /// 13 | sealed partial class App : Application 14 | { 15 | /// 16 | /// Initializes the singleton application object. This is the first line of authored code 17 | /// executed, and as such is the logical equivalent of main() or WinMain(). 18 | /// 19 | public App() 20 | { 21 | this.InitializeComponent(); 22 | this.Suspending += OnSuspending; 23 | } 24 | 25 | /// 26 | /// Invoked when the application is launched normally by the end user. Other entry points 27 | /// will be used such as when the application is launched to open a specific file. 28 | /// 29 | /// Details about the launch request and process. 30 | protected override void OnLaunched(LaunchActivatedEventArgs e) 31 | { 32 | 33 | #if DEBUG 34 | if (System.Diagnostics.Debugger.IsAttached) 35 | { 36 | this.DebugSettings.EnableFrameRateCounter = true; 37 | } 38 | #endif 39 | 40 | Frame rootFrame = Window.Current.Content as Frame; 41 | 42 | // Do not repeat app initialization when the Window already has content, 43 | // just ensure that the window is active 44 | if (rootFrame == null) 45 | { 46 | // Create a Frame to act as the navigation context and navigate to the first page 47 | rootFrame = new Frame(); 48 | 49 | rootFrame.NavigationFailed += OnNavigationFailed; 50 | 51 | if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) 52 | { 53 | //TODO: Load state from previously suspended application 54 | } 55 | 56 | // Place the frame in the current Window 57 | Window.Current.Content = rootFrame; 58 | } 59 | 60 | if (rootFrame.Content == null) 61 | { 62 | // When the navigation stack isn't restored navigate to the first page, 63 | // configuring the new page by passing required information as a navigation 64 | // parameter 65 | rootFrame.Navigate(typeof(MainPageTopDown), e.Arguments); 66 | } 67 | // Ensure the current window is active 68 | Window.Current.Activate(); 69 | } 70 | 71 | /// 72 | /// Invoked when Navigation to a certain page fails 73 | /// 74 | /// The Frame which failed navigation 75 | /// Details about the navigation failure 76 | void OnNavigationFailed(object sender, NavigationFailedEventArgs e) 77 | { 78 | throw new Exception("Failed to load Page " + e.SourcePageType.FullName); 79 | } 80 | 81 | /// 82 | /// Invoked when application execution is being suspended. Application state is saved 83 | /// without knowing whether the application will be terminated or resumed with the contents 84 | /// of memory still intact. 85 | /// 86 | /// The source of the suspend request. 87 | /// Details about the suspend request. 88 | private void OnSuspending(object sender, SuspendingEventArgs e) 89 | { 90 | var deferral = e.SuspendingOperation.GetDeferral(); 91 | //TODO: Save application state and stop any background activity 92 | deferral.Complete(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /PullToRefreshXaml/MainPageTopDown.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 32 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /PullToRefreshXaml/AsyncDelegateCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows.Input; 4 | 5 | namespace PullToRefreshXaml 6 | { 7 | public class AsyncDelegateCommand : ICommand 8 | { 9 | #region Fields 10 | 11 | private readonly Func _execute; 12 | private readonly Func _canExecute; 13 | 14 | private bool _isExecuting; 15 | 16 | #endregion 17 | 18 | public AsyncDelegateCommand(Func execute, Func canExcute = null) 19 | { 20 | _execute = execute; 21 | _canExecute = canExcute ?? (() => true); 22 | } 23 | 24 | #region Events 25 | 26 | public event EventHandler CanExecuteChanged; 27 | 28 | #endregion 29 | 30 | #region Methods 31 | 32 | public bool CanExecute(object parameter) => !_isExecuting && _canExecute(); 33 | 34 | public bool CanExecute(object parameter, bool ignoreIsExecutingFlag) 35 | { 36 | if (ignoreIsExecutingFlag) 37 | { 38 | return _canExecute(); 39 | } 40 | 41 | return !_isExecuting && _canExecute(); 42 | } 43 | 44 | public async void Execute(object parameter) 45 | { 46 | _isExecuting = true; 47 | 48 | try 49 | { 50 | RaiseCanExecuteChanged(); 51 | await _execute(); 52 | } 53 | finally 54 | { 55 | _isExecuting = false; 56 | RaiseCanExecuteChanged(); 57 | } 58 | } 59 | 60 | public async Task ExecuteAsync(object parameter) 61 | { 62 | _isExecuting = true; 63 | 64 | try 65 | { 66 | RaiseCanExecuteChanged(); 67 | await _execute(); 68 | } 69 | finally 70 | { 71 | _isExecuting = false; 72 | RaiseCanExecuteChanged(); 73 | } 74 | } 75 | 76 | public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); 77 | 78 | #endregion 79 | } 80 | 81 | public class AsyncDelegateCommand : ICommand 82 | { 83 | #region Fields 84 | 85 | private readonly Func _execute; 86 | private readonly Func _canExecute; 87 | 88 | private bool _isExecuting; 89 | 90 | #endregion 91 | 92 | public AsyncDelegateCommand(Func execute, Func canExcute = null) 93 | { 94 | _execute = execute; 95 | _canExecute = canExcute ?? (x => true); 96 | } 97 | 98 | #region Events 99 | 100 | public event EventHandler CanExecuteChanged; 101 | 102 | #endregion 103 | 104 | #region Methods 105 | 106 | public bool CanExecute(object parameter) => !_isExecuting && _canExecute((T)parameter); 107 | 108 | public bool CanExecute(object parameter, bool ignoreIsExecutingFlag) 109 | { 110 | if (ignoreIsExecutingFlag) 111 | { 112 | return _canExecute((T)parameter); 113 | } 114 | 115 | return !_isExecuting && _canExecute((T)parameter); 116 | } 117 | 118 | public async void Execute(object parameter) 119 | { 120 | _isExecuting = true; 121 | 122 | try 123 | { 124 | RaiseCanExecuteChanged(); 125 | await _execute((T)parameter); 126 | } 127 | finally 128 | { 129 | _isExecuting = false; 130 | RaiseCanExecuteChanged(); 131 | } 132 | } 133 | 134 | public async Task ExecuteAsync(object parameter) 135 | { 136 | _isExecuting = true; 137 | 138 | try 139 | { 140 | RaiseCanExecuteChanged(); 141 | await _execute((T)parameter); 142 | } 143 | finally 144 | { 145 | _isExecuting = false; 146 | RaiseCanExecuteChanged(); 147 | } 148 | } 149 | 150 | public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); 151 | 152 | #endregion 153 | } 154 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/MainPageBottomUp.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 32 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /PullToRefreshXaml/PullToRefreshXaml.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | x86 7 | {DD6DDEB5-E3C9-4E0C-8B2F-4AC7FC4C69D4} 8 | AppContainerExe 9 | Properties 10 | PullToRefreshXaml 11 | PullToRefreshXaml 12 | en-US 13 | UAP 14 | 10.0.14393.0 15 | 10.0.14393.0 16 | 14 17 | 512 18 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | PullToRefreshXaml_TemporaryKey.pfx 20 | 21 | 22 | true 23 | bin\x86\Debug\ 24 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 25 | ;2008 26 | full 27 | x86 28 | false 29 | prompt 30 | true 31 | 32 | 33 | bin\x86\Release\ 34 | TRACE;NETFX_CORE;WINDOWS_UWP 35 | true 36 | ;2008 37 | pdbonly 38 | x86 39 | false 40 | prompt 41 | true 42 | true 43 | 44 | 45 | true 46 | bin\ARM\Debug\ 47 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 48 | ;2008 49 | full 50 | ARM 51 | false 52 | prompt 53 | true 54 | 55 | 56 | bin\ARM\Release\ 57 | TRACE;NETFX_CORE;WINDOWS_UWP 58 | true 59 | ;2008 60 | pdbonly 61 | ARM 62 | false 63 | prompt 64 | true 65 | true 66 | 67 | 68 | true 69 | bin\x64\Debug\ 70 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 71 | ;2008 72 | full 73 | x64 74 | false 75 | prompt 76 | true 77 | 78 | 79 | bin\x64\Release\ 80 | TRACE;NETFX_CORE;WINDOWS_UWP 81 | true 82 | ;2008 83 | pdbonly 84 | x64 85 | false 86 | prompt 87 | true 88 | true 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | App.xaml 97 | 98 | 99 | 100 | 101 | MainPageTopDown.xaml 102 | 103 | 104 | MainPageBottomUp.xaml 105 | 106 | 107 | MainPage.xaml 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Designer 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | MSBuild:Compile 136 | Designer 137 | 138 | 139 | MSBuild:Compile 140 | Designer 141 | 142 | 143 | MSBuild:Compile 144 | Designer 145 | 146 | 147 | MSBuild:Compile 148 | Designer 149 | 150 | 151 | 152 | 14.0 153 | 154 | 155 | 162 | -------------------------------------------------------------------------------- /PullToRefreshXaml/Model/Contact.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | 6 | namespace PullToRefreshXaml.Model 7 | { 8 | public class Contact 9 | { 10 | private static Random random = new Random(); 11 | 12 | #region Properties 13 | private string initials; 14 | public string Initials 15 | { 16 | get 17 | { 18 | if (initials == string.Empty && FirstName != string.Empty && LastName != string.Empty) 19 | { 20 | initials = FirstName[0].ToString() + LastName[0].ToString(); 21 | } 22 | return initials; 23 | } 24 | } 25 | private string name; 26 | public string Name 27 | { 28 | get 29 | { 30 | if (name == string.Empty && FirstName != string.Empty && LastName != string.Empty) 31 | { 32 | name = FirstName + " " + LastName; 33 | } 34 | return name; 35 | } 36 | } 37 | private string lastName; 38 | public string LastName 39 | { 40 | get 41 | { 42 | return lastName; 43 | } 44 | set 45 | { 46 | lastName = value; 47 | initials = string.Empty; // force to recalculate the value 48 | name = string.Empty; // force to recalculate the value 49 | } 50 | } 51 | private string firstName; 52 | public string FirstName 53 | { 54 | get 55 | { 56 | return firstName; 57 | } 58 | set 59 | { 60 | firstName = value; 61 | initials = string.Empty; // force to recalculate the value 62 | name = string.Empty; // force to recalculate the value 63 | } 64 | } 65 | public string Position { get; set; } 66 | public string PhoneNumber { get; set; } 67 | public string Biography { get; set; } 68 | #endregion 69 | 70 | public Contact() 71 | { 72 | // default values for each property. 73 | initials = string.Empty; 74 | name = string.Empty; 75 | LastName = string.Empty; 76 | FirstName = string.Empty; 77 | Position = string.Empty; 78 | PhoneNumber = string.Empty; 79 | Biography = string.Empty; 80 | } 81 | 82 | #region Public Methods 83 | public static Contact GetNewContact() 84 | { 85 | return new Contact() 86 | { 87 | FirstName = GenerateFirstName(), 88 | LastName = GenerateLastName(), 89 | Biography = GetBiography(), 90 | PhoneNumber = GeneratePhoneNumber(), 91 | Position = GeneratePosition(), 92 | }; 93 | } 94 | public static ObservableCollection GetContacts(int numberOfContacts) 95 | { 96 | ObservableCollection contacts = new ObservableCollection(); 97 | 98 | for (int i = 0; i < numberOfContacts; i++) 99 | { 100 | contacts.Add(GetNewContact()); 101 | } 102 | return contacts; 103 | } 104 | public static ObservableCollection GetContactsGrouped(int numberOfContacts) 105 | { 106 | ObservableCollection groups = new ObservableCollection(); 107 | 108 | var query = from item in GetContacts(numberOfContacts) 109 | group item by item.LastName[0] into g 110 | orderby g.Key 111 | select new { GroupName = g.Key, Items = g }; 112 | 113 | foreach (var g in query) 114 | { 115 | GroupInfoList info = new GroupInfoList(); 116 | info.Key = g.GroupName.ToString(); 117 | foreach (var item in g.Items) 118 | { 119 | info.Add(item); 120 | } 121 | groups.Add(info); 122 | } 123 | 124 | return groups; 125 | } 126 | #endregion 127 | 128 | #region Helpers 129 | private static string GeneratePosition() 130 | { 131 | List positions = new List() { "Program Manager", "Developer", "Product Manager", "Evangelist" }; 132 | return positions[random.Next(0, positions.Count)]; 133 | } 134 | private static string GetBiography() 135 | { 136 | List biographies = new List() 137 | { 138 | @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer id facilisis lectus. Cras nec convallis ante, quis pulvinar tellus. Integer dictum accumsan pulvinar. Pellentesque eget enim sodales sapien vestibulum consequat.", 139 | @"Maecenas eu sapien ac urna aliquam dictum.", 140 | @"Nullam eget mattis metus. Donec pharetra, tellus in mattis tincidunt, magna ipsum gravida nibh, vitae lobortis ante odio vel quam.", 141 | @"Quisque accumsan pretium ligula in faucibus. Mauris sollicitudin augue vitae lorem cursus condimentum quis ac mauris. Pellentesque quis turpis non nunc pretium sagittis. Nulla facilisi. Maecenas eu lectus ante. Proin eleifend vel lectus non tincidunt. Fusce condimentum luctus nisi, in elementum ante tincidunt nec.", 142 | @"Aenean in nisl at elit venenatis blandit ut vitae lectus. Praesent in sollicitudin nunc. Pellentesque justo augue, pretium at sem lacinia, scelerisque semper erat. Ut cursus tortor at metus lacinia dapibus.", 143 | @"Ut consequat magna luctus justo egestas vehicula. Integer pharetra risus libero, et posuere justo mattis et.", 144 | @"Proin malesuada, libero vitae aliquam venenatis, diam est faucibus felis, vitae efficitur erat nunc non mauris. Suspendisse at sodales erat.", 145 | @"Aenean vulputate, turpis non tincidunt ornare, metus est sagittis erat, id lobortis orci odio eget quam. Suspendisse ex purus, lobortis quis suscipit a, volutpat vitae turpis.", 146 | @"Duis facilisis, quam ut laoreet commodo, elit ex aliquet massa, non varius tellus lectus et nunc. Donec vitae risus ut ante pretium semper. Phasellus consectetur volutpat orci, eu dapibus turpis. Fusce varius sapien eu mattis pharetra.", 147 | @"Nam vulputate eu erat ornare blandit. Proin eget lacinia erat. Praesent nisl lectus, pretium eget leo et, dapibus dapibus velit. Integer at bibendum mi, et fringilla sem." 148 | }; 149 | return biographies[random.Next(0, biographies.Count)]; 150 | } 151 | 152 | private static string GeneratePhoneNumber() 153 | { 154 | return string.Format("{0:(###)} {1:###}-{2:####}", random.Next(100, 999), random.Next(100, 999), random.Next(1000, 9999)); 155 | } 156 | private static string GenerateFirstName() 157 | { 158 | List names = new List() { "Lilly", "Mukhtar", "Sophie", "Femke", "Abdul-Rafi'", "Chirag-ud-D...", "Mariana", "Aarif", "Sara", "Ibadah", "Fakhr", "Ilene", "Sardar", "Hanna", "Julie", "Iain", "Natalia", "Henrik", "Rasa", "Quentin", "Gadi", "Pernille", "Ishtar", "Jimme", "Justina", "Lale", "Elize", "Rand", "Roshanara", "Rajab", "Bijou", "Marcus", "Marcus", "Alima", "Francisco", "Thaqib", "Andreas", "Mariana", "Amalie", "Rodney", "Dena", "Fadl", "Ammar", "Anna", "Nasreen", "Reem", "Tomas", "Filipa", "Frank", "Bari'ah", "Parvaiz", "Jibran", "Tomas", "Elli", "Carlos", "Diego", "Henrik", "Aruna", "Vahid", "Eliana", "Roxane", "Amanda", "Ingrid", "Wander", "Malika", "Basim", "Eisa", "Alina", "Andreas", "Deeba", "Diya", "Parveen", "Bakr", "Celine", "Bakr", "Marcus", "Daniel", "Mathea", "Edmee", "Hedda", "Maria", "Maja", "Alhasan", "Alina", "Hedda", "Victor", "Aaftab", "Guilherme", "Maria", "Kai", "Sabien", "Abdel", "Fadl", "Bahaar", "Vasco", "Jibran", "Parsa", "Catalina", "Fouad", "Colette" }; 159 | return names[random.Next(0, names.Count)]; 160 | } 161 | private static string GenerateLastName() 162 | { 163 | List lastnames = new List() { "Carlson", "Attia", "Quint", "Hollenberg", "Khoury", "Araujo", "Hakimi", "Seegers", "Abadi", "al", "Krommenhoek", "Siavashi", "Kvistad", "Sjo", "Vanderslik", "Fernandes", "Dehli", "Sheibani", "Laamers", "Batlouni", "Lyngvær", "Oveisi", "Veenhuizen", "Gardenier", "Siavashi", "Mutlu", "Karzai", "Mousavi", "Natsheh", "Seegers", "Nevland", "Lægreid", "Bishara", "Cunha", "Hotaki", "Kyvik", "Cardoso", "Pilskog", "Pennekamp", "Nuijten", "Bettar", "Borsboom", "Skistad", "Asef", "Sayegh", "Sousa", "Medeiros", "Kregel", "Shamoun", "Behzadi", "Kuzbari", "Ferreira", "Van", "Barros", "Fernandes", "Formo", "Nolet", "Shahrestaani", "Correla", "Amiri", "Sousa", "Fretheim", "Van", "Hamade", "Baba", "Mustafa", "Bishara", "Formo", "Hemmati", "Nader", "Hatami", "Natsheh", "Langen", "Maloof", "Berger", "Ostrem", "Bardsen", "Kramer", "Bekken", "Salcedo", "Holter", "Nader", "Bettar", "Georgsen", "Cunha", "Zardooz", "Araujo", "Batalha", "Antunes", "Vanderhoorn", "Nader", "Abadi", "Siavashi", "Montes", "Sherzai", "Vanderschans", "Neves", "Sarraf", "Kuiters" }; 164 | return lastnames[random.Next(0, lastnames.Count)]; 165 | } 166 | #endregion 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /PullToRefreshXaml/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using PullToRefreshXaml.Model; 2 | using System; 3 | using System.Collections.ObjectModel; 4 | using System.Diagnostics; 5 | using System.Numerics; 6 | using System.Threading.Tasks; 7 | using Windows.UI.Composition; 8 | using Windows.UI.Xaml.Controls; 9 | using Windows.UI.Xaml.Hosting; 10 | using Windows.UI.Xaml.Media; 11 | 12 | namespace PullToRefreshXaml 13 | { 14 | public sealed partial class MainPage : Page 15 | { 16 | ScrollViewer _scrollViewer; 17 | Compositor _compositor; 18 | readonly ObservableCollection _list = Contact.GetContactsGrouped(250); 19 | 20 | CompositionPropertySet _scrollerViewerManipulation; 21 | ExpressionAnimation _rotationAnimation, _opacityAnimation, _offsetAnimation; 22 | ScalarKeyFrameAnimation _resetAnimation, _loadingAnimation; 23 | 24 | Visual _borderVisual; 25 | Visual _refreshIconVisual; 26 | float _refreshIconOffsetY; 27 | const float REFRESH_ICON_MAX_OFFSET_Y = 36.0f; 28 | 29 | bool _refresh; 30 | DateTime _pulledDownTime, _restoredTime; 31 | 32 | public MainPage() 33 | { 34 | this.InitializeComponent(); 35 | ContactsCVS.Source = _list; 36 | 37 | this.Loaded += (s, e) => 38 | { 39 | _scrollViewer = ListView.GetScrollViewer(); 40 | _scrollViewer.DirectManipulationStarted += OnDirectManipulationStarted; 41 | _scrollViewer.DirectManipulationCompleted += OnDirectManipulationCompleted; 42 | 43 | // Retrieve the ScrollViewer manipulation and the Compositor. 44 | _scrollerViewerManipulation = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollViewer); 45 | _compositor = _scrollerViewerManipulation.Compositor; 46 | 47 | // At the moment there are three things happening when pulling down the list - 48 | // 1. The Refresh Icon fades in. 49 | // 2. The Refresh Icon rotates (400°). 50 | // 3. The Refresh Icon gets pulled down a bit (REFRESH_ICON_MAX_OFFSET_Y) 51 | // QUESTION 5 52 | // Can we also have Geometric Path animation so we can also draw the Refresh Icon along the way? 53 | // 54 | 55 | // Create a rotation expression animation based on the overpan distance of the ScrollViewer. 56 | _rotationAnimation = _compositor.CreateExpressionAnimation("min(max(0, ScrollManipulation.Translation.Y) * Multiplier, MaxDegree)"); 57 | _rotationAnimation.SetScalarParameter("Multiplier", 10.0f); 58 | _rotationAnimation.SetScalarParameter("MaxDegree", 400.0f); 59 | _rotationAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); 60 | 61 | // Create an opacity expression animation based on the overpan distance of the ScrollViewer. 62 | _opacityAnimation = _compositor.CreateExpressionAnimation("min(max(0, ScrollManipulation.Translation.Y) / Divider, 1)"); 63 | _opacityAnimation.SetScalarParameter("Divider", 30.0f); 64 | _opacityAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); 65 | 66 | // Create an offset expression animation based on the overpan distance of the ScrollViewer. 67 | _offsetAnimation = _compositor.CreateExpressionAnimation("(min(max(0, ScrollManipulation.Translation.Y) / Divider, 1)) * MaxOffsetY"); 68 | _offsetAnimation.SetScalarParameter("Divider", 30.0f); 69 | _offsetAnimation.SetScalarParameter("MaxOffsetY", REFRESH_ICON_MAX_OFFSET_Y); 70 | _offsetAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); 71 | 72 | // Create a keyframe animation to reset properties like Offset.Y, Opacity, etc. 73 | _resetAnimation = _compositor.CreateScalarKeyFrameAnimation(); 74 | _resetAnimation.InsertKeyFrame(1.0f, 0.0f); 75 | 76 | // Create a loading keyframe animation (in this case, a rotation animation). 77 | _loadingAnimation = _compositor.CreateScalarKeyFrameAnimation(); 78 | _loadingAnimation.InsertKeyFrame(1.0f, 360); 79 | _loadingAnimation.Duration = TimeSpan.FromMilliseconds(1200); 80 | _loadingAnimation.IterationBehavior = AnimationIterationBehavior.Forever; 81 | 82 | // Get the RefreshIcon's Visual. 83 | _refreshIconVisual = ElementCompositionPreview.GetElementVisual(RefreshIcon); 84 | // Set the center point for the rotation animation. 85 | _refreshIconVisual.CenterPoint = new Vector3(Convert.ToSingle(RefreshIcon.ActualWidth / 2), Convert.ToSingle(RefreshIcon.ActualHeight / 2), 0); 86 | 87 | // Get the ListView's inner Border's Visual. 88 | var border = (Border)VisualTreeHelper.GetChild(ListView, 0); 89 | _borderVisual = ElementCompositionPreview.GetElementVisual(border); 90 | 91 | PrepareExpressionAnimationsOnScroll(); 92 | }; 93 | 94 | this.Unloaded += (s, e) => 95 | { 96 | _scrollViewer.DirectManipulationStarted -= OnDirectManipulationStarted; 97 | _scrollViewer.DirectManipulationCompleted -= OnDirectManipulationCompleted; 98 | }; 99 | } 100 | 101 | private void PrepareExpressionAnimationsOnScroll() 102 | { 103 | _refreshIconVisual.StartAnimation("RotationAngleInDegrees", _rotationAnimation); 104 | _refreshIconVisual.StartAnimation("Opacity", _opacityAnimation); 105 | _refreshIconVisual.StartAnimation("Offset.Y", _offsetAnimation); 106 | _borderVisual.StartAnimation("Offset.Y", _offsetAnimation); 107 | } 108 | 109 | void OnDirectManipulationStarted(object sender, object e) 110 | { 111 | // QUESTION 1 112 | // I cannot think of a better way to monitor overpan changes, maybe there should be an Animating event? 113 | // 114 | Windows.UI.Xaml.Media.CompositionTarget.Rendering += OnCompositionTargetRendering; 115 | 116 | // Initialise the values. 117 | _refresh = false; 118 | } 119 | 120 | async void OnDirectManipulationCompleted(object sender, object e) 121 | { 122 | Windows.UI.Xaml.Media.CompositionTarget.Rendering -= OnCompositionTargetRendering; 123 | 124 | //Debug.WriteLine($"ScrollViewer Rollback animation duration: {(_restoredTime - _pulledDownTime).Milliseconds}"); 125 | 126 | // The ScrollViewer's rollback animation is appx. 200ms. So if the duration between the two DateTimes we recorded earlier 127 | // is greater than 250ms, we should cancel the refresh. 128 | var cancelled = (_restoredTime - _pulledDownTime) > TimeSpan.FromMilliseconds(250); 129 | 130 | if (_refresh) 131 | { 132 | if (cancelled) 133 | { 134 | Debug.WriteLine("Refresh cancelled..."); 135 | 136 | StartResetAnimations(); 137 | } 138 | else 139 | { 140 | Debug.WriteLine("Refresh now!!!"); 141 | 142 | await StartLoadingAnimation(() => StartResetAnimations()); 143 | } 144 | } 145 | } 146 | 147 | async Task StartLoadingAnimation(Action completed) 148 | { 149 | // Create a short delay to allow the expression rotation animation to more smoothly transition 150 | // to the new keyframe animation 151 | await Task.Delay(100); 152 | 153 | _refreshIconVisual.StartAnimation("RotationAngleInDegrees", _loadingAnimation); 154 | 155 | await FakeServiceCall(); 156 | completed(); 157 | } 158 | 159 | void StartResetAnimations() 160 | { 161 | var batch = _compositor.CreateScopedBatch(CompositionBatchTypes.Animation); 162 | // Looks like expression aniamtions will be removed after the following keyframe 163 | // animations have run. So here I have to re-start them once the keyframe animations 164 | // are completed. 165 | batch.Completed += (s, e) => PrepareExpressionAnimationsOnScroll(); 166 | 167 | _borderVisual.StartAnimation("Offset.Y", _resetAnimation); 168 | _refreshIconVisual.StartAnimation("Opacity", _resetAnimation); 169 | batch.End(); 170 | } 171 | 172 | async Task FakeServiceCall() 173 | { 174 | await Task.Delay(2000); 175 | 176 | _list.Insert(0, Contact.GetContactsGrouped(1)[0]); 177 | } 178 | 179 | void OnCompositionTargetRendering(object sender, object e) 180 | { 181 | // QUESTION 2 182 | // What I've noticed is that I have to manually stop and 183 | // start the animation otherwise the Offset.Y is 0. Why? 184 | // 185 | _refreshIconVisual.StopAnimation("Offset.Y"); 186 | 187 | // QUESTION 3 188 | // Why is the Translation always (0,0,0)? 189 | // 190 | //Vector3 translation; 191 | //var translationStatus = _scrollerViewerManipulation.TryGetVector3("Translation", out translation); 192 | //switch (translationStatus) 193 | //{ 194 | // case CompositionGetValueStatus.Succeeded: 195 | // Debug.WriteLine($"ScrollViewer's Translation Y: {translation.Y}"); 196 | // break; 197 | // case CompositionGetValueStatus.TypeMismatch: 198 | // case CompositionGetValueStatus.NotFound: 199 | // default: 200 | // break; 201 | //} 202 | 203 | _refreshIconOffsetY = _refreshIconVisual.Offset.Y; 204 | //Debug.WriteLine($"RefreshIcon's Offset Y: {_refreshIconOffsetY}"); 205 | 206 | // Question 4 207 | // It's not always the case here as the user can pull it all the way down and then push it back up to 208 | // CANCEL a refresh!! Though I cannot seem to find an easy way to detect right after the finger is lifted. 209 | // DirectManipulationCompleted is called too late. 210 | // What might be really helpful is to have a DirectManipulationDelta event with velocity and other values. 211 | // 212 | // At the moment I am calculating the time difference between the list gets pulled all the way down and rolled back up. 213 | // 214 | if (!_refresh) 215 | { 216 | _refresh = _refreshIconOffsetY == REFRESH_ICON_MAX_OFFSET_Y; 217 | } 218 | 219 | if (_refreshIconOffsetY == REFRESH_ICON_MAX_OFFSET_Y) 220 | { 221 | _pulledDownTime = DateTime.Now; 222 | //Debug.WriteLine($"When the list is pulled down: {_pulledDownTime}"); 223 | 224 | // Stop the Opacity animation on the RefreshIcon and the Offset.Y animation on the Border (ScrollViewer's host) 225 | _refreshIconVisual.StopAnimation("Opacity"); 226 | _borderVisual.StopAnimation("Offset.Y"); 227 | } 228 | 229 | if (_refresh && _refreshIconOffsetY <= 1) 230 | { 231 | _restoredTime = DateTime.Now; 232 | //Debug.WriteLine($"When the list is back up: {_restoredTime}"); 233 | } 234 | 235 | _refreshIconVisual.StartAnimation("Offset.Y", _offsetAnimation); 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /PullToRefreshXaml/PullToRefreshBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Numerics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Windows.Foundation; 7 | using Windows.UI.Composition; 8 | using Windows.UI.Xaml; 9 | using Windows.UI.Xaml.Controls; 10 | using Windows.UI.Xaml.Hosting; 11 | using Windows.UI.Xaml.Media; 12 | using Microsoft.Xaml.Interactivity; 13 | 14 | namespace PullToRefreshXaml 15 | { 16 | public class PullToRefreshBehavior : Behavior 17 | { 18 | #region Fields 19 | 20 | private ScrollViewer _scrollViewer; 21 | private Compositor _compositor; 22 | 23 | private const float ScrollViewerMaxOverpanViewportRatio = 0.1f; 24 | 25 | private CompositionPropertySet _scrollerViewerManipulation; 26 | private ExpressionAnimation _rotationAnimation, _opacityAnimation, _offsetAnimation; 27 | private ScalarKeyFrameAnimation _resetAnimation, _loadingAnimation; 28 | 29 | private Visual _borderVisual; 30 | private Visual _refreshIconVisual; 31 | private float _refreshIconOffsetY; 32 | 33 | private bool _refresh; 34 | private DateTime _pulledDownTime, _restoredTime; 35 | 36 | private Task _pendingRefreshTask; 37 | private CancellationTokenSource _cts; 38 | 39 | private long _callbackId; 40 | 41 | #endregion 42 | 43 | #region Properties 44 | 45 | public FrameworkElement IconElement 46 | { 47 | get { return (FrameworkElement)GetValue(IconElementProperty); } 48 | set { SetValue(IconElementProperty, value); } 49 | } 50 | public static readonly DependencyProperty IconElementProperty = DependencyProperty.Register("IconElement", typeof(FrameworkElement), typeof(PullToRefreshBehavior), new PropertyMetadata(null)); 51 | 52 | public double IconElementMaxPulledDistance 53 | { 54 | get { return (double)GetValue(IconElementMaxPulledDistanceProperty); } 55 | set { SetValue(IconElementMaxPulledDistanceProperty, value); } 56 | } 57 | public static readonly DependencyProperty IconElementMaxPulledDistanceProperty = DependencyProperty.Register("IconElementMaxPulledDistance", typeof(double), typeof(PullToRefreshBehavior), new PropertyMetadata(36.0d)); 58 | 59 | public double IconElementMaxRotationAngle 60 | { 61 | get { return (double)GetValue(IconElementMaxRotationAngleProperty); } 62 | set { SetValue(IconElementMaxRotationAngleProperty, value); } 63 | } 64 | public static readonly DependencyProperty IconElementMaxRotationAngleProperty = DependencyProperty.Register("IconElementMaxRotationAngle", typeof(double), typeof(PullToRefreshBehavior), new PropertyMetadata(400.0d)); 65 | 66 | public double PullThresholdMaxOverpanRatio 67 | { 68 | get { return (double)GetValue(PullThresholdMaxOverpanRatioProperty); } 69 | set { SetValue(PullThresholdMaxOverpanRatioProperty, value); } 70 | } 71 | public static readonly DependencyProperty PullThresholdMaxOverpanRatioProperty = DependencyProperty.Register("PullThresholdMaxOverpanRatio", typeof(double), typeof(PullToRefreshBehavior), new PropertyMetadata(0.5d, 72 | (s, e) => 73 | { 74 | var threshold = (double)e.NewValue; 75 | if (threshold <= 0 || threshold > 1) 76 | { 77 | throw new ArgumentOutOfRangeException($"{nameof(PullThresholdMaxOverpanRatio)} should be in between 0 and 1"); 78 | } 79 | })); 80 | 81 | public AsyncDelegateCommand RefreshCommand 82 | { 83 | get { return (AsyncDelegateCommand)GetValue(RefreshCommandProperty); } 84 | set { SetValue(RefreshCommandProperty, value); } 85 | } 86 | public static readonly DependencyProperty RefreshCommandProperty = DependencyProperty.Register("RefreshCommand", typeof(AsyncDelegateCommand), typeof(PullToRefreshBehavior), new PropertyMetadata(null)); 87 | 88 | public PullDirection PullDirection 89 | { 90 | get { return (PullDirection)GetValue(PullDirectionProperty); } 91 | set { SetValue(PullDirectionProperty, value); } 92 | } 93 | public static readonly DependencyProperty PullDirectionProperty = DependencyProperty.Register("PullDirection", typeof(PullDirection), typeof(PullToRefreshBehavior), new PropertyMetadata(PullDirection.TopDown)); 94 | 95 | private float IconElementMaxPulledOffsetY 96 | => (float)IconElementMaxPulledDistance * (PullDirection == PullDirection.TopDown ? 1 : -1); 97 | 98 | private string PullDistanceExpression => 99 | $"max(0, {(PullDirection == PullDirection.TopDown ? string.Empty : "-")}ScrollManipulation.Translation.Y{(PullDirection == PullDirection.TopDown ? string.Empty : " -ScrollManipulation.ScrollableHeight")})"; 100 | 101 | private string PullThresholdExpression => $"(ScrollManipulation.MaxOverpan * {nameof(PullThresholdMaxOverpanRatio)})"; 102 | 103 | #endregion 104 | 105 | #region Events 106 | 107 | public event RefreshRequestedEventHandler RefreshRequested; 108 | 109 | #endregion 110 | 111 | #region Overrides 112 | 113 | /// 114 | /// Called after the behavior is attached to the . 115 | /// 116 | protected override void OnAttached() 117 | { 118 | base.OnAttached(); 119 | 120 | AssociatedObject.Loaded += OnAssociatedObjectLoaded; 121 | AssociatedObject.Unloaded += OnAssociatedObjectUnloaded; 122 | } 123 | 124 | /// 125 | /// Called when the behavior is being detached from its . 126 | /// 127 | protected override void OnDetaching() 128 | { 129 | base.OnDetaching(); 130 | 131 | _compositor?.Dispose(); 132 | _scrollerViewerManipulation?.Dispose(); 133 | _rotationAnimation?.Dispose(); 134 | _opacityAnimation?.Dispose(); 135 | _offsetAnimation?.Dispose(); 136 | _resetAnimation?.Dispose(); 137 | _loadingAnimation?.Dispose(); 138 | _borderVisual?.Dispose(); 139 | _refreshIconVisual?.Dispose(); 140 | 141 | AssociatedObject.Loaded -= OnAssociatedObjectLoaded; 142 | AssociatedObject.Unloaded -= OnAssociatedObjectUnloaded; 143 | } 144 | 145 | #endregion 146 | 147 | #region Handlers 148 | 149 | private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e) 150 | { 151 | _scrollViewer = AssociatedObject.GetScrollViewer(); 152 | _scrollViewer.DirectManipulationStarted += OnScrollViewerDirectManipulationStarted; 153 | _scrollViewer.DirectManipulationCompleted += OnScrollViewerDirectManipulationCompleted; 154 | _callbackId = _scrollViewer.RegisterPropertyChangedCallback(ScrollViewer.ScrollableHeightProperty, 155 | OnScrollViewerScrollableHeightChanged); 156 | 157 | // Retrieve the ScrollViewer manipulation and the Compositor. 158 | _scrollerViewerManipulation = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollViewer); 159 | _compositor = _scrollerViewerManipulation.Compositor; 160 | 161 | // Set boundaries. 162 | var listViewBaseVisual = ElementCompositionPreview.GetElementVisual(AssociatedObject); 163 | var clip = _compositor.CreateInsetClip(); 164 | listViewBaseVisual.Clip = clip; 165 | 166 | // At the moment there are three things happening when pulling down the list - 167 | // 1. The Refresh Icon fades in. 168 | // 2. The Refresh Icon rotates (IconElementMaxRotationAngle). 169 | // 3. The Refresh Icon gets pulled down/up a bit (IconElementMaxOffsetY) 170 | // QUESTION 5 171 | // Can we also have Geometric Path animation so we can also draw the Refresh Icon along the way? 172 | // 173 | 174 | UpdateScrollableHeightInScrollViewerPropertySet(); 175 | 176 | // Create a rotation expression animation based on the overpan distance of the ScrollViewer. 177 | _rotationAnimation = _compositor.CreateExpressionAnimation($"min({PullDistanceExpression} * DegreeMultiplier, MaxDegree)"); 178 | _rotationAnimation.SetScalarParameter("DegreeMultiplier", 10.0f); 179 | _rotationAnimation.SetScalarParameter("MaxDegree", (float)IconElementMaxRotationAngle); 180 | _rotationAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); 181 | 182 | // Create an opacity expression animation based on the overpan distance of the ScrollViewer. 183 | _opacityAnimation = _compositor.CreateExpressionAnimation($"min({PullDistanceExpression} / {PullThresholdExpression}, 1)"); 184 | _opacityAnimation.SetScalarParameter("PullThresholdMaxOverpanRatio", (float)PullThresholdMaxOverpanRatio); 185 | _opacityAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); 186 | 187 | // Create an offset expression animation based on the overpan distance of the ScrollViewer. 188 | _offsetAnimation = _compositor.CreateExpressionAnimation($"min({PullDistanceExpression} / {PullThresholdExpression}, 1) * MaxPulledDistance"); 189 | _offsetAnimation.SetScalarParameter("PullThresholdMaxOverpanRatio", (float)PullThresholdMaxOverpanRatio); 190 | _offsetAnimation.SetScalarParameter("MaxPulledDistance", IconElementMaxPulledOffsetY); 191 | _offsetAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); 192 | 193 | // Create a keyframe animation to reset properties like Offset.Y, Opacity, etc. 194 | _resetAnimation = _compositor.CreateScalarKeyFrameAnimation(); 195 | _resetAnimation.InsertKeyFrame(1.0f, 0.0f); 196 | 197 | // Create a loading keyframe animation (in this case, a rotation animation). 198 | _loadingAnimation = _compositor.CreateScalarKeyFrameAnimation(); 199 | _loadingAnimation.InsertExpressionKeyFrame(0.0f, "this.StartingValue"); 200 | _loadingAnimation.InsertExpressionKeyFrame(1.0f, "this.StartingValue + 360"); 201 | _loadingAnimation.Duration = TimeSpan.FromMilliseconds(1200); 202 | _loadingAnimation.IterationBehavior = AnimationIterationBehavior.Forever; 203 | 204 | // Get the RefreshIcon's Visual. 205 | _refreshIconVisual = ElementCompositionPreview.GetElementVisual(IconElement); 206 | // Set the center point for the rotation animation. 207 | _refreshIconVisual.CenterPoint = new Vector3(IconElement.RenderSize.ToVector2() / 2, 0.0f); 208 | 209 | // Get the ListView's inner Border's Visual. 210 | var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0); 211 | _borderVisual = ElementCompositionPreview.GetElementVisual(border); 212 | 213 | StartExpressionAnimations(); 214 | } 215 | 216 | private void OnAssociatedObjectUnloaded(object sender, RoutedEventArgs e) 217 | { 218 | _scrollViewer.UnregisterPropertyChangedCallback(ScrollViewer.ScrollableHeightProperty, _callbackId); 219 | _scrollViewer.DirectManipulationStarted -= OnScrollViewerDirectManipulationStarted; 220 | _scrollViewer.DirectManipulationCompleted -= OnScrollViewerDirectManipulationCompleted; 221 | } 222 | 223 | private void OnScrollViewerScrollableHeightChanged(DependencyObject sender, DependencyProperty dp) => 224 | UpdateScrollableHeightInScrollViewerPropertySet(); 225 | 226 | private void OnScrollViewerDirectManipulationStarted(object sender, object e) 227 | { 228 | // QUESTION 1 229 | // I cannot think of a better way to monitor overpan changes, maybe there should be an Animating event? 230 | // 231 | Windows.UI.Xaml.Media.CompositionTarget.Rendering += OnCompositionTargetRendering; 232 | 233 | // Initialise the values. 234 | _refresh = false; 235 | } 236 | 237 | private async void OnScrollViewerDirectManipulationCompleted(object sender, object e) 238 | { 239 | Windows.UI.Xaml.Media.CompositionTarget.Rendering -= OnCompositionTargetRendering; 240 | 241 | //Debug.WriteLine($"ScrollViewer Rollback animation duration: {(_restoredTime - _pulledDownTime).Milliseconds}"); 242 | 243 | // The ScrollViewer's rollback animation is appx. 200ms. So if the duration between the two DateTimes we recorded earlier 244 | // is greater than 250ms, we should cancel the refresh. 245 | var cancelled = _restoredTime - _pulledDownTime > TimeSpan.FromMilliseconds(250); 246 | 247 | if (!_refresh) return; 248 | 249 | if (cancelled) 250 | { 251 | Debug.WriteLine("Refresh cancelled..."); 252 | StartResetAnimations(); 253 | } 254 | else 255 | { 256 | Debug.WriteLine("Refresh now!!!"); 257 | await StartLoadingAnimationAndRequestRefreshAsync(StartResetAnimations); 258 | } 259 | } 260 | 261 | private void OnCompositionTargetRendering(object sender, object e) 262 | { 263 | // QUESTION 2 264 | // What I've noticed is that I have to manually stop and 265 | // start the animation otherwise the Offset.Y is 0. Why? 266 | // 267 | _refreshIconVisual.StopAnimation("Offset.Y"); 268 | 269 | // QUESTION 3 270 | // Why is the Translation always (0,0,0)? 271 | // 272 | //Vector3 translation; 273 | //var translationStatus = _scrollerViewerManipulation.TryGetVector3("Translation", out translation); 274 | //switch (translationStatus) 275 | //{ 276 | // case CompositionGetValueStatus.Succeeded: 277 | // Debug.WriteLine($"ScrollViewer's Translation Y: {translation.Y}"); 278 | // break; 279 | // case CompositionGetValueStatus.TypeMismatch: 280 | // case CompositionGetValueStatus.NotFound: 281 | // default: 282 | // break; 283 | //} 284 | 285 | _refreshIconOffsetY = _refreshIconVisual.Offset.Y; 286 | //Debug.WriteLine($"RefreshIcon's Offset Y: {_refreshIconOffsetY}"); 287 | 288 | // Question 4 289 | // It's not always the case here as the user can pull it all the way down and then push it back up to 290 | // CANCEL a refresh!! Though I cannot seem to find an easy way to detect right after the finger is lifted. 291 | // DirectManipulationCompleted is called too late. 292 | // What might be really helpful is to have a DirectManipulationDelta event with velocity and other values. 293 | // 294 | // At the moment I am calculating the time difference between the list gets pulled all the way down and rolled back up. 295 | // 296 | if (!_refresh) 297 | { 298 | _refresh = _refreshIconOffsetY.AlmostEqual(IconElementMaxPulledOffsetY); 299 | } 300 | 301 | if (_refreshIconOffsetY.AlmostEqual(IconElementMaxPulledOffsetY)) 302 | { 303 | _pulledDownTime = DateTime.Now; 304 | //Debug.WriteLine($"When the list is pulled down: {_pulledDownTime}"); 305 | 306 | // Stop the Opacity animation on the RefreshIcon and the Offset.Y animation on the Border (ScrollViewer's host) 307 | _refreshIconVisual.StopAnimation("Opacity"); 308 | _borderVisual.StopAnimation("Offset.Y"); 309 | } 310 | 311 | if (_refresh && _refreshIconOffsetY <= 1) 312 | { 313 | _restoredTime = DateTime.Now; 314 | //Debug.WriteLine($"When the list is back up: {_restoredTime}"); 315 | } 316 | 317 | _refreshIconVisual.StartAnimation("Offset.Y", _offsetAnimation); 318 | } 319 | 320 | #endregion 321 | 322 | #region Methods 323 | 324 | private void StartExpressionAnimations() 325 | { 326 | _refreshIconVisual.StartAnimation("RotationAngleInDegrees", _rotationAnimation); 327 | _refreshIconVisual.StartAnimation("Opacity", _opacityAnimation); 328 | _refreshIconVisual.StartAnimation("Offset.Y", _offsetAnimation); 329 | _borderVisual.StartAnimation("Offset.Y", _offsetAnimation); 330 | } 331 | 332 | private void StopExpressionAnimations() 333 | { 334 | _refreshIconVisual.StopAnimation("RotationAngleInDegrees"); 335 | _refreshIconVisual.StopAnimation("Opacity"); 336 | _refreshIconVisual.StopAnimation("Offset.Y"); 337 | _borderVisual.StopAnimation("Offset.Y"); 338 | } 339 | 340 | private async Task StartLoadingAnimationAndRequestRefreshAsync(Action completed) 341 | { 342 | // Create a short delay to allow the expression rotation animation to smoothly transition 343 | // to the new keyframe animation. 344 | await Task.Delay(100); 345 | 346 | _refreshIconVisual.StartAnimation("RotationAngleInDegrees", _loadingAnimation); 347 | 348 | // When using the event... 349 | if (RefreshRequested != null) 350 | { 351 | var e = new RefreshRequestedEventArgs(new DeferralCompletedHandler(completed)); 352 | RefreshRequested.Invoke(this, e); 353 | } 354 | 355 | // When using the command... 356 | if (RefreshCommand != null) 357 | { 358 | try 359 | { 360 | _cts?.Cancel(); 361 | _cts = new CancellationTokenSource(); 362 | 363 | _pendingRefreshTask = DoCancellableRefreshTaskAsync(_cts.Token); 364 | await _pendingRefreshTask; 365 | } 366 | catch (OperationCanceledException) 367 | { 368 | } 369 | 370 | if (_cts != null && !_cts.IsCancellationRequested) 371 | { 372 | completed(); 373 | } 374 | } 375 | } 376 | 377 | private async Task DoCancellableRefreshTaskAsync(CancellationToken token) 378 | { 379 | try 380 | { 381 | if (_pendingRefreshTask != null) 382 | { 383 | await _pendingRefreshTask; 384 | } 385 | } 386 | catch (OperationCanceledException) 387 | { 388 | } 389 | 390 | token.ThrowIfCancellationRequested(); 391 | 392 | if (RefreshCommand != null && RefreshCommand.CanExecute(token, true)) 393 | { 394 | await RefreshCommand.ExecuteAsync(token); 395 | } 396 | } 397 | 398 | private void StartResetAnimations() 399 | { 400 | StopExpressionAnimations(); 401 | 402 | var batch = _compositor.CreateScopedBatch(CompositionBatchTypes.Animation); 403 | // Looks like expression aniamtions will be removed after the following keyframe 404 | // animations have run. So here I have to re-start them once the keyframe animations 405 | // are completed. 406 | batch.Completed += (s, e) => StartExpressionAnimations(); 407 | 408 | _borderVisual.StartAnimation("Offset.Y", _resetAnimation); 409 | _refreshIconVisual.StartAnimation("Opacity", _resetAnimation); 410 | 411 | batch.End(); 412 | } 413 | 414 | private void UpdateScrollableHeightInScrollViewerPropertySet() 415 | { 416 | _scrollerViewerManipulation.InsertScalar("MaxOverpan", (float)_scrollViewer.ViewportHeight * ScrollViewerMaxOverpanViewportRatio); 417 | 418 | if (PullDirection == PullDirection.BottomUp) 419 | { 420 | _scrollerViewerManipulation.InsertScalar("ScrollableHeight", (float)_scrollViewer.ScrollableHeight); 421 | } 422 | } 423 | 424 | #endregion 425 | } 426 | 427 | public enum PullDirection 428 | { 429 | TopDown, 430 | BottomUp 431 | } 432 | } 433 | --------------------------------------------------------------------------------