The BlackBerry Bramble

BlackBerry 10 Developer Blog

Pull to Refresh

If you’ve been using any modern smartphone for any length of time I’m sure you’ve seen “Pull to Refresh” (PTF). This has quickly become the preferred and expected way for a user to force the content they are looking at to refresh from the Internet. Simply drag the screen downwards with a finger, then release it and let it snap back up, and anything you were looking at on the screen is updated with the latest content. I believe this paradigm originated on Android, but wherever it came from, it’s here on BlackBerry 10 in a big way. There is no built-in support for PTF in BlackBerry 10 or Cascades, so if you want it in your app you’ll need to code it yourself.

The Objective

I integrated PTF into my multiFEED application, and I’ll use it as the template for this tutorial. When a user drags the front page ListView down a bit they see this:

IMG_00000312-crop

…and if they pull down a bit further the arrow swivels upward and the text changes to this:

IMG_00000313-crop

At this point the refresh trigger is “cocked” and if the user releases it then the refresh will happen. If the user changes their mind and pushes the ListView back up the arrow will swivel down again and the refresh won’t happen even if they now release it suddenly. Note that the arrow and message text are horizontally centered.

When the refresh is triggered, if it takes a while to execute we want to hide the arrow and message and also prevent re-cocking till the refresh completes.

ListView

The basic prerequisite to implement PTF is a ListView to attach it to. The vertical scrolling provided by ListView is a natural place to insert PTF logic, and luckily it supplies a convenient hook for it. First we need a “visual” to display above the list items when the list is dragged down past the top. We’ll create this in a file called RefreshHeader.qml:

This QML will recreate the first image above when the ListView is partially dragged down, but won’t change in any way when the user drags all the way down. We’ll add that later. Now we will tell the ListView to use our new header:

The leadingVisualSnapThreshold setting tells the ListView that if the user drags down past the top more than twice the height of the header image and text combined that it should leave the header exposed unless the user explicitly pushes it back up.

Animating the Header

At this point dragging your ListView down past the top will display the refresh header, but nothing will happen if you drag it further down or release it suddenly. Let’s make the arrow swivel and text change when the user drags past a certain point:

Note the two new properties, refreshing and refreshCocked. The former will be set elsewhere when a refresh is in progress, and the latter is set by the LayoutUpdateHandler when the user drags down past pullThreshold. The purpose of refreshCocked will become clear later.

The attached LayoutUpdateHandler watches for changes to the way the ListView is being displayed and signals with layoutFrameChanged when the scroll position changes. In onLayoutFrameChanged the value of layoutFrame.y will be zero if the list is right at the top, negative if the user has scrolled the list upward, and positive if they have dragged it down past the top. If we are not already in the middle of a refresh, and the refresh trigger is not already cocked then lines 12 and 13 rotate the arrow image upward and change the text in the refresh header if the user has dragged down past pullThreshold. Cascades gives us a freebie by animating the arrow rotation smoothly instead of just snapping it to the new orientation. Alternately, if the refresh is already cocked and the user pushed the list back up below pullThreshold, the the arrow is rotated back downward and the text is reverted to its default. Lines 14 and 18 cock or uncock the refresh trigger.

Handling User Touch

Our refresh header can’t respond to user touch events directly, it must be notified about them by the parent ListView. We need to create a touch handler function on our header which the ListView can call:

Line 9 prevents unnecessary processing while a refresh in on progress. Line 13 watches for the user to lift their finger off the list, and when they do line 15 checks to see if the refresh trigger is cocked. If so then line 21 hides the header for the duration of the refresh. Even when the header is hidden, if the user pulls the list down far enough the ListView won’t pop right back up to the top when released, same as when it is visible, so we have to collapse the header to zero height too with line 24. Line 27 emits the signal to do the refresh. Line 30 shows the header again, but for reasons I’ll explain later we don’t want to restore it to its original height here too, but instead rely on line 11 to always do that when the user touches the list again.

Now we can modify our ListView to use what we have so far:

Asynchronous Refresh

If your refresh code executes very quickly then you don’t need to do anything else, pull-to-refresh will work fine with the code already presented, but in most situations you will need to refresh your data from the Internet and response time is likely to be too long to just hang and wait for. In this case, you are better off doing the refresh asynchronously, that is, initiate the refresh and then forget about it until it reports that it is finished. Lets add this capability to RefreshHeader.qml:

In this version we have added a new asynchronous property so we can turn the feature on or off. There is also a new function call refreshDone() that will act a a slot to catch the end of the refresh. Finally, on line 30 we check for asynchronous mode and if it is switched on we don’t restore the refresh header visibility or turn off the refreshing flag since we’ll do that when the refresh reports that it is finished.

The refreshDone() function just returns the refresh header to starting conditions and turns off the refreshing flag. To use asynchronous mode with our ListView just add a few lines:

RefreshManager.refresh() is just a placeholder for any other control and function that can perform the refresh asynchronously then trigger the refreshDone() function on the ListView when it has finished. The refreshDone() function just passes the signal on down to the same-named event on the RefreshHeader.

Refreshing With a C++ Function

In my multiFEED application the refresh functionality is all handled by a Q_INVOKABLE C++ function on the main application class. For this to work I have to connect the “refresh finished” signal from the application to the refreshDone() slot on the ListView on the fly. My ListView code looks like this:

 Orientation Handling

We are almost done, but there is one thing that doesn’t quite work right. On a full touch device with landscape mode the rotating arrow and refresh header text get skewed off-center when the device orientation is changed because the width of the header isn’t automatically rescaled. For this we need to attach an OrientationHandler to the Container in RefreshHeader.qml:

The preferredWidth property on the Container uses the OrientationHandler to determine how wide it should be. Whenever you assign the result of an expression to a property like this Cascades connects a slot to the corresponding signal behind the scenes. In this case there is an invisible handler for orientationHandler.onOrientationChanged, so every time the user reorients the device the preferred width is reset. Note that DisplayInfo.height and DisplayInfo.width require a little explanation. There is no built-in way to determine the screen dimensions from a BlackBerry10 application so we have to add a little code to our main application cpp file to make this information available at runtime. This code must be inserted after main.qml has been loaded, but before it is set as the root scene:

Putting It All Together

Combining everything we get:

…and to use it our ListView looks something like this:

 Conclusion

The RefreshHeader we just built can be used with any ListView with just a few extra lines of QML and JavaScript. Now every ListView in all your applications can have “pull-to-refresh” functionality with very little effort.

Leave a Reply