in Apps

Building Pull to refresh

twitter

One of the most common and desired touch interactions in modern apps is pull to refresh a list of items. We see it phone and tablet apps as the preferred, touch-first way to implement refresh for list. Rather than tapping an awkward button, just keep scrolling. Bam. I’ve seen good and bad implementations of the interaction across apps on all platforms, and Twitter’s pull to refresh on Windows, Windows Phone and iOS is one of the best.

In the last post we looked at a basic framework for designing custom touch interactions. Based on what we learned there, we’ll specifically break down the mechanics of the pull to refresh interaction, and talk about how we want it to work. Finally, we’ll walk through step-by-step how to build the pull to refresh touch interaction for a Windows 8 app written entirely in HTML5. Also, I’ve posted the full code up on GitHub if you want to skip to the code!

Pull to refresh mechanics

Before we dig into the architecture and code for how it will be implemented let’s outline some mechanics for how we want it to work using the custom touch interactions scorecard from the last post.

Down feedback

We want to make sure that there is immediate touch down feedback, so when you start scrolling the view you can start to see pull to refresh working. Since the pull to refresh interaction piggy-backs off of the scrolling interaction, we get this down feedback for free. Clutch.

Continuous feedback

We want there to be feedback the entire time during the interaction. One piece of continuous feedback that we have is the fact that you are pulling the pannable surface that’s moving under your finger, and slows down when you hit the threshold. We want this to be super responsive, so we will also rotate an arrow as you move your finger closer or farther away from the threshold.

No timers

We’re using a design where no no matter how fast or slow you pull to refresh it performs the same way. You can watch the arrow turn slowly, or just swipe fast and it always works the same way. Sweet, because building timers is usually a pain and buggy.

Threshold feedback

We show feedback when you reach the different threshold states. We indicate that something is going to happen or not happen when your lift your finger:

  1. If you release, it will not refresh
  2. If you release, it will refresh

Reversible

When you drag to the refresh threshold, we want to make it so you don’t have to refresh if you don’t want to. This means you should be able to pan back up before you lift your finger if you don’t want the refresh to happen.

Up feedback

We want to make sure that we convey that something happened or didn’t happen when you lift your finger. If you dragged past the threshold, then lift your finger, then we want to show that it’s loading, if you don’t, we want to animate smoothly back to the list.

Fast and fluid

Because this is piggy-backing off of the scrolling action, we have to be super careful. Nothing that we do to make the pull to refresh work can interfere with basic scrolling of the list. Also, we want this to be stick to your finger fast, and buttery smooth so we will need to avoid UI thread-dependent animations were possible.

Options investigated

From the beginning I knew that I wanted to build this in HTML. I have never seen a good implementation of pull to refresh in HTML/JS/CSS, so I knew that this would be a challenge worth pursing!

Scroll events with element.scrollTop

I started off by playing with the basic HTML scroll event and element.scrollTop. Scroll events worked to rotate the arrow in real-time, but no scroll events are sent when the user is in the scroll bounceback area above the content (i.e. scrollTop < 0).

-ms-pointer events

My next approach was to try to add –ms-pointer manipulations dynamically based upon scroll position. I wasn’t alone either, CobraTap on the MSDN forums asked the same question. Although, the -ms-pointer events are promising as they offer many touch and direct manipulation capabilities, they aren’t the right option when building something that interacts with scrolling or zooming behavior.
By default, on a scrollable surface you don’t get any pointer events during a pan or a zoom. You can use the -ms-touch-action CSS property to allow for some -ms-pointer events, but that prevents scrolling. Basically, it’s either or: you can get native panning or you get custom behavior. There’s no way I could completely re-implement the scrolling list behavior in JavaScript with -ms-pointer events (UI thread independent, panning indicator, bounce back behavior, etc.) so this eliminated -ms-pointer as a viable option.

The solution: Chained scrollers and snap points

Starting with IE10 and Windows 8, we introduced some very interesting and powerful behavior with chained scrollers and snap points. These are native behaviors baked into the platform and CSS, so they don’t require any custom JavaScript to enable. This allows us to take advantage of these built-in behaviors for the most performance-intensive interactions.

Chained scrolling allows you to put one scrollable region inside of another scrollable region. When you pan to the end of the inner scrollable region it starts panning the outer scrollable region, all automatically.

Snap points allow you to define pixel or percentage distances in your scrollable region, to snap to when panning with touch. Think of a photos app, where you are flipping through the photos like pages. You can drag between the photos, but it always sticks to one photo or another.

Step-by-step implementation

With chained scrolling, snap points and some basic scroll events, we’re able to implement the full pull to refresh behavior. Let’s walk through step-by-step how to implement this from start to finish!

Step 1: Setup the nested scrolling layout

The first step is to setup the Russian-doll of scrollers. We have our innerScroller which is where all of our list items live, this could be a manual collection of HTML items, or a ListView or Repeater control. What is in the innerScroller isn’t important, just that it’s the place with all of the items. We also have our pullBox; this is the box that has the instruction text, rotating arrow and progress ring. Finally, we have our outerScroller which has both the pullBox and the innerScroller inside of it.

<div class="outerScroller">
    <div class="pullBox">
        <progress class="pullProgress win-ring"></progress>
        <div class="pullArrow"></div>
        <div class="pullLabel">Pull down to refresh</div>
    </div>
    <div class="innerScroller">
    </div>
 </div>

 

Step 2: Add your mandatory snap point

The trick of getting this to work, is to creating one mandatory snap point on the outerScroller that is just past where your pullBox information is. This forces the outerScroller at rest state to be stuck just past the pullbox, but the innerScroller will still be allowed to scroll.

.outerScroller {
    overflow-y: auto;
    -ms-scroll-snap-y: mandatory snapList(80px);
    -ms-overflow-style: none;
}

 

Step 3: Setup your viewport and scroll position

Since we set a mandatory snap point past the pullBox, we’d hope that it would default there, unfortunately snap points only seem to be evaluated once panning has started. To solve this, we simply set our initial scroll position to be the height of the pullBox. In our case, the height of the innerScroller is the full height of the screen.

// Set the initial scroll past the pull box
document.querySelector(".outerScroller").scrollTop = _pullBoxHeight;

 

Step 4: Rotate the arrow

At this point we have some of the basic functionality built. You land on your list, and you pull down and you can see the pull to refresh label. This is a huge step! The next step is to rotate the arrow, and change the label do give feedback as you perform the pull to refresh interaction.

Implementation-wise this is fairly straightforward, because the chained scrollers are handling all of the physics and bounceback. So, the snap point tries to keep outerScroller scrolled the innerScroller content, instead of on pullbox. So all we have to do is look at the scroll position of the outerScroller. Anytime, it’s position is less than the height of the pullBox we know we are doing the pull to refresh interaction, so we can rotate the arrow.

outerScroller.addEventListener("scroll", function(e) { 
    var rotationAngle = ((_pullBoxHeight - outerScroller.scrollTop) /  _pullBoxHeight) * 360; 
    pullArrow.style.transform = "rotate(" + rotationAngle + "deg)"
});

We rotate the arrow using a simple CSS rotate transform. CSS transforms are hardware accelerated and the scroll event doesn’t affect the scrolling performance so we can be confident it will be fast and fluid.

Step 5: Show feedback at the threshold

Like rotating the arrow, during the scroll we can use the scroll position of the outerScroller to determine if we hit the threshold. If the outerScroller hits a scroll position of zero, we know that that they made it!

// Change the label once you pull to the top
if (outerScroller.scrollTop === 0) {
    pullLabel.innerText = "Release to refresh";
} else {
    pullLabel.innerText = "Pull to refresh";
}

 

Step 6: Refresh the list

Our next step is to refresh the list when you lift past the threshold. Detecting when to refresh isn’t as trivial to implement as rotating the arrow with the scrolling events. As we discussed earlier, there aren’t any pointer touch events during panning because this interaction has been offloaded from the UI thread to the graphics and touch hardware.

What we do have at our disposal is a lesser known event, MSManipulationStateChanged. It fires whenever you start or finish panning or zooming an element. We can use this along with the currentState property, which tells us what the current panning state is. We can check when the currentState is in the MS_MANIPULATION_STATE_INERTIA state, which means that content is still moving, but contact with the surface has ended. Our single mandatory snap point is what keeps keeps the content moving. Then we just check that the outerScroller scroll position is at the top and we know to refresh.

// Listen for panning state change events 
outerScroller.addEventListener("MSManipulationStateChanged", function(e) {
    // Check to see if they lifted while pulled to the top 
    if (e.currentState == MS_MANIPULATION_STATE_INERTIA && 
        outerScroller.scrollTop === 0) { 
        refreshItemsAsync(); 
    } 
});

Step 7: Show loading progress

The finishing touch is showing feedback and progress during the refresh. This is particularly important if your refresh modifies the list that you’re scrolling and can take up to a few seconds.

There are plenty of different ways to show progress, but my favorite way to show progress after pull to refresh, is to hold the scroll position with a progress ring, and snap back after refresh. Since this is playing with the bounce back behavior, coding this up is a little tricky. Other designs where you overlay a progress bar are simpler to implement, but we’ll start with the hardest.

To show progress, first we’re going to want to show a progress ring. We do that simply with a .loading CSS class.

.loading .pullProgress {
    transition: opacity ease 0.3s;
    opacity: 1;
}

The next step is to prevent the view from snapping back. We’re going to use the same .loading class to override the snap point to a “proximity” snap point instead of a “manditory” one.

 

.outerScroller.loading {
    -ms-scroll-snap-y: proximity snapList(80px);
}

Then we just add that class in JavaScript before we refresh the items. We also want to update the loading text and disable the scroller so you can’t refresh the view again while it’s refreshing.

// Change the loading state and prevent panning 
WinJS.Utilities.addClass(_outerScroller, "loading"); 
pullLabel.innerText = "Loading...";
outerScroller.disabled = true; refreshItemsAsync();

Next we return the view to the rest state after loading is complete just by removing the .loading class and by re-enabling the scroller. We also need to scroll back to the rest state after it’s loaded. We’re going to use the msZoomTo function to smoothly animate it back to position.

refreshItemsAsync().then(function () { 
   // After the refresh, return to the default state 
   WinJS.Utilities.removeClass(outerScroller, "loading"); 
    outerScroller.disabled = false; 
    // Scroll back to the top of the list, 8.1 only 
    outerScroller.msZoomTo({ 
        contentX: 0, 
        contentY: _pullBoxHeight, 
        viewporX: 0, 
        viewportY: 0}); 
});

Note: MsZoomTo is available for Windows 8.1 only. If you are building for the browser, or earlier versions of Windows you can either set outerScroller.scrollTop without animating it, or animate in JavaScript.

Wrapping it up

To recap, we hooked up a chained scroller with a mandatory snap point to get the scrolling mechanics. We use scroll position and events to animate the arrow and show when we hit the threshold. Finally, we use MSManipulationStateChanged to detect when the user lifts their finger during the pan, and show loading progress. The end result is fast, fluid and super efficient.

Get the code

All the code for the project is available on GitHub, so go ahead and grab that and let me know if you run into any trouble.

Cheers,
David

pulltorefresh

Write a Comment

Comment

  1. I wanted to thank you for the article on “pull to refresh”. I wanted to know about how I could use the code to be able to “pull to close” a window instead of to refresh…. Any help would be much appreciated…

    Thanks.

    David

    • Cool! Glad it was helpful. Explain the scenario, and the interaction for how pull-to-close would work. I’m not able to visualize it.

  2. Hi David,
    How can I archive the same functionality in Windows Phone 8 using C# XAML with a LongListSelector.

    • It should be possible, I just haven’t done the work to build it out yet. @timheuer was thinking about adding this to his Calisto control toolkit. ping him on twitter.

  3. Hi David,
    You don’t have a license on your github repo. What license is this code made available under? I want to see if it’s suitable for my project which uses GPLv2.
    Thanks

  4. Very Useful. But I’m developing WP8.1 app using c# , so it would be greatful if someone helped in achieving this by using c#.