Adding physics to your gestures

Adding gesture support for your Flex apps is very easy. But it takes a little extra code to take the experience to the next level by adding a layer of physics to it. The example below is built for touch screens but will work with a mouse just as well. If you want to view it on your mobile phone then try this link: http://polygeek.com/flex/2846_GesturePhysics/_2846_GesturePhysics.html

view source

The code in this example is an extension of work done by James Ward in his post Flex 4 List Scrolling on Android with Flash Player 10.1

How it works
Start by listening for applicationComplete and adding listeners for the following gestures: TOUCH_BEGIN, TOUCH_MOVIE, TOUCH_END.

public function appInit():void {
	_movieService = new MovieService_2846();

	_tic = new Timer( 33 );
	_tic.addEventListener( TimerEvent.TIMER, onTic );

	if ( Multitouch.supportsTouchEvents ) {
		// supports touch events
		Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT;
		FlexGlobals.topLevelApplication.stage.addEventListener(
			TouchEvent.TOUCH_BEGIN, onTouchBegin );
	} else {
		// Does NOT support touch events
		FlexGlobals.topLevelApplication.stage.addEventListener(
			MouseEvent.MOUSE_DOWN, onMouseDown );
		FlexGlobals.topLevelApplication.stage.addEventListener(
			MouseEvent.MOUSE_UP, onMouseUp );
	}
}

Here are the event handlers. The _isTouching Boolean prevents the scrolling from behaving oddly when the user tries to scroll past the top or bottom of the list.

private function onTouchBegin( e:TouchEvent ):void {
	// A touch event has begun so add listeners for TOUCH_MOVE and TOUCH_END.
	FlexGlobals.topLevelApplication.stage.addEventListener(
		TouchEvent.TOUCH_MOVE, onTouchMove );
	FlexGlobals.topLevelApplication.stage.addEventListener(
		TouchEvent.TOUCH_END, onTouchEnd );

	// We can stop listening for TOUCH_BEGIN until this current TOUCH_BEGIN ends.
	FlexGlobals.topLevelApplication.stage.removeEventListener(
		TouchEvent.TOUCH_BEGIN, onTouchBegin );

	// _isTouching prevents the scrolling from behaving oddly when
	// the user tries to scroll past the min/max.
	_isTouching = true;
	_prevY = e.stageY;
	_tic.stop(); // Just to make sure the Timer isn't running.
}

I’m offloading the code to determine how to move the list to a separate function so that it can be used by both touch and mouse events.

private function onTouchMove( e:TouchEvent ):void {
	moving( e.stageY );
}

moving();
tempDelta measures how far the user has moved their finger since the last time the onTouchMove event fired. I’m multipling by a small factor, 1.2 in this example, to help the user scroll. This will make the list scroll just a little faster than the user swipes. This is also beneficial in the case that you are dealing with a list of Buttons. If in the process of swiping the user presses and releases on the same button it may register a button press event – which you probably don’t want. If the list moves faster than the swipe then the chances of that happening are reduced.

Instead of setting _deltaY directly I’m going through an intermediate – _tempDelta. That is to make sure we don’t set _deltaY to 0. Sometimes that happens with the touch events registering twice just as the user finishes a swipe. It also has the added benefit of letting the user swipe once and then just place their finger over the nav to keep the nav scrolling automatically.

private function moving( stageY:int ):void {
	// Multiply by a small factor - 1.2 in this example - to help the user scroll.
	// This will make the app scroll just a little faster than they swipe.
	var tempDelta:Number = Math.round( ( _prevY - stageY ) ) * 1.2;

	// The 'tempDelta' makes sure that we don't set _deltaY to 0. Sometimes that
	// happens with the touch events registering twice just as the user finishes
	// a swipe. It also has the added benefit of letting the user swipe once and then
	// just place their finger over the nav to keep the nav scrolling automatically.
	_deltaY = ( tempDelta != 0 ) ? tempDelta : _deltaY;

	// Under some circumstances just barely touching, or tapping with two fingers,
	// can create a very large _deltaY. This makes sure that _deltaY doesn't exceed
	// a max value of ± 20 - which is arbitrary.
	_deltaY = ( _deltaY > 20 ) ? 20 : _deltaY;
	_deltaY = ( _deltaY < -20 ) ? -20 : _deltaY;

	calculateScrollPosition();
	_prevY = stageY;
}

Now that the user has ended a swipe start the timer so we can let the nav coast to a stop.

private function onTouchEnd( e:TouchEvent ):void {
	// The touch event has ended so now start listening for a TOUCH_BEGIN event again.
	FlexGlobals.topLevelApplication.stage.addEventListener(
		TouchEvent.TOUCH_BEGIN, onTouchBegin );

	// We don't need to listen for TOUCH_MOVE or TOUCH_END until a touch event begins again.
	FlexGlobals.topLevelApplication.stage.removeEventListener(
		TouchEvent.TOUCH_MOVE, onTouchMove );
	FlexGlobals.topLevelApplication.stage.removeEventListener(
		TouchEvent.TOUCH_END, onTouchEnd );
	_isTouching = false;

	// Now that the user has ended a swipe start the timer so we can let the nav
	// coast to a stop.
	_tic.start();
}

The actual moving is done by the calculateScrollPosition() function which is called when the user is in the process of swiping and when the Timer ( _tic ) is running.

You can see that the if-statement executes when the list has reached the top. I’m using the last value of _deltaY to adjust the animation that moves the entire list down and then bounces it back into position. If _deltaY is large then the entire list will bounce a lot. If it’s just barely moving then the list will move just a little bit. Same goes for the else-statement when the list has reached the bottom.

private function calculateScrollPosition():void {
	var desiredScrollPosition:Number = dataGroup.verticalScrollPosition + _deltaY;

	if ( desiredScrollPosition < 0 && !_isTouching ) {
		// Stop coasting the nav.
		desiredScrollPosition = 0;
		_tic.stop();
		// Use the last value of _deltaY to adjust the bounce. If _deltaY is
		// large then the nav will bounce a lot. If it's just barely moving
		// then the nav will move just a little bit.
		bounceBottomStart.yBy = Math.abs( _deltaY ) * 2;
		bounceBottom.play();
	} else if ( desiredScrollPosition > dataGroup.contentHeight - dataGroup.height
			&& !_isTouching ) {
		desiredScrollPosition = dataGroup.contentHeight;
		_tic.stop();
		// See comment above.
		bounceTopStart.yBy = -Math.abs( _deltaY ) * 2;
		bounceTop.play();
	}
	dataGroup.verticalScrollPosition = desiredScrollPosition;
}

Lastly we’ll need the onTic handler which gives the list its coasting to a stop behavior. Multiply _deltaY by a factor smaller than 1 so that the nav will slow down over time. The closer the value is to 0 the faster the nav will slow down. Likewise the closer the factor is to 1 the longer it will take. Don’t make the factor larger than 1. That would be bad.

private function onTic( e:TimerEvent ):void {
	// Multiply by a factor smaller than 1 so that the nav will slow down over time.
	// The closer the value is to 0 the faster the nav will slow down. Likewise the
	// closer the factor is to 1 the longer it will take. Don't make the factor larger
	// than 1. That would be bad.
	_deltaY *= 0.9;
	// 0.3 is an arbitrary threshold to stop the coasting.
	if( Math.abs( _deltaY ) < 0.3 ) {
		_tic.stop();
	}
	calculateScrollPosition();
}

You may notice the scrollBounceTop.play() and scrollBounceBottom.play(). Those are animations declared in the <fx:Declarations> section. That’s what bounces the list up or down when it scrolls to either the beginning or end.

<fx:Declarations>
	<!--Easing-->
	<s:Bounce id="bounce" />
	<s:Power id="easeInOut" easeInFraction=".5" exponent="2" />

	<!--Bounce DataGroup: top-->
	<s:Sequence id="bounceBottom">
		<s:Move
			id="bounceBottomStart"
			target="{ scroller }"
			duration="400"
			easer="{ easeInOut }" />
		<s:Move
			id="bounceBottomEnd"
			target="{ scroller }"
			yTo="50" duration="400"
			easer="{ bounce }" />
	</s:Sequence>

	<!--Bounce DataGroup buttom-->
	<s:Sequence id="bounceTop">
		<s:Move
			id="bounceTopStart"
			target="{ scroller }"
			duration="400"
			easer="{ easeInOut }"/>
		<s:Move
			target="{ scroller }"
			yTo="50" duration="400"
			easer="{ bounce }"/>
	</s:Sequence>
</fx:Declarations>

thats it