RadListView: Swipe to Execute

A popular way to enable actions on list items is the swipe-to-execute pattern. In this pattern, a list view item can be swiped left or right triggering a specific action. For example, many e-mail client apps implement this pattern to allow users to delete or archive e-mail messages. Here's an example of swipe-to-execute used with a reading list to delete or add an item to a favorites list:

Figure 1: The Swipe-to-Execute behavior in action:

RadListView: Swipe to execute RadListView: Swipe to execute

Enabling Swipe-to-Execute

For this tutorial we will use the already familiar setup from the RadListView: Getting started section.

To enable the swipe-to-execute functionality in RadListView you first need to set the itemSwipe property to true:

Example 1: Enabling swipe-to-execute on RadListView in XML:

<lv:RadListView 
    id="listView" 
    items="{{ dataItems }}" 

    row="1" 
    selectionBehavior="None" 
    itemSwipeProgressEnded="onSwipeCellFinished" 
    itemTap="onItemClick" 
    itemSwipeProgressStarted="onSwipeCellStarted" 
    itemSwipeProgressChanged="onCellSwiping" 
    itemSwipe="true">

You can also set this property via code

Second, you need to define the content that will be shown to the user when they swipe an item. This is done via the itemSwipeTemplate property as shown in the XML snippet below:

Example 2: Providing a template for the swipe-to-execute content via the itemSwipeTemplate property:

<lv:RadListView.itemSwipeTemplate>
    <GridLayout columns="auto, *, auto" backgroundColor="White">
        <StackLayout id="mark-view" col="0" style="background-color: blue;" tap="onLeftSwipeClick" padding="16">
            <Label text="mark" style="text-size: 20" verticalAlignment="center" horizontalAlignment="center"/>
        </StackLayout>
        <StackLayout id="delete-view" col="2" style="background-color: red;" tap="onRightSwipeClick" padding="16">
            <Label text="delete" style="text-size: 20" verticalAlignment="center" horizontalAlignment="center"/>
        </StackLayout>
    </GridLayout>
</lv:RadListView.itemSwipeTemplate>

The swipe template will be a GridLayout with three columns. The first and the last column each contain a StackLayout. These elements represent the actions revealed by swiping the item either left or right.

Implementing Swipe Actions

There are two popular ways to implement swipe actions. In the first approach you define a swipe threshold which makes the swiped item stick at a given position revealing the swipe actions. The user then taps on a given action to perform it.
In the second approach you simply listen for the itemSwipeProgressChangedEvent event and once the item being swiped reaches a given threshold, you execute the corresponding action when the item is released. The following examples demonstrate how these approaches are implemented.

Tap-to-Execute Actions

In this scenario the user needs to swipe an item to reveal the actions and then tap on one to execute it. Let's use the swipe template from the XML snippet above:

Example 3: Defining a swipe-to-execute template for the tap-to-execute scenario:

<lv:RadListView.itemSwipeTemplate>
    <GridLayout columns="auto, *, auto" backgroundColor="White">
        <StackLayout id="mark-view" col="0" style="background-color: blue;" tap="onLeftSwipeClick" padding="16">
            <Label text="mark" style="text-size: 20" verticalAlignment="center" horizontalAlignment="center"/>
        </StackLayout>
        <StackLayout id="delete-view" col="2" style="background-color: red;" tap="onRightSwipeClick" padding="16">
            <Label text="delete" style="text-size: 20" verticalAlignment="center" horizontalAlignment="center"/>
        </StackLayout>
    </GridLayout>
</lv:RadListView.itemSwipeTemplate>

The behavior that we have to implement is as follows:

  • user swipes an item
  • user releases the item and it sticks open revealing the actions
  • user taps on an action, an event handler is called and the swiped item is closed

Note that we have subscribed for the tap events of the StackLayout elements that are used to represent the actions.

For that purpose we need to handle the itemSwipeProgressStartedEvent event and define the swipe parameters that will determine where the swiped item will stick:

Example 4: Subscribing for the itemSwipeProgressStarted event:

<lv:RadListView 
    id="listView" 
    items="{{ dataItems }}" 

    row="1" 
    selectionBehavior="None" 
    itemTap="onItemClick" 
    itemSwipeProgressStarted="onSwipeCellStarted" 
    itemSwipe="true">

Note that on Android the itemSwipeProgressEndedEvent needs to be used in order to modify the threshold property for the swiped item to stick at that position.

Heres a sample code snippet demonstrating the implementation of the event handler:

Example 5: Handling the itemSwipeProgressStarted event and applying the swipe parameters:

export function onSwipeCellStarted(args: listViewModule.ListViewEventData) {
    var swipeLimits = args.data.swipeLimits;
    swipeLimits.threshold = 60 * utilsModule.layout.getDisplayDensity();
    swipeLimits.left = 80 * utilsModule.layout.getDisplayDensity();
    swipeLimits.right = 80 * utilsModule.layout.getDisplayDensity();
}
function onSwipeCellStarted(args) {
    var swipeLimits = args.data.swipeLimits;
    swipeLimits.threshold = 60 * utilsModule.layout.getDisplayDensity();
    swipeLimits.left = 80 * utilsModule.layout.getDisplayDensity();
    swipeLimits.right = 80 * utilsModule.layout.getDisplayDensity();
}
exports.onSwipeCellStarted = onSwipeCellStarted;

The args.swipeLimits object can be used to define the distance an item can be swiped either left or right, as well as the threshold that determines where the swiped item will stick open.

Note that we're multiplying the limits and offsets by the display density factor exposed by the utils module, part of NS. This is needed because iOS and Android devices all have different display densities and a plain limit like 150 will look completely different in different context. This multiplication takes care of addressing this difference.

So now we have a list with items that can be swiped open revealing an action. Using the tap event handlers we provided (as demonstrated in the XML snippet above), we now can understand when our users tap on a given action and execute it:

Example 6: Executing the swipe actions:

export function onLeftSwipeClick(args) {
    let listView:listViewModule.RadListView = <listViewModule.RadListView>(frameModule.topmost().currentPage.getViewById("listView"));
    listView.notifySwipeToExecuteFinished();
}

export function onRightSwipeClick(args) {
    let listView:listViewModule.RadListView = <listViewModule.RadListView>(frameModule.topmost().currentPage.getViewById("listView"));
    listView.notifySwipeToExecuteFinished();
}
function onLeftSwipeClick(args) {
    var listView = (frameModule.topmost().currentPage.getViewById("listView"));
    listView.notifySwipeToExecuteFinished();
}
exports.onLeftSwipeClick = onLeftSwipeClick;
function onRightSwipeClick(args) {
    var listView = (frameModule.topmost().currentPage.getViewById("listView"));
    listView.notifySwipeToExecuteFinished();
}
exports.onRightSwipeClick = onRightSwipeClick;

Note that we call the notifySwipeToExecuteFinished() method to make sure the swipe item is closed after the action is performed.

Swipe-to-Execute Actions

In this scenario the user needs to swipe and release the item to execute the corresponding action. The item must be swiped at a given distance over the provided threshold so that the associated action is executed. To implement this approach you need to handle the following RadListView events:

  • itemSwipeProgressStartedEvent - used to specify the swipe limits in a way that will make the item return to its original place when released
  • itemSwipeProgressEndedEvent - used to determine if a swipe action has to be executed (in case the item was swiped beyond a given threshold)
  • itemSwipeProgressChangedEvent - in case you want to notify the user that they have swiped the item beyond a specified threshold and the swipe action will be executed

So let's look at the handler of the itemSwipeProgressStartedEvent event:

Example 7: Providing a swipe distance beyond which the action is executed upon release:

export function onSwipeCellStarted(args: listViewModule.ListViewEventData) {
    var swipeLimits = args.data.swipeLimits;
    var swipeView = args['object'];
    var leftItem = swipeView.getViewById('mark-view');
    var rightItem = swipeView.getViewById('delete-view');
    swipeLimits.left = leftItem.getMeasuredWidth();
    swipeLimits.right = rightItem.getMeasuredWidth();
    swipeLimits.threshold = leftItem.getMeasuredWidth() / 2;
}
function onSwipeCellStarted(args) {
    var swipeLimits = args.data.swipeLimits;
    var swipeView = args['object'];
    var leftItem = swipeView.getViewById('mark-view');
    var rightItem = swipeView.getViewById('delete-view');
    swipeLimits.left = leftItem.getMeasuredWidth();
    swipeLimits.right = rightItem.getMeasuredWidth();
    swipeLimits.threshold = leftItem.getMeasuredWidth() / 2;
}
exports.onSwipeCellStarted = onSwipeCellStarted;

You can see that the width of the list control is used to specify the swiping limits and the threshold. Setting all these properties to getMeasuredWidth() of the list will make the item draggable along the whole list view width. It will also return to its original place once released as the threshold beyond which the item sticks will never be reached.

Now, by handling the itemSwipeProgressEndedEvent event we can decide if the corresponding action (right or left) will be executed:

Example 8: Triggering a swipe action depending on the swiped distance by using the itemSwipeProgressEnded event:

export function onSwipeCellFinished(args: listViewModule.ListViewEventData) {
}
function onSwipeCellFinished(args) {
}
exports.onSwipeCellFinished = onSwipeCellFinished;

In this particular case we have decided that the threshold beyond which an action is considered executed is 200 pixels.

By handling the itemSwipeProgressChangedEvent event you can track if the user reaches a given threshold (in this case 200 pixels) and notify them that if the item is released the action will be executed:

Example 9: Notifying the user that the swipe distance beyond which the action will be executed has been reached:

export function onCellSwiping(args: listViewModule.ListViewEventData) {
    var swipeLimits = args.data.swipeLimits;
    var currentItemView = args.object;
    var currentView;

    if (args.data.x > 200) {
        console.log("Notify perform left action");
    } else if (args.data.x < -200) {
        console.log("Notify perform right action");
    }
}
function onCellSwiping(args) {
    var swipeLimits = args.data.swipeLimits;
    var currentItemView = args.object;
    var currentView;
    if (args.data.x > 200) {
        console.log("Notify perform left action");
    }
    else if (args.data.x < -200) {
        console.log("Notify perform right action");
    }
}
exports.onCellSwiping = onCellSwiping;

All swipe events exposed by RadListView provide you with a ListViewEventData object which in turn carries a SwipeOffsets object through its data property. This object exposes the following properties:

  • x - the X offset of the item being released after swiping
  • y - the Y offset of the item being released after swiping
  • swipeLimits - an instance of the SwipeLimits containing the dimensional limits which restrict the movement of the swiped item. Its properties are as follows:
    • top - determines how far from the top edge can an item can be swiped
    • left - determines how far from the left edge can an item can be swiped
    • right - determines how far from the right edge can an item can be swiped
    • bottom - determines how far from the bottom edge can an item can be swiped
    • threshold - determines the amount if pixels that needs to be surpassed in any swiping direction so that the associated action is considered executed.

Additionally, you can acquire the View object being swiped by accessing the args.object property.

Animating Action Views While Swiping

Many mobile apps that implement the swipe-to-execute experience also animate the action views while the user is swiping. This can be easily achieved with RadListView in the itemSwipeProgressChangedEvent event. Assuming that we're using the same visual template as shown above, here is a sample implementation that accesses the two StackLayout elements within the swipe view and stretches them along with the swiped content:

Example 10: Animating the swipe-to-execute content while swiping:

export function onCellSwiping(args: listViewModule.ListViewEventData) {
    var swipeLimits = args.data.swipeLimits;
    var currentItemView = args.object;
    var currentView;
    if (args.data.x >= 0) {
        currentView = currentItemView.getViewById("mark-view");
        var dimensions = viewModule.View.measureChild(
            currentView.parent,
            currentView, 
            utilsModule.layout.makeMeasureSpec(args.data.x, utilsModule.layout.EXACTLY),
            utilsModule.layout.makeMeasureSpec(currentView.getMeasuredHeight(), utilsModule.layout.EXACTLY));
        viewModule.View.layoutChild(currentView.parent, currentView, 0, 0, dimensions.measuredWidth, dimensions.measuredHeight);
    } else {
        currentView = currentItemView.getViewById("delete-view");
        var dimensions = viewModule.View.measureChild(
            currentView.parent,
            currentView,
            utilsModule.layout.makeMeasureSpec(Math.abs(args.data.x), utilsModule.layout.EXACTLY),
            utilsModule.layout.makeMeasureSpec(currentView.getMeasuredHeight(), utilsModule.layout.EXACTLY));
        viewModule.View.layoutChild(currentView.parent, currentView, currentItemView.getMeasuredWidth() - dimensions.measuredWidth, 0, currentItemView.getMeasuredWidth(), dimensions.measuredHeight);
    }

    if (args.data.x > 200) {
        console.log("Notify perform left action");
    } else if (args.data.x < -200) {
        console.log("Notify perform right action");
    }
}
function onCellSwiping(args) {
    var swipeLimits = args.data.swipeLimits;
    var currentItemView = args.object;
    var currentView;
    if (args.data.x >= 0) {
        currentView = currentItemView.getViewById("mark-view");
        var dimensions = viewModule.View.measureChild(currentView.parent, currentView, utilsModule.layout.makeMeasureSpec(args.data.x, utilsModule.layout.EXACTLY), utilsModule.layout.makeMeasureSpec(currentView.getMeasuredHeight(), utilsModule.layout.EXACTLY));
        viewModule.View.layoutChild(currentView.parent, currentView, 0, 0, dimensions.measuredWidth, dimensions.measuredHeight);
    }
    else {
        currentView = currentItemView.getViewById("delete-view");
        var dimensions = viewModule.View.measureChild(currentView.parent, currentView, utilsModule.layout.makeMeasureSpec(Math.abs(args.data.x), utilsModule.layout.EXACTLY), utilsModule.layout.makeMeasureSpec(currentView.getMeasuredHeight(), utilsModule.layout.EXACTLY));
        viewModule.View.layoutChild(currentView.parent, currentView, currentItemView.getMeasuredWidth() - dimensions.measuredWidth, 0, currentItemView.getMeasuredWidth(), dimensions.measuredHeight);
    }
    if (args.data.x > 200) {
        console.log("Notify perform left action");
    }
    else if (args.data.x < -200) {
        console.log("Notify perform right action");
    }
}
exports.onCellSwiping = onCellSwiping;

The code that changes the size of the views consists of measuring the view with the new size and calling the layout mechanism after that.

References

Want to see this scenario in action?
Check our SDK examples repo on GitHub. You will find this and many other practical examples with NativeScript UI.

Related articles you might find useful: