Infinitely Scrolling Lists in Flex Applications

Have you noticed when using twitter, google plus, or certain areas of facebook that when you scroll the page, it automatically loads more data?  You don’t have to continually hit “next” to go through page after page of data. Instead, the content just “appears” as you need it. In this post we will explore a technique for making Flex list components behave in this exact way. As you scroll through the list, it continually requests more data from the server. Take a look at the video preview below, and afterwards we’ll explore the code.

The basic workflow is that you need to detect when you’ve scrolled to the bottom of the list, then load additional data to be displayed further in that list. Since you know how many records are currently in the list, you always know which “page” you are viewing. When you scroll down again, just request the next set of results that are subsequent to the last results that you requested. Each time you request data, append the list items to the data provider of the list.

First things first, you need to detect when you’ve scrolled to the bottom of the list. Here’s a great example showing how to detect when you have scrolled to the bottom of the list. You can just add an event listener to the list’s scroller viewport. Once you have a vertical scroll event where the new value is equal to the viewport max height minus the item renderer height, then you have scrolled to the end. At this point, request more data from the server.

One other trick that I am using here is that I am using conditional item renderers based upon the type of object being displayed. I have a dummy “LoadingVO” value object that is appended to the end of the list data provider. The item renderer function for the list will return a LoadingItemRenderer instance if the data passed to it is a LoadingVO.

Here it is up close, in case you missed it:

Here’s my InfiniteScrollList class:

package components
{
	import model.InfiniteListModel;
	import model.LoadingVO;
	
	import mx.core.ClassFactory;
	import mx.events.PropertyChangeEvent;
	
	import spark.components.IconItemRenderer;
	import spark.components.List;
	
	import views.itemRenderer.LoadingItemRenderer;
	
	public class InfiniteScrollList extends List
	{
		override protected function createChildren():void
		{
			super.createChildren();
			scroller.viewport.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE, propertyChangeHandler );
			itemRendererFunction = itemRendererFunctionImpl;
		}	
	
		protected function propertyChangeHandler( event : PropertyChangeEvent ) : void 
		{
			//trace( event.property, event.oldValue, event.newValue );
			
			if ( event.property == "verticalScrollPosition" ) 
			{
				if ( event.newValue == ( event.currentTarget.measuredHeight - event.currentTarget.height )) 
				{
					fetchNextPage();
				}
			}
		}
		
		protected function fetchNextPage() : void
		{
			if ( dataProvider is InfiniteListModel )
				InfiniteListModel( dataProvider ).getNextPage();
		}
		
		private function itemRendererFunctionImpl(item:Object):ClassFactory 
		{
			var cla:Class = IconItemRenderer;
			if ( item is LoadingVO )
				cla = LoadingItemRenderer;
			return new ClassFactory(cla);
		}
	}
}

You may have noticed in the fetchNextPage() function that the dataProvider is referenced as an InfiniteListModel class… let’s examine this class next. The InfiniteListModel class is simply an ArrayCollection which gets populated by the getNextPage() function. Inside of the getNextPage() function, it calls a remote service which returns data to the client, based on the current “page”. In the result handler, you can see that I disable binding events using disableAutoUpdate(), remove the dummy LoadingVO, append the service results to the collection, add a new LoadingVO, and then re-enable binding events using enableAutoUpdate(). Also, notice that I have a boolean _loading value that is true while requesting data from the server. This boolean flag is used to prevent multiple service calls for the same data.

Let’s take a look at the InfiniteListModel class:

package model
{
	import flash.events.Event;
	import flash.utils.setTimeout;
	
	import mx.collections.ArrayCollection;
	import mx.rpc.AsyncToken;
	import mx.rpc.Responder;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.remoting.RemoteObject;
	
	public class InfiniteListModel extends ArrayCollection
	{
		private var _remoteObject : RemoteObject;
		
		protected var _loading : Boolean = false;
		
		public function get remoteObject():RemoteObject
		{
			return _remoteObject;
		}

		public function set remoteObject(value:RemoteObject):void
		{
			_remoteObject = value;
			if ( _remoteObject )
				getNextPage();
		}
		
		public function InfiniteListModel(source:Array=null)
		{
			super(source);
			addItem( new LoadingVO() );
		}
		
		public function getNextPage() : void
		{
			if ( !_loading)
			{
				_loading = true;
				
				trace( "fetching data starting at " + (this.length-1).toString() );
				var token : AsyncToken = remoteObject.getData( this.length-1 );
				var responder : Responder = new Responder( resultHandler, faultHandler );
				token.addResponder( responder );
			}
		}
		
		protected function resultHandler(event:ResultEvent):void
		{
			this.disableAutoUpdate();
			
			if ( this.getItemAt( this.length-1 ) is LoadingVO )
				this.removeItemAt( this.length-1 );
			
			for each ( var item : * in event.result )
			{
				addItem( item );
			}
			addItem( new LoadingVO() );
			this.enableAutoUpdate();
			
			_loading = false;
		}
		
		protected function faultHandler(event:FaultEvent):void
		{
			trace( event.fault.toString() );
		}
	}
}

Now, let’s take a look at the root view that puts everything together. There is an InfiniteScrollList whose dataProvider is an InfiniteListModel instance. The InfiniteListModel also references a RemoteObject instance, which loads data from a remote server.

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" 
		xmlns:s="library://ns.adobe.com/flex/spark" 
		title="Infinite Scroll" 
		xmlns:model="model.*" 
		xmlns:components="components.*">
	
	<fx:Declarations>
		<model:InfiniteListModel id="infiniteListModel"
								 remoteObject="{ remoteObject }" />
		
		<s:RemoteObject id="remoteObject"
						destination="ColdFusion"
						source="com.tricedesigns.infiniteScroll.Services"
						endpoint="http://tricedesigns.com/flex2gateway/" />
	</fx:Declarations>
	
	<components:InfiniteScrollList 
			id="list"
			width="100%" height="100%"
			dataProvider="{ infiniteListModel }" />
			
</s:View>

Let’s not forget the remote service. In this case, I’m calling into a very basic remote CFC that returns an Array of string values. You can see the code below:

<cfcomponent>
	
	<cffunction name="getData" access="remote" returntype="array">
		<cfargument name="startIndex" type="numeric" required="yes">
		
        <cfset items = ArrayNew(1)>
        <cfloop from="1" to="25" index="i">
         	<cfset item = "item " & (i+startIndex)>
            <cfset ArrayAppend( items, item )>
         </cfloop>
        
		<cfreturn items>
	</cffunction>
    
</cfcomponent>

You can download the full source code for this application directly from here:
http://tricedesigns.com/portfolio/infiniteScroll/infiniteScroll.zip

“Almost Native” – iOS Apps Powered By iAd.js and PhoneGap

One of the benefits of using a cross-platform development technology that I mentioned in my last post is that cross-platform development technologies can be solution accelerator platforms.  I mean “solution accelerator” because it can be easier and faster to develop an application using cross-platform technologies (web development skills) rather than native development, even if you are only targeting a single platform.  This isn’t the case for every solution, but certainly can be for many scenarios.

Here’s a sample application that I put together while exploring the PhoneGap product offering.   For those that weren’t aware, Adobe has entered an agreement to acquire Nitobi, the makers of PhoneGap, and PhoneGap is an HTML5 app platform that allows you to author native applications with web technologies and get access to APIs and app stores.

This application consumes data from the rottentomatoes.com API, and is built entirely in JavaScript.  If you haven’t seen it before, RottenTomatoes.com is a site for searching movie ratings & information.  Check out the video below, and then we’ll examine how the application was built (be sure to check out the CoverFlow runtime performance at 34 seconds).

The entire codebase that I had to write for this application is 289 lines of HTML & JavaScript (including whitespace), all in a single file, and was built using iAd.js, xui.js, and of course, PhoneGap.   The first thing you might be wondering is “what is iAd.js?” or, if you know about the iAd platform, you might be thinking “um… isn’t that just for advertisements?”. Everyone who uses an iOS device has probably encountered an iAd at least once, whether they know it or not.  The iAd program is an advertising platform for iOS devices that enables advertisers to create rich, engaging, & interactive advertisements.    Interestingly, all iAds are built on top of a JavaScript framework built by Apple.

It just so happens that the iAd.js JavaScript framework can also be used outside of the advertising context (only on iOS devices – I tried Android as well, but not all of the elements worked correctly).   Not all features of the iAd framework will work outside of the advertising context, such as purchasing or interacting with the iTunes store.  However the iAd.js framework will provide you with user interface elements that look nearly identical to native iOS components.  This includes view navigators, table views (with groups, disclosure indicators, etc…), carousel views, cover flow views, wheel pickers, progress bars, sliders, switches, buttons, and much more.   In addition, the interactions and animations for these components are highly optimized for mobile safari, and interaction with these elements feels very, very close to native.  There are a few minor things here and there, but overall it is not necessarily easy to distinguish the difference.

Note: I have not submitted any apps using this technique to Apple’s app store.  However, I have heard from others that Apple has accepted their applications which are built using this approach.   

While these components are instantiated in the browser and created via HTML & JavaScript, the programming model of iAd components is very similar to native iOS development.  You still have the usage of protocols (interfaces), and controller & delegate patterns.  For example, using the iAd.TableView ui component, still requires use of the TableViewDataSource and TableViewDelegate protocols (just implemented in JavaScript). Familiarity with native iOS development will definitely be a big plus if you are using the iAd.js framework.

I used xui.js to simplify the syntax for XMLHttpRequest for asynch data loading, and of course, PhoneGap is used for the application container, as well as any native OS interaction if you wanted any.  The source code could certainly be broken up into multiple controllers or separate files for maintainability, however I was just going for a “quick & dirty” example.

Basically, you just need to include the iAd JavaScript and CSS files, then build your application as you would any other HTML/JS PhoneGap experience.   You can download the iAd framework from here if you are a registered Apple iOS developer.   In theory, this isn’t that much different from using jQuery mobile components, however these have better performance on iOS, and have a more-native feel.

Thanks to pixelranger and merhl from Universal Mind for showing me a while back that you could use this approach!

Full source code below:

<html>

    <head>
        <title>RottenTomatoes</title>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <meta name="apple-mobile-web-app-capable" content="yes">

        <!-- Import iAd Assets -->
        <link rel="stylesheet" href="iAd/iAd.css">
        <script type="text/javascript" src="iAd/iAd.js" charset="utf-8"></script>

        <!-- Import other libraries -->
        <script type="text/javascript" charset="utf-8" src="libs/xui-2.2.0.js"></script>
        <script type="text/javascript" charset="utf-8" src="libs/phonegap-1.1.0.js"></script>

        <style type="text/css" media="screen">

            body {
                margin: 0;
                overflow: hidden;
            }

            .ad-flow-view {
                position: relative;
                left: 0px;
                width: 300px;
                height: 380px;
                -webkit-perspective: 400;
            }

            .ad-flow-view .ad-flow-view-camera {
                position: absolute;
                left: 50%;
                width: 10px;
                height: 10px;
            }

            .ad-flow-view .ad-flow-view-cell {
                position: absolute;
                top: 30px;
                left: -125px;
                width: 250px;
                height: 350px;
            }

            .ad-flow-view .ad-flow-view-cell img {
                pointer-events: none;
                width: 180px;
                height: 267px;
                -webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0.8, transparent), to(rgba(255,255,255,0.35)));
            }

        </style>

        <script type="text/javascript" charset="utf-8">

            var API_KEY = "put your api key here";

            var sampleData;

            /* ==================== Controller ==================== */

            var controller = {
                data : []
            };

            controller.init = function () {
                var url = "http://api.rottentomatoes.com/api/public/v1.0/lists/movies/box_office.json?limit=15&country=us&apikey=" + API_KEY;
                console.log( url );

                x$().xhr( url, {
                         async: true,
                         callback: function() {

                            var trimmedResponse = this.responseText.replace(/^\s\s*/, '').replace(/\s\s*$/, '');

                            if ( trimmedResponse.length > 0 )
                            {
                                sampleData = eval( "(" + trimmedResponse + ")" );

                                if ( sampleData )
                                {
                                    controller.data =  sampleData.movies ;
                                    controller.table.reloadData();
                                }
                            }

                         }
                    });

                this.navigation = new iAd.NavigationController();

                this.navigation.delegate = this;
                iAd.RootView.sharedRoot.addSubview(this.navigation.view);

                this.navigation.navigationBar.barStyle = iAd.NavigationBar.STYLE_BLACK;
                this.navigation.pushViewControllerAnimated(this.createTableViewController(), false);
            };

            controller.handleEvent = function (event) {
                if ( event.type == iAd.View.TOUCH_UP_INSIDE_EVENT ) {
                    var viewController = this.createFlowViewController();
                    this.navigation.pushViewControllerAnimated(viewController, true);
                    viewController.view.hidden = false;
                }
            };

            controller.displayDetails = function (index) {
                var item = this.data[index];
                var viewController = this.createDetailViewController(item);
                this.navigation.pushViewControllerAnimated(viewController, true);
                viewController.view.hidden = false;
            }

            controller.createTableViewController = function (index) {

                if ( this.viewController == null ) {
                    this.viewController = new iAd.ViewController();
                    this.viewController.title = "Rotten Tomatoes";

                    this.viewController.navigationItem.rightBarButtonItem = new iAd.BarButtonItem();
                    this.viewController.navigationItem.rightBarButtonItem.style = iAd.BarButtonItem.STYLE_DONE;
                    this.viewController.navigationItem.rightBarButtonItem.title = 'Flow';
                    this.viewController.navigationItem.rightBarButtonItem.addEventListener( iAd.View.TOUCH_UP_INSIDE_EVENT, this, false );

                    // create a TableView
                    this.table = new iAd.TableView();
                    this.table.tableStyle = iAd.TableView.STYLE_GROUPED;
                    this.table.delegate = this;
                    this.table.dataSource = this;
                    this.table.size = new iAd.Size(window.innerWidth, window.innerHeight-46);

                    this.viewController.view.addSubview(this.table);
                }

                return this.viewController;
            };

            controller.createFlowViewController = function (index) {
                var viewController = new iAd.ViewController();
                viewController.title = "Flow";

                var flowView = new iAd.FlowView();
                flowView.dataSource = this;
                flowView.delegate = this;
                flowView.layer.style.backgroundColor = "#FFFFFF";

                // customize flow view
                flowView.sidePadding = 150;
                flowView.cellRotation = 65;
                flowView.cellGap = 50;
                flowView.dragMultiplier = 1.0;
                flowView.sideZOffset = -200;

                // load the data
                flowView.reloadData();
                flowView.centerCamera();
                this.flowView = flowView;

                viewController.view.addSubview(flowView);

                viewController.view.hidden = true;
                this.flowSelectedIndex = 0;
                return viewController;
            };

            controller.flowViewNumberOfCells = function(flowView) {
                return this.data.length;
            };

            controller.flowViewCellAtIndex = function(flowView, index) {
                var cell = document.createElement('div');
                cell.appendChild(document.createElement('img')).src = this.data[index].posters.detailed;
                return cell;
            };

            /* ==================== iAd.FlowViewDelegate Protocol ==================== */

            controller.flowViewDidTapFrontCell = function (flowView, index) {
                console.log('flowViewDidTapFrontCell ' + index + ", " + this.flowSelectedIndex);
                if ( this.flowSelectedIndex == index )
                    this.displayDetails( index );
            };

            controller.flowViewDidSelectCell = function (flowView, index) {
                console.log('flowViewDidSelectCell ' + index);
                this.flowSelectedIndex = index;
            };

            controller.flowViewDidBeginSwipe = function (flowView) {
                console.log('flowViewDidBeginSwipe');
            };

            controller.flowViewDidEndSwipe = function (flowView) {
                console.log('flowViewDidEndSwipe');
            };

            controller.createDetailViewController = function (item) {
                var viewController = new iAd.ViewController();
                viewController.title = item.title;

                var scrollView = new iAd.ScrollView();
                scrollView.userInteractionEnabled = true;
                scrollView.size = new iAd.Size(window.innerWidth, window.innerHeight-46);
                scrollView.horizontalScrollEnabled = false;
                scrollView.layer.style.backgroundColor = "#FFFFFF";

                var imageView = scrollView.addSubview(new iAd.ImageView());
                imageView.image = iAd.Image.imageForURL( item.posters.profile );
                imageView.position = new iAd.Point(10, 10);
                imageView.size = new iAd.Size(120, 178);

                var synopsisLabel = scrollView.addSubview(new iAd.Label());

                synopsisLabel.text = item.synopsis;

                synopsisLabel.numberOfLines = 0;

                synopsisLabel.size = new iAd.Size(this.navigation.view.size.width-150, 800	);
                synopsisLabel.position = new iAd.Point(140, 10);
                synopsisLabel.autoresizingMask = iAd.View.AUTORESIZING_FLEXIBLE_WIDTH | iAd.View.AUTORESIZING_FLEXIBLE_HEIGHT;
                synopsisLabel.verticalAlignment = iAd.Label.VERTICAL_ALIGNMENT_TOP;

                viewController.view.addSubview( scrollView );
                viewController.view.hidden = true;
                return viewController;
            };

            /* ==================== TableViewDataSource Protocol ==================== */

            controller.numberOfSectionsInTableView = function (tableView) {
                return 1;
            };

            controller.tableViewNumberOfRowsInSection = function (tableView, section) {
                return this.data.length;
            };

            controller.tableViewCellForRowAtPath = function (tableView, path) {
                var cell = new iAd.TableViewCell();
                cell.text = this.data[path.row].title;
                cell.detailedText = 'title';
                cell.accessoryType = iAd.TableViewCell.ACCESSORY_DISCLOSURE_INDICATOR;
                cell.selectionStyle = iAd.TableViewCell.SELECTION_STYLE_BLUE;
                return cell;
            };

            controller.tableViewTitleForHeaderInSection = function (tableView, section) {
                return "Box Office Movies";
            };

            controller.tableViewTitleForFooterInSection = function (tableView, section) {
                return "Powered by RottenTomatoes.com";
            };

            /* ==================== TableViewDelegate Protocol ==================== */

            controller.tableViewDidSelectRowAtPath = function (theTableView, path) {
                this.displayDetails(path.row);
            };

            controller.tableViewDidSelectAccessoryForRowAtPath = function (theTableView, path) {
            };

            /* ==================== iAd.NavigationViewDelegate Protocol ==================== */

            controller.navigationControllerWillShowViewControllerAnimated = function (theNavigationController, viewController, animated) {
            };

            controller.navigationControllerDidShowViewControllerAnimated = function (theNavigationController, viewController, animated) {
            };

            /* ==================== Init ==================== */

            function init () {
                console.log( "init" );
                controller.init();
            }

            window.addEventListener('load', init, false);

        </script>

    </head>

    <body></body>

</html>

Why Cross Platform Mobile Development?

Perhaps you have heard of the topic “cross platform development”, but aren’t really sure what it is, or you aren’t sure why you would want to use cross-platform technologies. If this is the case, then this post is especially for you. I’ll try to shed some light onto what it is, and why you would want to use cross-platform development strategies.

What is cross-platform development?

Cross platform development is a concept in computer software development where you write application code once, and it runs on multiple platforms. This is very much inline with the “write once, run everywhere” concept pioneered in the 90s, and brought to a mainstream reality with Flash in the browser, and AIR on the desktop. The standard evolution of technology has been to make everything faster, smaller, and more portable, and it is only natural that that this concept has now come into the mobile development world. In mobile scenarios, it is applied by writing an application using a codebase & technology that allows the application to be deployed and distributed across multiple disparate platforms/operating systems/devices.

Using Adobe AIR, this includes Apple’s iOS devices (iPhone and iPad), Android (a plethora of devices), BlackBerry Playbook & upcoming BBX platform, and soon Windows Metro (the tablet offering of Windows 8).   Using PhoneGap, this includes Apple iOS, Android, BlackBerry 4.6 and higher, HP WebOS, Symbian, Samsung Bada, and Windows Phone 7 – Mango platforms.

In case you’re wondering why I offered 2 cross platform technologies, that is because Adobe will soon have 2 cross-platform product offerings.  Adobe has entered an agreement to purchase Nitobi, the creators of PhoneGap.

Adobe AIR

Adobe AIR is a cross-platform technology with roots in the Flash Player and the AIR desktop runtime.  AIR allows you to build cross-platform mobile applications using ActionScript and the open source Flex framework.   AIR apps can be built from the Flash Professional timeline-based design/animation tool, Flash Builder (an Eclipse-based development environment), or other open source solutions using the freely available AIR SDK.   Applications developed with Adobe AIR can target desktop platforms (Mac, & Windows), smart phone and tablet platforms (iOS, Android, BlackBerry, soon Windows), and even smart televisions.

PhoneGap (Apache Callback)

PhoneGap is an open source cross platform technology with roots in the HTML world. Essentially, a PhoneGap application consists of a web view that consumes 100% of the available width & 100% of the available height, taking advantage of web browsers on each platform.   PhoneGap offers a JavaScript to native bridge that enables you to build natively-installed applications using HTML and JavaScript, using the native bridge to interact with the device hardware/APIs.   Note: PhoneGap is also being submitted to the Apache Foundation as the Apache Callback project.

More Devices, Less Code

The driving factor behind cross-platform technologies is that you will be able to use those technologies to target more devices & platforms, with writing a minimal amount of source code.   There are many advantages with this approach.  Here are a few of the major reasons…

Lower Barrier of Entry

Generally speaking, development with HTML & JavaScript or Flex & ActionScript is easier than developing with Objective-C or Java.   Due to the ease of use of the development tooling and familiarity of the languages, cross platform technologies lower the technical barriers which may have prevented adoption of native development.  This allows your development team to build applications that they may not previously have been able to, and also enables your team to focus on what matters – the application; not the skills required to develop on multiple disparate platforms.

Reduce the Number of Required Skills for the Development Team

Native development on multiple platforms requires your development team to learn Objective C for iOS applications, Java for Android applications, Silverlight for Windows Phone applications, etc…   Finding all of these skills in a single developer is nearly impossible.   Using cross-platform development technologies, your team only needs to be proficient with one language/skillset.  Knowledge of the native development paradigms and languages are always a plus, but are no longer a requirement.   Many developers transitioning from web development already know either Flex/ActionScript and/or HTML/JavaScript, and making the transition from web to mobile development will not be a major undertaking.

Reduced Development & Long Term Maintenance Costs

Cross-platform mobile applications can originate from a single codebase, which requires a single development skillset.  You don’t need to have staff for each individual platform.  Instead, resources working on the shared codebase can cover all target platforms.   Having a single codebase also reduces long term maintenance costs.  You no longer need to have bug tracking for X number of codebases, and do not need to maintain a larger staff to support each platform.  Did I also mention that you have one codebase to maintain?

Having a single codebase doesn’t reduce the need for QA/testing on each target platform – nothing can get rid of this.  It is absolutely imperative that you test your codebase on physical devices for all platforms that you intend to support.   Emulators and Simulators can go a long way during development, but they will undoubtedly not cover all scenarios possible on a physical device, and they will not have the same runtime performance as a physical device.

Play the Strengths of a Technology

Some technologies make tasks easier than others.   For example, programmatic drawing and data visualization are very easy using Flex & ActionScript.   Developing equivalent experiences in native code can be significantly more complex and time consuming.   Use the the features of the language to their fullest potential, to your advantage- that’s why they exist.

Adobe at BlackBerry DevCon 2011

BlackBerry DevCon 2011 kicked off earlier this week, and surrounding it were some exciting announcements around Adobe tools and BlackBerry platforms. These announcements include Flash Player 11 and AIR 3 features available on the PlayBook – including Stage3D (among many other great features). Also announced was AIR support for the new BlackBerry BBX operating system, as well as PhoneGap support for BBX/QNX. BBX is the new QNX based operating system for BlackBerry smartphones.

Adobe’s VP and General Manager of Interactive Solutions, Danny Winokur, joined RIM’s Alec Saunders, VP of Developer Relations and Ecosystems Development, on stage at BlackBerry DevCon Americas 2011. Danny spoke about the exciting possibilities that Flash and HTML5 bring to the web and mobile app development – specifically for the BlackBerry PlayBook and BBX in the future. (read more here, or check out the video below)

MAX Sneaks Now Available!

Adobe MAX was a great opportunity to see the latest projects that Adobe has been working on. Not only are the keynotes and announcements exciting, but there is a lot that which doesn’t make it into the keynote. In addition, Adobe gives you a glimpse into the future with sneak previews of new software ideas, before they have made it into actual products. All of the “sneaks” from this year are now available on Adobe TV. There are some great examples of de-blurring images, creating 3D video from 2D video sources, audio/video synchronization, and much more.

Here are a few sneaks/enhancements specifically focusing around the Flash Platform:

Reverse Debugging in Flash Builder:

Near-Field Communications with AIR Native Extensions:

GPU Parallism with PixelBender:

“Monocle” Realtime Profiling: