Over a million developers have joined DZone.

Swipe on mobile items in Flex

· Mobile Zone
I’ve been playing with the mobile item renderers this week for a customer. I wanted to reproduce a classic mobile gesture: a swipe from the right to the left on a list item to display custom actions. Extending mobile item renderers is very fun with Flex 4.5. That said, to exchange data between the list and your renderers, you need to implement an event-based micro-architecture. With Flex 4.5 and the new “view navigator” paradigm, it’s very easy. I opted for a global event dispatcher. First, let me show you the final result on my Android devices and on my iOS devices.



Here is a picture that summarizes the architecture of this Flex 4.5 application:



<ViewNavigatorApplication>

My main application uses the new ViewNavigatorApplication architecture. It automates the transitions between the view,persisting your data, navigation, actionBar components, etc… More information here: http://www.adobe.com/devnet/flex/articles/mobile-development-flex-flashbuilder.html

Instead of pushing the first view automatically, I launch a HTTPRequest to get the list of employees (an XML file stored on my server). By the way, I’m using this URL: http://www.riagora.com/sfdc/employees.xml, feel free to use it for your own demos. When I get a ResultEvent, I push the first view passing the event.result to feed the data object.

protected function employeeService_resultHandler(event:ResultEvent):void
			{
				navigator.pushView(views.sfdcempHomeView, event.result as ArrayCollection);
			}

I also declared an EventDispatcher event object. This will become my events hub for the whole application. I need it to dispatch events between the item renderers and the list of my first view.
dispatcher = new EventDispatcher;

<Views> and events

My first view displays a list of employees. The dataProvider for my <s:List> is just binded to the {data} object as it contains the ArrayCollection pushed by my main application. The interesting code is within the custom item renderer for my list.

Here the source code of my custom item renderer:


<?xml version="1.0" encoding="utf-8"?>
<s:IconItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"  creationComplete="iconitemrenderer1_creationCompleteHandler(event)"
					xmlns:s="library://ns.adobe.com/flex/spark"   height="100"  labelField="firstName" messageField="title" iconField="picture" iconWidth="64" iconHeight="64" xmlns:views="views.*" >
	<fx:Script>
		<![CDATA[
			import events.ScrollingEvent;
 
			import mx.events.EffectEvent;
			import mx.events.FlexEvent;
			import mx.events.MoveEvent;
 
			import spark.components.Button;
			import spark.components.List;
			import spark.events.ListEvent;
 
			private var FLAGSTATE:int = 2;
 
			protected function iconitemrenderer1_creationCompleteHandler(event:FlexEvent):void
			{
 
				Multitouch.inputMode = MultitouchInputMode.GESTURE;
				this.addEventListener(TransformGestureEvent.GESTURE_SWIPE, onSwipe);
 
				this.parentApplication.dispatcher.addEventListener(ScrollingEvent.SCROLLING_STARTED, onScrollAgain);
				wipeEffect.addEventListener(EffectEvent.EFFECT_END, onEffectEnd);
				wipeEffectOut.addEventListener(EffectEvent.EFFECT_END, onEffetEndOut);
			}
 
			protected function onSwipe(event:TransformGestureEvent):void
			{
 
				var myScrollEvent:ScrollingEvent = new ScrollingEvent(ScrollingEvent.SCROLLING_STARTED);
 
				if((event.offsetX == -1) && (FLAGSTATE == 2)){
 
					this.addChild(actBar);
 
					actBar.width = this.width;
					actBar.height = this.height;
					actBar.visible = true;
					actBar.theData = data;
					actBar.addEventListener(FlexEvent.CREATION_COMPLETE, onItemComplete);
					FLAGSTATE = 0;
 
					this.parentApplication.dispatcher.dispatchEvent(myScrollEvent);
				}else{
					if ((event.offsetX == -1)){
						wipeEffect.play();
						this.parentApplication.dispatcher.dispatchEvent(myScrollEvent);
					}
				}
			}
 
			protected function onItemComplete(event:FlexEvent):void
			{
				wipeEffect.play();
			}
 
			private function onScrollAgain(event:ScrollingEvent):void
			{
 
				trace("scrolling");
				if (FLAGSTATE == 1){
					wipeEffectOut.play();
				}
 
			}			
 
			protected function onEffectEnd(event:EffectEvent):void
			{
				FLAGSTATE = 1;
			}
 
			protected function onEffetEndOut(event:EffectEvent):void
			{
				FLAGSTATE = 0;
			}
 
		]]>
	</fx:Script>
	<fx:Declarations>
		<s:HGroup id="actBar2">
			<s:Button id="btn1" label="action1"/>
			<s:Button label="action2"/>
		</s:HGroup>
		<s:Parallel id="wipeEffect" target="{actBar}">
			<!--<s:Fade duration="800" alphaFrom="0.7" alphaTo="1" />-->
			<s:Move duration="300" xFrom="{this.width}" xTo="0"/>
		</s:Parallel>
		<s:Parallel id="wipeEffectOut" target="{actBar}">
			<!--<s:Fade duration="800" alphaFrom="0.7" alphaTo="1" />-->
			<s:Move duration="150" xTo="{this.width}" xFrom="0"/>
		</s:Parallel>
		<views:ActionBG id="actBar" width="{this.width}" height="{this.height}"/>
	</fx:Declarations>
</s:IconItemRenderer>

First, I add event listeners to catch “swipe” gestures events. The goal is to display a ‘toolbar’ with four potential actions (four buttons). If the user swipes on the item, I check the gesture direction (if event.offsetX == -1, then the swipe was the right to the left) and check if the toolbar is already displayed. If not, I instantiate a new ActionBG object (a custom component) and display it over the item renderer with a Move effect. For usability reasons, I had to pay attention to the duration of these Move effects and use a flag variable (FLAGSTATE) to describe the current state of my toolbar.

My item renderer code just contains functions to control the visual states. My actionBG custom component (the toolbar) displays four action buttons. Let’s look at the code of this component.

<?xml version="1.0" encoding="utf-8"?>
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
		 xmlns:s="library://ns.adobe.com/flex/spark" width="400" height="150">
 
	<fx:Script>
		<![CDATA[
			import events.ScrollingEvent;
 
			import mx.events.FlexEvent;
 
			import valueObjects.Employee;
 
			public var theData:Object;
 
			protected function button1_clickHandler(event:MouseEvent):void
			{
				// DISPLAY the full name of the employee
				myTI.text = theData.id + ": " + theData.firstName + " " + theData.lastName;
			}
 
			protected function button2_clickHandler(event:MouseEvent):void
			{
				// TAP TO PUSH A NEW VIEW WITH Employee's details
				var tapEvent:ScrollingEvent = new ScrollingEvent(ScrollingEvent.TAP_ACTION);
				tapEvent.userObj = theData as Employee;
				this.parentApplication.dispatcher.dispatchEvent(tapEvent);
			}
 
			protected function button3_clickHandler(event:MouseEvent):void
			{
				// Remove the employee from the list
				var removeEvent:ScrollingEvent = new ScrollingEvent(ScrollingEvent.DELETE_ACTION);
				removeEvent.userId = int(theData.id);
				this.parentApplication.dispatcher.dispatchEvent(removeEvent);
			}
 
		]]>
	</fx:Script>
 
	<fx:Declarations>
		<!-- Place non-visual elements (e.g., services, value objects) here -->
		<!--<s:DropShadowFilter inner="true" blurY="120" alpha="0.2" angle="90" distance="20" id="innerS"/>	-->
	</fx:Declarations>
	<s:BitmapImage width="100%" height="100%" source="@Embed('assets/pattern.png')" fillMode="repeat" />
	<s:HGroup verticalAlign="middle" horizontalAlign="center" width="100%" height="100%">
		<s:Button icon="@Embed('assets/icon1.png')" click="button1_clickHandler(event)"/>
		<s:Button icon="@Embed('assets/icon3.png')" click="button2_clickHandler(event)"/>
		<s:Button icon="@Embed('assets/icon2.png')"/>
		<s:Button icon="@Embed('assets/icon4.png')" click="button3_clickHandler(event)"/>
 
	</s:HGroup>
	<s:Label id="myTI" text="" bottom="3" horizontalCenter="0" backgroundColor="0x444444" color="0xFFFFFF"/>
</s:Group>

This component has a public variable named “theData”. When I instantiate the component, I can pass some data relative to the employee. If the user clicks on the second button, the application must push a new view to display some details about the selected employee. To inform my view, I use the dispatcher object created in the main application document. To reach it, I’m simply using the parentApplication property. I also created a custom event named ScrollingEvent. I use this technique for the second button and also the last one, which is used to delete an item from the list (look at the button2 and button3 click handler functions in the code).

In my view, I just have to reuse the Dispatcher object and listen for these custom events.

protected function myList_creationCompleteHandler(event:FlexEvent):void
			{
				// TODO Auto-generated method stub
 
				this.parentApplication.dispatcher.addEventListener(ScrollingEvent.TAP_ACTION, onTapItem);
				this.parentApplication.dispatcher.addEventListener(ScrollingEvent.DELETE_ACTION, onDeleteAction);
			}
 
			private function onTapItem(event:ScrollingEvent):void
			{
				// TODO Auto Generated method stub
				this.parentApplication.dispatcher.removeEventListener(ScrollingEvent.TAP_ACTION, onTapItem);
				this.parentApplication.dispatcher.removeEventListener(ScrollingEvent.DELETE_ACTION, onDeleteAction);
				navigator.pushView(views.EmployeeDetailsView, event.userObj);
			}
 
			private function getItemIndexByProperty(array:ArrayCollection, property:String, value:String):Number
			{
 
				for (var i:Number = 0; i < array.length; i++)
				{
					var obj:Object = Object(array[i])
					if (obj[property] == value)
						return i;
				}
				return -1;
			}
 
			private function onDeleteAction(event:ScrollingEvent):void
			{
				// TODO Auto Generated method stub
 
				var userIndex:int = getItemIndexByProperty(myEmployees, "id", String(event.userId));
				myEmployees.removeItemAt(userIndex);
			}

ScrollingEvent

Last usability issue: I need to remove the toolbar on an item renderer when the user scrolls the list again, or if he swipes another item renderer. To do so, I used a new event available on the List component: touchInteraction events. Again, I use my global dispatcher to inform the item renderer that it should hide the toolbar.

In my view:

protected function myList_touchInteractionStartHandler(event:TouchInteractionEvent):void
			{
				var myScrollEvent:ScrollingEvent = new ScrollingEvent(ScrollingEvent.SCROLLING_STARTED);
				this.parentApplication.dispatcher.dispatchEvent(myScrollEvent);
 
			}
 
(...)
 
<s:List width="100%"   useVirtualLayout="false" dataProvider="{myEmployees}" creationComplete="myList_creationCompleteHandler(event)" itemRenderer="views.MyIR" touchInteractionStart="myList_touchInteractionStartHandler(event)"    height="100%" id="myList"/>

In my item renderer:

this.parentApplication.dispatcher.addEventListener(ScrollingEvent.SCROLLING_STARTED, onScrollAgain);
 
(...)
 
private function onScrollAgain(event:ScrollingEvent):void
{
 
	trace("scrolling");
	if (FLAGSTATE == 1){
		wipeEffectOut.play();
	}
 
}

Source code

The Flash Builder project is available here: http://www.riagora.com/sfdc/sfdcemp.fxp.zip

In the video, I’m using the “June update” of Flex and AIR for mobile applications. That’s why it performs very well.

Topics:

Published at DZone with permission of Michael Chaize, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}