Plugin.Maui.PullToRefresh 0.1.3

dotnet add package Plugin.Maui.PullToRefresh --version 0.1.3
                    
NuGet\Install-Package Plugin.Maui.PullToRefresh -Version 0.1.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Plugin.Maui.PullToRefresh" Version="0.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Plugin.Maui.PullToRefresh" Version="0.1.3" />
                    
Directory.Packages.props
<PackageReference Include="Plugin.Maui.PullToRefresh" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Plugin.Maui.PullToRefresh --version 0.1.3
                    
#r "nuget: Plugin.Maui.PullToRefresh, 0.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Plugin.Maui.PullToRefresh@0.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Plugin.Maui.PullToRefresh&version=0.1.3
                    
Install as a Cake Addin
#tool nuget:?package=Plugin.Maui.PullToRefresh&version=0.1.3
                    
Install as a Cake Tool

Plugin.Maui.PullToRefresh

A pull-to-refresh control for .NET MAUI with a native, elastic curved-strip animation that tracks the finger 1:1 — no spinner, no RefreshView wrapper, no janky snap-back. Pull down, a colored strip stretches from behind the content with a soft elastic curve, follows your finger's X position, and either snaps back or triggers a refresh depending on how far you pulled.

<img src="https://raw.githubusercontent.com/aminparsa18/Plugin.Maui.PullToRefresh/main/demo.gif" alt="Demo" width="300" />

Why this exists

MAUI's built-in RefreshView gives you a platform spinner. Every third-party pull-to-refresh control for MAUI (and most other frameworks) is a variation on the same spinner-and-arrow idea. This one isn't: it's a physically-damped curved strip, drawn natively (CAShapeLayer on iOS, Canvas/Path on Android — no GraphicsView), that grows with sqrt(pullDistance) so it feels elastic instead of linear, and whose peak follows the horizontal position of the finger doing the pulling.

It was built for a real app's timesheet/absence lists and is being extracted here as a standalone, reusable control.

How it works

  • Gesture detection happens at the platform layer, not through a MAUI PanGestureRecognizer: a UIPanGestureRecognizer on iOS, a raw IOnTouchListener on Android. Both look for "at the top of the scrollable content and moving down past a small slop" before treating the touch as a pull, so normal scrolling is never intercepted.
  • The scrollable content is auto-discovered. PullToRefreshViewHandler walks the native view tree under whatever you put inside PullToRefreshView looking for a UIScrollView (iOS) or a RecyclerView/NestedScrollView (Android) — so it works with CollectionView and ScrollView without any extra wiring.
  • Non-scrollable content works too. If nothing scrollable is found (say, a plain Grid or VerticalStackLayout), the handler falls back to treating the content as always "at rest" — a downward drag from anywhere in it starts a pull immediately, same as pulling from the top of a list.
  • The curve is damped, not linear: strip height is min(sqrt(pullDistance) * 5.5, maxHeight), which is what gives the "stretchy" feel instead of a strip that grows 1:1 with the finger.
  • The curve tip follows the finger's X position, clamped to the middle 60% of the width so the Bézier control points never invert.
  • Crossing the trigger threshold (72dp/pt) and releasing fires Command and the Refreshing event; releasing before that just animates the strip back to nothing.

Both platforms are independent implementations of the same math and gesture state machine — there's no shared native code, just a shared MAUI-facing PullToRefreshView control and PullToRefreshViewHandler partial class.

Project structure

src/Plugin.Maui.PullToRefresh/     the library (net10.0-android, net10.0-ios)
  PullToRefreshView.cs             the public ContentView subclass (Command, IsRefreshing, Refreshing)
  Handlers/
    PullToRefreshViewHandler.cs    shared partial: property mapper + ctor
  Platforms/
    iOS/Handlers/                  UIPanGestureRecognizer + CAShapeLayer curve
    Android/Handlers/              touch listener + Canvas/Path curve

example/Plugin.Maui.PullToRefresh.Example/   a runnable MAUI app demonstrating usage

Install

dotnet add package Plugin.Maui.PullToRefresh

Or via the NuGet package manager in Visual Studio / Rider. See the package on NuGet.org.

Setup

Register the handler in MauiProgram.cs:

using Plugin.Maui.PullToRefresh;

builder
    .UseMauiApp<App>()
    .UsePullToRefresh();

Usage

Wrap any content — a CollectionView, a ScrollView, or even non-scrollable content like a Grid — in PullToRefreshView and bind Command to whatever refreshes your data:

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:ptr="clr-namespace:Plugin.Maui.PullToRefresh;assembly=Plugin.Maui.PullToRefresh">

    <ptr:PullToRefreshView Command="{Binding RefreshCommand}">
        <CollectionView ItemsSource="{Binding Items}" />
    </ptr:PullToRefreshView>

</ContentPage>

PullToRefreshView isn't limited to scrollable content — wrap a plain Grid of static tiles or a dashboard layout the same way, and pulling down from anywhere in it triggers a refresh just as it would from the top of a list.

PullToRefreshView also exposes a Refreshing event as an alternative to binding Command, and an IsRefreshing bindable property reserved for host pages that want to track state explicitly (the handler itself doesn't read or write it — it just fires Command/Refreshing and lets you decide when the operation is done).

The strip color is bindable via StripColor:

<ptr:PullToRefreshView
    Command="{Binding RefreshCommand}"
    StripColor="{Binding AccentColor}">
    <CollectionView ItemsSource="{Binding Items}" />
</ptr:PullToRefreshView>

It defaults to the plugin's brand blue (#CC5B80C1). The color's own alpha channel sets the strip's opacity at full pull — it fades in with pull progress from there.

See example/Plugin.Maui.PullToRefresh.Example for a full working sample — its Feed tab wraps a CollectionView, and its Dashboard tab wraps a plain Grid of stat tiles to demonstrate the non-scrollable fallback.

Platform support

Platform Status
Android
iOS
Mac Catalyst ❌ not implemented yet
Windows ❌ not implemented yet

Building the example

cd example/Plugin.Maui.PullToRefresh.Example
dotnet build -f net10.0-android   # or -f net10.0-ios

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0-android36.0 is compatible.  net10.0-ios26.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 47 8/28/2026
0.1.2 91 8/24/2026
0.1.1 76 8/24/2026
0.1.0 87 8/24/2026