Berichten met label flex

GraniteDS and AIR for mobile

In this article I will briefly show how to resolve some obstacles I came across when I developed my first application with AIR for mobile and GraniteDS. The most noteworthy reason of using AIR to create mobile applications is of course the multi-platform deployment using a single codebase. Furthermore, with Granite you are able to disclose the services of an existing Java backend to a mobile platform without significant changes to the backend. This offers great potential for enterprises who are struggling with the fragmented mobile market and don’t want to completely rewrite their existing Java backend.

I will assume you have some familiarity with AIR for mobile and Granite. It’s mostly the same as for Flex but there are some things you have to take into account.

1. Get the right version of Granite

The latest version of Granite is 2.2.1 GA. However, in the most recent version of AIR and Flex Adobe made some changes in the API which breaks backwards compatibility for some features of Granite. Therefore this release of Granite won’t work using the newest SDK. Refer to this post on the Granite form for more info on how to make these changes yourself. If you don’t want be bothered with building Granite yourself just download this version of GraniteDS to get started immediately.

2. Connecting to the right server

A problem for AIR applications in general (both desktop and mobile) is setting the right server settings. It is quite simple when running within the browser: The server address is changed with a single reconfiguration of the swf-file and all clients are using the new address on a browser refresh. With AIR it is a bit different and you’re not always at liberty to ‘hardcode’ the service settings in the AIR distribution package.
Granite offers a method for dynamic server configuration using the server initializer component:

Tide.getInstance().addComponentWithFactory(
  "serviceInitializer",
  DefaultServiceInitializer, {
    contextRoot: '/my-app',
    serverName: “10.0.0.1”,
    serverPort: “8080});

Note that once a connection is made, it is not possible to reconnect with another configuration, because the service initializer is only used once. You have to restart the application to enable the new connection settings or reset Tide’s RemoteObject. Unfortunately Tide’s API doesn’t support this reset. I came up with a small workaround which requires you to extend the EJB class with an extra reset method with the following body:

public class Ejb extends org.granite.tide.ejb.Ejb {
  /**
   * Reset the Tide Connection to allow new server settings
   */
  public function resetConnection():void {
    if (_ro) {
      _ro.disconnect();
    }
    _ro = null;
  }
 
}

This method resets Tide’s RemoteObject so the next remote call will force a reinitialization using the current settings of serviceInitializer. Refer to Granite’s issue tracker or forum thread for more details.

3. Automatic logout

Applications running on mobile platforms are always susceptible to unpredictable interruptions. For example when a phone call or text is received. Mobile AIR applications provide a deactivate event which is dispatched when the application is halted somehow. The application I wrote was using Tide’s Identity class for user login. Therefore I added an event handler to automatically logout the user and push the LoginView on top of the navigator stack:

private function deactivateHandler(event:Event):void {
  if (identity.loggedIn) {
    identity.logout();
  }
  navigator.popAll(); // Purge the navigator history to disable back button usage
  navigator.pushView(LoginView);
}

4. Build, build, build!

This isn’t directly related to Granite or AIR for mobile. But since they can both be used for enterprise scale applications I thought I’d mention it shortly: Make sure you have a proper build script. Now, I’ve got an example from Chris Black which provides a good starting point. I’ve only added the metadata compiler options required for Tide and of course a reference to the Granite libraries and generated Actionscript classes.

 
 <mxmlc ... >
 
  <!-- .... -->
 
  <!-- location of generated as classes with gas3 -->
  <source-path path-element="${gen.src.dir}" />
 
  <compiler.library-path dir="${basedir}/libs" append="true">
    <include name="granite-essentials.swc" />   
    <include name="granite.swc" />
  </compiler.library-path>
 
  <keep-as3-metadata name="Bindable" />
  <keep-as3-metadata name="ChangeEvent" />
  <keep-as3-metadata name="Destroy" />
  <keep-as3-metadata name="Id" />
  <keep-as3-metadata name="In" />
  <keep-as3-metadata name="Inject" />
  <keep-as3-metadata name="Managed" />
  <keep-as3-metadata name="ManagedEvent" />
  <keep-as3-metadata name="Name" />
  <keep-as3-metadata name="NonCommittingChangeEvent" />
  <keep-as3-metadata name="Observer" />
  <keep-as3-metadata name="Out" />
  <keep-as3-metadata name="PostConstruct" />
  <keep-as3-metadata name="Transient" />
  <keep-as3-metadata name="Version" />
 
</mxmlc>

One plus one

I can imagine one must be thinking: ‘Everyone could have figured that out!’ And I totally agree, because that’s exactly what this article is about. With some experience with Flex, a developer can write a mobile application on top of a Java EE backend. It doesn’t take much to utilize an existing backend from a mobile platform. Since the latest release of AIR the performance for iOS and Android is pretty good and together with the Granite Enterprise Platform the barrier to emerge an enterprise application to a mobile platform has become much lower.

, , , , , , ,

2 reacties

Introduction to AIR for mobile

A couple of weeks ago, Adobe released a preview of the new Flash Builder and SDK. One of the new features is support for mobile devices using AIR. For now, only Android 2.2 is capable of running mobile AIR applications, but since Steve has changed the rules again, AIR on iOS is nearby, according to Adobe. Time to take a test drive with a tutorial.

Goal

We will create a simple RSS reader. Not a very exciting example, but already shows some nice features of AIR for mobile. I’ll assume you have experience with Flex or AIR, but I think you will manage with some level of programming experience. If not, feel free to leave a reply if you have any questions or take a look at the full source.

Setup

Empty project contents

Download and install the preview release of Flash Builder called Burrito It provides a wizard to start an empty mobile application. By default the wizard creates two mxml files containing a MobileApplication and a View component.

The MobileApplication class is inherited from the Application class used for desktop AIR. The mobile version provides, among other things, an action bar and a navigator. Furthermore its firstView property points to the empty view component. The view component is a normal group, but optimized for mobile use as well.

Article list

Open the main view component (called <projectName>Home.mxml). Create a List component in the main view. Provide positioning constraints to let it occupy the whole screen.

<s:List left="0" top="0" bottom="0" right="0" />

To retrieve the feed, create an HTTPService pointing to the RSS feed in the declaration section of the view. Add a resultHandler function to parse the feed-data. Parsing xml data is quite simple. You can just navigate to the DOM-tree as it were a normal ActionScript object. Use the data property of the view to store the articles locally. Using this property will provide some benefits with navigation, which will become clear in a bit.

<fx:Script>
  <![CDATA[
    import mx.rpc.events.ResultEvent;
 
    protected function service_resultHandler(event:ResultEvent):void {
      data = event.result.rss.channel.item;
    }
  ]]>
</fx:Script>
<fx:Declarations>
  <s:HTTPService
    id="service"
    url="http://lsd.luminis.nl/feed/"
    result="service_resultHandler(event)" />
</fx:Declarations>

Now there are only a couple of things left to do. Invoke the server and provide the list with the articles and instructions to show them.

Both are quite simple. The feeds are retrieved by invoking the send() method of the service. To automatically retrieve them, this method should be invoked when the view is created so we will use the creationComplete handler.

<s:View
  xmlns:fx="http://ns.adobe.com/mxml/2009" 
  xmlns:s="library://ns.adobe.com/flex/spark"
  title="Home"
  creationComplete="service.send()">

Finally, bind the data property to the dataProvider of the list and set its labelField attribute to ‘title’.

<s:List
  dataProvider="{data}"
  labelField="title"
  left="0" top="0" bottom="0" right="0" />

Your view will probably look something like this.

<?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="Home"
  creationComplete="service.send()">
 
  <fx:Script>
    <![CDATA[
      import mx.rpc.events.ResultEvent;
 
      protected function service_resultHandler(event:ResultEvent):void {
        data = event.result.rss.channel.item;
      }
 
    ]]>
  </fx:Script>
 
  <fx:Declarations>
    <s:HTTPService
      id="service"
      url="http://lsd.luminis.nl/feed/"
      result="service_resultHandler(event)" />
  </fx:Declarations>
 
  <s:List
    dataProvider="{data}"
    labelField="title"
    left="0" top="0" bottom="0" right="0" />
 
</s:View>

Emulator

Emulate hardware buttons

Emulate hardware buttons


Now it is time to run a first test. Click the Run button and select ‘On Desktop’ as launch method in run configuration dialog. Also choose a device to simulate and run the application.

When the emulator is launched it shows a list of articles after a couple of seconds. Now you can select Rotate Right from the Device menu to display the landscape view. You will also notice the list is able to scroll up and down when dragging your mouse pointer (Which of course is the emulated equivalent of a one-finger swipe)

Article details

Next is to create a detailed view showing the full content of an article. Close the emulator and return to Flash Builder. In the views package, create a new component and call it DetailView. Bind the title attribute to data.title. Add a label as well and bind the text property to data.description. Don’t forget to specify the positioning constraints.

<s:View
  xmlns:fx="http://ns.adobe.com/mxml/2009" 
  xmlns:s="library://ns.adobe.com/flex/spark"
  title="{data.title}">
 
  <s:Label
    text="{data.description}"
    left="20" right="20" bottom="20" top="20" />
</s:View>

All view components provide a navigator object. We will use this in the change event handler of the article list. This event is fired when the user selects an article from the list. It looks as follows:

import spark.events.IndexChangeEvent;
 
protected function articleList_changeHandler(event:IndexChangeEvent):void {
  navigator.pushView(DetailView, data[event.newIndex]);
}

It simply instructs the navigator to create a new DetailView and push it on top of of the navigation stack. Furthermore the selected item from the article list is passed on to the data property of the DetailView.

Add the change handler and assign it to the change event of the article list:

<s:List
  dataProvider="{data}"
  labelField="title"
  change="articleList_changeHandler(event)"
  left="0" top="0" bottom="0" right="0" />

Article list inside the emulator

Article list inside the emulator

For performance optimization, AIR destroys the view when a user navigates away and recreates it when he returns. Only the contents of the data property is saved and passed into a recreated view of the same type. This is why we used it to store the articles. Otherwise the user has to wait for a reload when he returns to the main view.

When you launch the new version in the emulator, you will notice the application slides to the detail view when an article is selected. Click Back from the Device menu to simulate a click on the hardware back button.

Last but not least: action buttons

One thing our application is missing, is a button to go to the original webpage showing the full article. This button is placed inside the header of our detail view, next to the title.
To achieve this, add the button inside the actionContent of the detail view. Give the button an icon (I used one from the Tango project) and set the click handler to open the url of the article.

<s:actionContent>
  <s:Button
    click="{navigateToURL(new URLRequest(data.link))}"
    icon="@Embed('/assets/internet-web-browser.png')"/>
</s:actionContent>

Of course you might want an additional button to return to the article list instead of using the ‘hardware button’. Place this button inside the navigationContent. This will place the back button at the upper left corner. The click event of this button will invoke the navigation.popView() method to remove the current view from the stack and return to the previous one.

<s:navigationContent>
  <s:Button
    click="navigator.popView()"
    icon="@Embed('/assets/go-previous.png')"/>		
</s:navigationContent>
Action and navigator buttons

Action and navigator buttons

If you launch the application you will notice the buttons are nicely aligned at the top of the screen.

What’s next?

The next step will be to package the application to an apk file so you could install it on an Android device. This is done by the Export Release build wizard inside the Project menu of Flash Builder. It includes the ability to sign your application to allow distribution through the Android Market.

Another nice feature which I will not discuss in detail is to add gesture based navigation. Take a look at this article from Adobe to find out more. The approach discussed should be working for mobile AIR applications as well.

Unfortunately the most interesting benefit of using AIR for mobile didn’t become clear with this tutorial. It would be nice to deploy our application on other mobile plastforms as well. However the cross compiler for iOS isn’t available yet and the same goes for AIR on Windows Phone, BlackBerry or Symbian. We will just have to wait when Adobe is ready so we can fully benefit from the “Write once, run anywhere” promise.

, , , ,

3 reacties

Developing a Flex AIR application

Project description

During the last few weeks, we’ve been working on setting up a sample application using Adobe Flex / ActionScript (working with Adobe Flex Builder 3).

Luminis had already developed a Web application for Nedap Healthcare, and we would like to know whether a desktop variant with an offline scenario would be possible. The web application displays items such as a worksheet, planning, news, weather and birthdays of colleagues.

The desktop application does not include all functionality of the Web application, but focuses on the worksheet. The existing functionality will be rebuilt in Flex and Air. The focus of our assignment was on modifying times in the worksheet and implementing an online / offline scenario.

User Interface

Web applications have set a new trend in interface design. The rules of interface design are no longer bound by the rules of the Operating System, and also no longer bound by the rules of the developer. The design for Web applications is usually defined by a designer, and implemented by the developer. For example the Gmail look and feel differs from a standard desktop application, but works in a familiar way. Buttons remain buttons, and input fields look like input fields, but there is the freedom to create a completely new interaction model and design.

The web application we were to base our desktop application on was designed by one of our team members which also did the interaction model. A specific design for a software application can help the customer to recognize the product and get a better feeling about the application. Don’t think of it as your corporate style embedded in the software, but also as user-friendly since it is targeted toward the end user.

The desktop application is a simple, familiar, user-friendly version of the Web application to be used by the target audience: women aged 40-50 years with little to no computer experience.

The interface is a direct derivative of the Web application and has the same look and feel.
The desktop application provides the same functionality as the web application, adding the possibility of changing times and the control of the application when it is offline.

Flex

Flex makes it easy and fast to build applications. This is especially due to the many standard components. Much can be modified in Flex, such as colors and properties such as shadows and corners. Also refer to the
Flex 3 Style Explorer

The same functionality of our Flex application could be built quickly if we would use standard Flex. The challenge was to make no concessions to our design. In this process in which we were responsible for design and implementation, it was occasionally hard to make a balance between adjusting the design or features in Flex to match original design.

The implementation

The most common approach building software appliances, is to build it with the standard components and skin it later on. This not only affects the design, but also does not involve the end user in the process.

The implementation chosen here is the same as implementing a design in HTML. From the existing design, slice the PSD and create a grid in Flex. Put the images into the grid and translate component design to Flex where necessary.

In Flex, the possibilities are extensive and there are minimal restrictions. Also covered are a lot of design possibilities like rounded corners, drop shadows and glows. This makes building your application very flexible. But also means you rebuild your original (Photoshop) design in Flex. So adjusting your original design could mean rebuilding your Flex implementation.

For example, when using the application, we noticed that there were indicators missing in the design. These were:

  • Feedback to notify the user what the application was doing.
  • An indication of whether the application was online or offline.

Not wanting to create a new, more complex design (adding items such as a status bar or an online/offline indicator [on/off light bulb]), we chose to show the online and offline situation turning the colors of the application to black and white, and displayed the status at the place where it is triggered on the button. The button is not useful when the application is busy.

Technologies

This should give an overview of the issues we ran into and how we solved them in the context of using the mentioned technologies to setup an AIR application. To get a bit of an overview of the possibilities of Flex / get familiar with the subject, a good first step would be to check out Tour de Flex. Since that application includes all (re)sources, it’s also very convenient as a reference / to get basic samples up-and-running quickly.

Now, moving on to the issues encountered and how we tackled them. Please note that the sample code below includes only non-standard import statements and is taken from an Adobe AIR application that uses AIR-only functionality!

XML

It took a while to get a bit of a feeling for creating and handling XML type objects. To illustrate, a number of relevant subtleties are incorporated in the following bit of code:

    var i:int = 2;
    var s:String = 'green';

    // Convert a String value to an XML object to create the first element:
    var e1:XML = XML("<element attr1='1'><label>first label</label>" +
                     "<color>blue</color></element>");

    // Create the second XML element using the values of variables 'i' and 's':
    var e2:XML =
        <element attr1={i}>
            <label>second label</label>
            <color>{s}</color>
        </element>;

    // Now create the final XML object using the 2 elements that were created:
    var x1:XML = <main>{e1}{e2}</main>;

    // And query it for a specific attribute value ('attr1', be sure not to forget the '@'!)
    // and element value (color) to determine the label value of a specific element:
    var result:String = x1.element.(@attr1 == 2).(color == 'green').label;
    // The value of the 'result' variable now is "second label". It would've been an empty
    // string ("") in case no matching element was found.

HTTP POST

The application started out using only the HTTP GET method, which was easily achieved by using the HTTPService. However it was soon necessary to post data as well and getting the HTTPService to do this properly / finding some good examples (or even debuggin the (https) requests that were created properly) turned out to be a bit of a hassle. When we stumbled upon the URLRequest we got things up-and-running fairly easily:

    import flash.net.*;

    private function onPostComplete(event:Event):void {
    }

    private function doPost():void {
        var dataToPost:XML = x1; // Posting the XML object from the previous example.
        var urlLoader:URLLoader = new URLLoader();

        // Define a handler in which actions can be defined for when the post is complete.
        urlLoader.addEventListener(Event.COMPLETE, onPostComplete);

        var urlToPostTo:String = "https://fillinyourhosthere.net/post.xml"
        var urlRequest:URLRequest = new URLRequest(urlToPostTo);
        urlRequest.data = dataToPost.toXMLString();
        urlRequest.method = URLRequestMethod.POST;
        urlLoader.load(urlRequest);
    }

Online / Offline

Another requirement of the application was that it should be usable even if the target server went down or the client using the application is offline. As it turns out, this functionality is readily provided through using the URLMonitor:

    // This code assumes a 'mx:WindowedApplication' with 'initialize="onWindowInitialized(event)"'.

    import air.net.URLMonitor;

    private var m_monitor:URLMonitor;

    private function onWindowInitialized(e:Event):void {
        var serviceURL:String = "https://fillinyourhosthere.net";
        var serviceToMonitor:URLRequest = new URLRequest(serviceURL);
        m_monitor = new URLMonitor(serviceToMonitor);
        m_monitor.addEventListener(StatusEvent.STATUS, announceStatus);
        m_monitor.start();
    }

    private function announceStatus(e:StatusEvent):void {
        if (m_monitor.available) {
            // Possibly update work that was done if previously offline.
        } else {
            // The application is (now) offline.
        }
    }

Updating DataProvider data

When using e.g. an array of data objects as a dataprovider for a TileList (or a DataGrid/AdvancedDataGrid) and changing (the value of) an item in the array, the view isn’t (always) updated immediately. If you want to see the changes immediately, you should use the invalidateList() method:

    <mx:TileList
        id="tl"
        dataProvider="myDataProvider"
    <!-- ...rest of the declaration... -->

    // Do something that updates myDataProvider data.
    tl.invalidateList();

Conclusion

There are still some things that could be improved on.

It would be preferable that the hours could be entered only through the use of keyboard, or mouse. Now you have to use both, and that makes it less intuitive. And corrupted/invalid data could be entered as the input fields are not masked.

Using Flex as a RIA developer kit can be fun, especially for designer who already know a bit of HTML/CSS and Javascript. Creating a skinned application in Flex is easy to do but still takes more time time than using the original components.

Patrick de Klein & Jaap Vriend

, , ,

Nog geen reacties

Flex vs Java Deel 2: Effecten

Inleiding

Dat zowel met Flex als Java i.c.m Swing relatief makkelijk een simpel formuliertje te maken is, geloof ik wel. Ik wil me richten op geavanceerdere features die beide platforms te bieden hebben. Er valt hierbij te denken aan die sexy overgangs-effecten met transparantie, glijdende componenten, enzovoort.

In dit artikel wordt een uiteenzetting gegeven wat beide platforms bieden aan voorzieningen om een GUI echt sexy te maken.

Ten slotte geef ik een head-to-head vergelijking, waarbij identieke schermen en efffecten worden geïmplementeerd.

Effecten in Flex

Gezien het feit dat Flex uit de Flash-stal komt, zal het niemand verbazen dat ook Flex een rijk scala aan kant-en-klare visuele effecten biedt. Als we een blik in de mx.effects package werpen zien we onder andere: blur, fade, glow, rotate, diverse masking effecten, etc…

Net zoals alle Flex elementen, kunnen effecten imperatief dan wel declaratief gebruikt worden.
Voorbeeld van imperatief gebruik:

private var currentResize:Resize;

private function getWider():void {
	if (currentResize != null)
		currentResize.end();
	currentResize = new Resize(button);
	currentResize.widthBy = 100;
     currentResize.heightBy = 100;
	currentResize.play();
}
private function getNarrower():void {
	if (currentResize != null)
		currentResize.end();
	currentResize = new Resize(button);
	currentResize.widthBy = -100;
     currentResize.heightBy = -100;
	currentResize.play();
}
<mx:Button id="button" label="Click me!" mouseDown="getWider()" mouseUp="getNarrower()" />

En dezelfde functionaliteit, maar dan declaratief:

<mx:Resize id="wide" widthBy="100" heightBy="100" />
<mx:Resize id="narrow" widthBy="-100" heightBy="100" />
<mx:Button label="Click me" mouseDownEffect="wide" mouseUpEffect="narrow" />

Een leuk aspect van Flex effecten is dat ze ook parallel kunnen lopen. Neem het volgende voorbeeld:

<mx:Parallel id="ef" target="{button}" duration="3000">
  <mx:children>
    <mx:Blur blurXTo="0" blurYTo="0"/>
    <mx:Rotate angleFrom="0" angleTo="360" />
  </mx:children>
</mx:Parallel>
<mx:Button id="button" label="Click me!" click="ef.end();ef.play()" />

codebase="https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"
data="/roller/luminis/resource/flexvsjava/ParallelEffectExample.swf"
height="60"
type="application/x-shockwave-flash2-preview"
width="150" > quality="high" src="/roller/luminis/resource/flexvsjava/ParallelEffectExample.swf" type="application/x-shockwave-flash2-preview" width="150" height="60" />

Sequentiële afhandleing, d.w.z. achter elkaar, van effecten is ook mogelijk:

<mx:Sequence id="ef" target="{button}" duration="3000">
  <mx:children>
    <mx:Blur blurXTo="0" blurYTo="0"/>
    <mx:Rotate angleFrom="0" angleTo="360" />
  </mx:children>
</mx:Sequence>
<mx:Button id="button" label="Click me!" click="ef.end();ef.play()" />

codebase="https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"
data="/roller/luminis/resource/flexvsjava/SequentialEffectExample.swf"
height="60"
type="application/x-shockwave-flash2-preview"
width="150" > quality="high" src="/roller/luminis/resource/flexvsjava/SequentialEffectExample.swf" type="application/x-shockwave-flash2-preview" width="150" height="60" />

De tijdsduur dat een effect actief is kan ook gezet worden en wel met de duration property. Standaard duurt een effect 1 seconde (1000 milliseeconden). Een voorbeeld:

<mx:Rotate id="ef" angleFrom="0" angleTo="360" duration="3000"/>
<mx:Button id="button" label="Click me!" click="ef.end();ef.play()" />

Het laatste aspect dat ik hier ga behandelen ten aanzien van Flex effecten zijn de zogenaamde easing functions. Dit zijn functies die het verloop van de animatie bepalen, bijvoorbeeld of er een versnelling aan het begin en vertraging aan het einde van de animatie plaatsvindt. Het effect krijgt hierdoor een natuurlijker gevoel. In de package mx.effects.easing zijn diverse classes met easing functies aanwezig, maar er zelf een schrijven kan natuurlijk ook.

<mx:Script>
  <![CDATA[
    import mx.effects.easing.Cubic;
    import mx.effects.easing.Linear;
  ]]>
</mx:Script>

<mx:Rotate id="efNoEasing" target="{button1}" angleFrom="0" angleTo="360" duration="3000" easingFunction="{Linear.easeNone}"/>
<mx:Rotate id="efWithEasing" target="{button2}" angleFrom="0" angleTo="360" duration="3000" easingFunction="{Cubic.easeInOut}" />

	<mx:VBox paddingLeft="40" paddingTop="40">
		<mx:Button id="button1" label="Click me (no easing)!" click="efNoEasing.end();efNoEasing.play()" />
		<mx:Button id="button2" label="Click me (with easing)!" click="efWithEasing.end();efWithEasing.play()" />
</mx:VBox>

codebase="https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"
data="/roller/luminis/resource/flexvsjava/EffectEasingExample.swf"
height="150"
type="application/x-shockwave-flash2-preview"
width="250" > quality="high" src="/roller/luminis/resource/flexvsjava/EffectEasingExample.swf" type="application/x-shockwave-flash2-preview" width="250" height="150" />

Effecten in Java

Componenteffeten in Java zijn niet zo rechttoe rechtaan als in Flex. De voor de hand liggende reden hiervoor is dat de API’s van standaard componenten hiertoe geen mogelijkheden bieden. Dit wil niet zeggen dat het niet mogelijk is. Als je bijvoorbeeld kijkt naar de Java2D demo (https://jdk6.dev.java.net/Java2DApplet.html) zie je dat Java wel degelijk de mogelijkheid biedt om funky dingetjes te doen.

Hoe zit dit nou?

Java biedt een zeer uitgebreide, maar ook zeer complexe API voor 2-dimesionale beeldbewerking. Het gros hiervan is verspreid over de java.awt, java.awt.geom en java.awt.image packages. Deze API’s bieden de developer de mogelijkheid om complexe figuren en krommingen te tekenen, clips te definiëren, afbeeldings en overige graphics transformaties te maken, enz..

De uitdaging zit ‘m echter in de manier waarop deze effecten toegepast worden. Alle visuele component klassen hebben een gemeenschappelijke ouder: java.awt.Component. Iedere keer dat de AWT thread het nodig acht dat een component zichzelf opnieuw tekent, roept het de public void paint(Graphics) methode van dat component aan. Deze methode is vervolgens verantwoordelijk voor het tekenen van de staat van het component. Het Graphics object biedt de mogelijkheid om een heel scala aan graphische operaties uit te voeren, zoals tekenen van stukken tekst en basale vormen, en het zetten van kleuren.

In versie 1.2 van Java is de java.awt.Graphics2D klasse geïntroduceerd, die een extensie is van Graphics, en geavanceerdere functionaliteit biedt zoals vorm- en coördinatentransformaties, geometrie, transparantie en kleurenbeheer.

Een aspect aan effecten wat hier nog niet aan bod is gekomen, is die van tijd. Een effect is immers een animatie, maar de paint methode is verantwoordelijk voor het tekenen van een component op een specifiek moment in de tijd. Het gebruik van threads is hier dus noodzakelijk.

Willen we nou een arbitrair lijkend effect op een component toepassen, zullen we dus zijn paint(Graphics) methode moeten overriden en met behulp van een Thread in de loop van de animatie eigenschappen van het component wijzigen.

Laten we als voorbeeld een nemen een om as draaiende JLabel.
We beginnen met het schrijven van een JLabel subclass die te roteren is:

class RotatableLabel extends JLabel {

	private double rotation;

	public RotatableLabel(String s) {
		super(s);
	}

	public double getRotation() {
		return rotation;
	}

	public void setRotation(double r) {
		this.rotation = r;
		repaint();
	}

	public Dimension getPreferredSize() {
		Dimension d = super.getPreferredSize();
		return new Dimension(Math.max(d.width, d.height), Math.max(d.width,
				d.height));
	}

	public void paint(Graphics g) {
		Graphics2D g2 = (Graphics2D) g;

		AffineTransform tr = g2.getTransform();
		tr.rotate(this.rotation, getWidth() / 2.0, getHeight() / 2.0);

		g2.setTransform(tr);
		super.paint(g);
	}
}

In de paint methode wordt een zogenaamde rotate transform toegepast.
Vervolgens maken we een java.lang.Runnable waarin een instantie van RotatableLabel wordt geanimeerd:

class RotationAnimation implements Runnable {
	RotatableLabel m_labelToRotate;
	RotationAnimation(RotatableLabel labelToRotate) {
		m_labelToRotate = labelToRotate;
	}
	public void run() {
		double origRotation = m_labelToRotate.getRotation();
		int steps = 100;
		double step = Math.PI * 2 / 100;
		double curRotation = origRotation;
		for (int i = 0; i < steps; i++) {
			double newRotation = curRotation + step;
			if (newRotation > (Math.PI * 2)) {
				newRotation = newRotation - (Math.PI * 2);
			}
			m_labelToRotate.setRotation(newRotation);
			curRotation = newRotation;
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
			}
		}
		if (m_labelToRotate.getRotation() != origRotation) {
			m_labelToRotate.setRotation(origRotation);
		}
	}
}

Bij het bovenstaande voorbeeld moet opgemerkt worden dat de grootte van het geanimeerde label is aangepast (zie getPreferredSize()) om de animatie te accomoderen.

codebase="." archive="/roller/luminis/resource/flexvsjava/javavsflex.jar" width="200" height="200">

Een mooi alternatief voor het zelf ontwikkelen van effectenframework is de open source library AnimatedTransitions. Deze biedt een vijftal standaard effecten, en de mogelijkheid er zelf nog meerdere aan toe te voegen. Het centrale concept in deze library is een zogenaamde screen transition: effecten worden alleen toegepast indien er op het scherm noemenswaardige wijziging ten aanzien van de zichtbare componenten plaatsvindt.

Een klein voorbeeldje:

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import org.jdesktop.animation.transitions.ComponentState;
import org.jdesktop.animation.transitions.Effect;
import org.jdesktop.animation.transitions.EffectsManager;
import org.jdesktop.animation.transitions.ScreenTransition;
import org.jdesktop.animation.transitions.TransitionTarget;
import org.jdesktop.animation.transitions.EffectsManager.TransitionType;
import org.jdesktop.animation.transitions.effects.Move;

public class TransitionExample extends JPanel {

	boolean state;

	public TransitionExample() {
		setLayout(null);

		final JButton b = new JButton("Move me!");
		b.setBounds(50, 50, 150, 30);

		final JButton b2 = new JButton("Move me back!");
		b2.setBounds(100, 100, 150, 30);

		add(b);

		Effect effect = new Move(new ComponentState(b), new ComponentState(b2));
		EffectsManager.setEffect(b, effect, TransitionType.CHANGING);
		final ScreenTransition st = new ScreenTransition(this, new TransitionTarget() {
			public void setupNextScreen() {
				removeAll();
				if (state) {
					add(b2);
				} else {
					add(b);
				}

			}
		}, 1000);
		ActionListener listener = new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				st.start();
				state = !state;
			}
		};
		b.addActionListener(listener);
		b2.addActionListener(listener);
	}
}

codebase="." archive="/roller/luminis/resource/flexvsjava/javavsflex.jar,/roller/luminis/resource/flexvsjava/AnimatedTransitions-0.11.jar,/roller/luminis/resource/flexvsjava/TimingFramework-1.0.jar" width="300" height="200">

Een vergelijking in de praktijk

Om de vergelijking tussen Java en Flex wat meer te illustreren, zal ik een tweetal implementatie voorbeelden geven. Ten eerste een crossfading animatie, en vervolgens een component slide.

Om te beginnen met de crossfade. Een crossfade is een transitie van een scherm naar het andere in dezelfde ruimte. Het effect hierbij is dat geleidelijk de transparantie van het initiële component toeneemt en die van het volgende scherm afneemt. Hierbij krijg je een vloeiende overgang tussen de schermen.

Om te beginnen met de Flex implementatie.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="200" height="200">

    <mx:Boolean id="fadeToggle" />
    <mx:Fade id="fadeOut" duration="1000" alphaFrom="1.0" alphaTo="0.0"/>
    <mx:Fade id="fadeIn" duration="1000" alphaFrom="0.0" alphaTo="1.0"/>

	<mx:VBox height="100%" width="100%" paddingLeft="20" paddingTop="20" paddingRight="20" paddingBottom="20">
		<mx:Canvas width="100%" styleName="content">
			<mx:VBox id="box1" visible="{!fadeToggle}" hideEffect="{fadeOut}" showEffect="{fadeIn}" >
				<mx:Image x="45" y="52" source="@Embed(source='../assets/images/duke.gif')"/>
				<mx:Button label="Duke!!" enabled="false"/>
			</mx:VBox>
			<mx:VBox id="box2" visible="{fadeToggle}" hideEffect="{fadeOut}" showEffect="{fadeIn}">
				<mx:Image x="45" y="52" source="@Embed(source='../assets/images/penduke.gif')"/>
				<mx:CheckBox label="Duke with pen!!" enabled="false" selected="true"/>
			</mx:VBox>
		</mx:Canvas>
		<mx:Button click="fadeToggle=!fadeToggle" label="Cross Fade"/>
	</mx:VBox>

</mx:Application>



Hier worden twee containers, box1 en box2, gedeclareerd die in elkaar zullen overvloeien. Er zijn twee fade-effecten, fadeIn en fadeOut, die verantwoordelijk zijn voor de fading (transparantie).
Middels binding (fadeToggle) op het visiblity attribuut worden de containers zichtbaar en onzichtbaar gemaakt. De effecten worden geactiveerd middels de hideEffect en showEffect attributen van de containers.

codebase="https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"
data="/download/attachments/14812620/ButtonExample.swf"
height="200"
type="application/x-shockwave-flash2-preview"
width="200" > quality="high" src="/roller/luminis/resource/flexvsjava/CrossFadingExample.swf" type="application/x-shockwave-flash2-preview" width="200" />

Voor de Java implementatie dient er om te beginnen een subklasse van JPanel gemaakt te worden:

public class FadeablePanel extends JPanel {
	private float m_opacity;

	public FadeablePanel(float initialOpacity) {
		super();
		setOpaque(false);
		m_opacity = initialOpacity;
	}

	/**
	 * Overridden to set the opaqueness of <code>comp</code> to false
	 */
	protected void addImpl(Component comp, Object constraints, int index) {
		if (comp instanceof JComponent) {
			((JComponent)comp).setOpaque(false);
		}
		super.addImpl(comp, constraints, index);
	}

	/**
	 *
	 * @return
	 */
	public float getOpacity() {
		return m_opacity;
	}

	/**
	 *
	 * @param f
	 */
	public void setOpacity(float f) {
		if (f >= 0.0 && f <= 1.0) {
			m_opacity = f;
			repaint();
		}
	}

	/**
	 * Overridden to set the opacity to the level set using {@link #setOpacity(float)}.
	 */
	public void paintComponent(Graphics g) {
		Graphics2D g2 = (Graphics2D) g;
		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
				m_opacity));
		super.paintComponent(g);
	}

}

Deze klasse herimplementeert de paint methode om de gezette transparantie te tekenen. Vervolgens de implementatie van het animatie effect voor een enkel component:

public class FadeEffect {
	private static final int SLEEP_TIME = 10;
	private float m_fromAlpha;
	private float m_toAlpha;
	private int m_duration;

	public FadeEffect(float fromAlpha, float toAlpha, int duration) {
		if (fromAlpha < 0.0 || fromAlpha > 1.0)
			throw new IllegalArgumentException("From alpha value must be between 0.0 and 1.0");
		if (toAlpha < 0.0 || toAlpha > 1.0)
			throw new IllegalArgumentException("From alpha value must be between 0.0 and 1.0");
		m_fromAlpha = fromAlpha;
		m_toAlpha = toAlpha;
		m_duration = duration;
	}

	public void apply(final FadeablePanel componentToFade) {
		new SwingWorker<Object, Object>() {
			protected Object doInBackground() throws Exception {
				int numSteps = m_duration / SLEEP_TIME;
				float stepAmount = (m_toAlpha - m_fromAlpha) / (float)numSteps;
				float currentOpacity = componentToFade.getOpacity();
				for (int i = 0; i < numSteps; i++) {
					float newOpacity = currentOpacity + stepAmount;
					componentToFade.setOpacity(newOpacity);
					currentOpacity = newOpacity;
					try {
						Thread.sleep(SLEEP_TIME);
					} catch (InterruptedException e) {}
				}
				if (componentToFade.getOpacity() != m_toAlpha) {
					componentToFade.setOpacity(m_toAlpha);
				}
				return null;
			}
		}.execute();
	}
}

Hier wordt bij de constructor de start- en eindtransparantie en de duur van de animatie opgegeven. De apply methode start de animatie, die middels een background thread wordt gedaan. Voor de echte crossfade een voorbeeld paneltje:

public class CrossFadeExamplePanel extends JPanel {

	private static final FadeEffect FADE_IN = new FadeEffect(0.0f, 1.0f, 500);
	private static final FadeEffect FADE_OUT = new FadeEffect(1.0f, 0.0f, 500);
	private FadeablePanel panel1;
	private FadeablePanel panel2; 

	public CrossFadeExamplePanel() {
		super(new BorderLayout());

		JLabel label1;
		JLabel label2;
		try {
			ImageIcon icon1 = new ImageIcon(getClass().getResource("duke.gif"));
			ImageIcon icon2 = new ImageIcon(getClass().getResource(
					"penduke.gif"));
			label1 = new JLabel(icon1);
			label2 = new JLabel(icon2);

		} catch (Exception e) {
			label1 = new JLabel("Could not load image duke.gif");
			label2 = new JLabel("Could not load image penduke.gif");
		}

		panel1 = new FadeablePanel(1.0f);
		panel1.setLayout(new BorderLayout());
		panel1.add(label1, BorderLayout.CENTER);

		panel2 = new FadeablePanel(0.0f);
		panel2.setLayout(new BorderLayout());
		panel2.add(label2, BorderLayout.CENTER);

		JPanel content = new JPanel();
		content.setLayout(new OverlayLayout(content));
		content.add(panel1);
		content.add(panel2);

		add(content, BorderLayout.CENTER);

		JButton fadeButton = new JButton(new FadeAction());

		JPanel buttonPane = new JPanel(new FlowLayout(FlowLayout.RIGHT));
		buttonPane.add(fadeButton);

		add(buttonPane, BorderLayout.SOUTH);
	}

	private class FadeAction extends AbstractAction {

		FadeAction() {
			super("Cross fade");
		}

		public void actionPerformed(ActionEvent e) {
			if (panel1.getOpacity() == 1.0f) {
				FADE_IN.apply(panel2);
				FADE_OUT.apply(panel1);
			} else {
				FADE_IN.apply(panel1);
				FADE_OUT.apply(panel2);
			}
		}

	}
}

Hier worden twee componenten over elkaar geplaatst, en middels de twee gedeclareerde fade-effecten wordt de crossfade bereikt.

codebase="." archive="/roller/luminis/resource/flexvsjava/javavsflex.jar" width="400" height="250">

Het volgende voorbeeld betreft een een component in twee delen waarbij het onderste gedeelte omhoog en omlaag ‘glijdt’, vaak ook wel het ‘accordion’ effect genoemd. Het onderste gedeelte heeft een knop in breedte die het uit- en invouwen initiëert.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="378" height="528" applicationComplete="init()">
	<mx:Style>
		Text {
			padding-bottom: 10;
			padding-left: 10;
			padding-right: 10;
			padding-top: 10;
		}
	</mx:Style>
	<mx:Script>
		<![CDATA[
			import mx.effects.Resize;
			import mx.effects.Effect;
			import mx.events.EffectEvent;

			private var targetHeight:int = 100;
			private var collapsed:Boolean = false;

			private function init():void {
				collapse.addEventListener(EffectEvent.EFFECT_END,
					function():void
					{
						collapsed = true;
						header.label = "Click to expand";
					}
				);
				expand.addEventListener(EffectEvent.EFFECT_END,
					function():void
					{
						collapsed = false;
						header.label = "Click to collapse";
					}
				);
			}

			private function doCollapse():void {
				var resize:Effect;
				if (collapsed) {
					resize = expand;
				} else {
					resize = collapse;
				}
				resize.play();
			}
		]]>
	</mx:Script>

	<mx:Parallel id="collapse">
		<mx:Resize target="{panel1}" heightBy="100" />
		<mx:Resize target="{panel2}" heightBy="-100" />
		<mx:Move target="{panel2}" yBy="100"/>
	</mx:Parallel>
	<mx:Parallel id="expand">
		<mx:Resize target="{panel1}" heightBy="-100" />
		<mx:Resize target="{panel2}" heightBy="100" />
		<mx:Move target="{panel2}" yBy="-100"/>
	</mx:Parallel>

	<mx:Canvas x="56" y="40" width="250" height="420"
		horizontalScrollPolicy="off" verticalScrollPolicy="off" borderStyle="solid" cornerRadius="2">

		<mx:VBox id="panel1" width="100%" height="66%" top="0" verticalScrollPolicy="off">
				<mx:Text width="100%" height="100%">
					<mx:text>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</mx:text>
				</mx:Text>	

		</mx:VBox>
		<mx:VBox id="panel2" width="100%" height="34%" bottom="0" verticalScrollPolicy="off">
			<mx:Button id="header" label="Click to collapse" width="247" click="doCollapse()"/>
				<mx:Text width="100%" height="100%">
					<mx:text>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</mx:text>
				</mx:Text>
		</mx:VBox>
	</mx:Canvas>

</mx:Application>

De truc zit hem voornamelijk in de collapse en expand effecten; het collapse effect vergroot het bovenste en onderste gedeelte, en verplaatst het onderste naar beneden. Het expand effect doet hetzelfde, maar met inverse waarden.

De Flex versie:

codebase="https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"
data="/download/attachments/14812620/SlideEffectExample.swf"
height="400"
type="application/x-shockwave-flash2-preview"
width="250" > quality="high" src="/roller/luminis/resource/flexvsjava/SlideEffectExample.swf" type="application/x-shockwave-flash2-preview" width="250" height="400" />

Vervolgens de Java implementatie:

public class SlideExamplePanel extends JPanel {
	private static final String TEXT_1 = "Lorem ipsum dolor sit amet, " +
			"consectetur adipisicing elit, " +
			"sed do eiusmod tempor incididunt ut labore et " +
			"dolore magna aliqua. Ut enim ad minim veniam, " +
			"quis nostrud exercitation ullamco laboris nisi " +
			"ut aliquip ex ea commodo consequat. Duis aute " +
			"irure dolor in reprehenderit in voluptate velit " +
			"esse cillum dolore eu fugiat nulla pariatur. " +
			"Excepteur sint occaecat cupidatat non proident, " +
			"sunt in culpa qui officia deserunt mollit anim " +
			"id est laborum.";
	private static final String TEXT_2 = "Lorem ipsum dolor sit amet, " +
	"consectetur adipisicing elit";

	private static final int TOP_HEIGHT_EXPANDED = 300;
	private static final int BOTTOM_HEIGHT_EXPANDED = 100;
	private boolean collapsed = false;
	private JPanel topPanel;

	private JPanel bottomPanel;
	private JButton resizeButton;

	/**
	 * Create the panel.
	 */
	public SlideExamplePanel() {
		super();

		topPanel = new JPanel(new GridLayout());
		JTextPane textPane1 = new JTextPane();
		textPane1.setContentType("text/html");
		textPane1.setText(TEXT_1);
		textPane1.setEditable(false);
		textPane1.setBackground(topPanel.getBackground());
		topPanel.add(textPane1, BorderLayout.CENTER);

		bottomPanel = new JPanel(new BorderLayout());
		resizeButton = new JButton(new ResizeAction());
		resizeButton.setMargin(new Insets(5, 5, 5, 5));

		bottomPanel.add(resizeButton, BorderLayout.NORTH);
		JTextPane textPane2 = new JTextPane();
		textPane2.setContentType("text/html");
		textPane2.setText(TEXT_2);
		textPane2.setEditable(false);
		textPane2.setBackground(bottomPanel.getBackground());
		bottomPanel.add(textPane2, BorderLayout.CENTER);

		setLayout(null);

		topPanel.setBounds(0, 0, 300, TOP_HEIGHT_EXPANDED);
		bottomPanel.setBounds(0, 300, 300, BOTTOM_HEIGHT_EXPANDED);
		add(topPanel);
		add(bottomPanel);

		addComponentListener(new ComponentAdapter() {
			public void componentResized(ComponentEvent e) {
				Dimension size = getSize();
				int newTopPanelHeight =  (int)(size.getHeight() - bottomPanel.getHeight());
				int newBottomPanelHeight = bottomPanel.getHeight();
				int newBottomPanelY = newTopPanelHeight; 

				topPanel.setSize((int)size.getWidth(), newTopPanelHeight);
				bottomPanel.setBounds(0, newBottomPanelY, (int)size.getWidth(), newBottomPanelHeight);
			}
		});
	}

	/**
	 *
	 *
	 */
	private class ResizeAction extends AbstractAction {
		ResizeAction() {
			super("Collapse");
		}
		public void actionPerformed(ActionEvent e) {
			int currentHeight = getHeight();
			int prefBottomHeaderHeight = (int)resizeButton.getSize().getHeight();

			final int newTopHeight;
			if (collapsed) {
				newTopHeight = currentHeight - BOTTOM_HEIGHT_EXPANDED;
			} else {
				newTopHeight = (currentHeight - prefBottomHeaderHeight);
			}
			// Animation thread
			new Thread(new Runnable() {
				public void run() {
					int increment = collapsed ? -3 : 3;
					while (((int)topPanel.getHeight()) != newTopHeight) {

						topPanel.setSize(topPanel.getWidth(), topPanel.getHeight() + increment);
						bottomPanel.setBounds(0, bottomPanel.getY() + increment, bottomPanel.getWidth(), bottomPanel.getHeight() + increment);

						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {}
					}
					collapsed = !collapsed;
					putValue(NAME, collapsed ? "Expand" : "Collapse");
				}
			}).start();
		}

	}
}

In de klasse-constructor wordt dezelfde componenten hiërarchie opgebouwd als in de Flex applicatie. In de ResizeAction klasse wordt de collapse/expand logica uitgevoerd. In deze implementatie wordt gebruikt gemaakt van de void setBounds(int,int,int,int) methode van Component, om het effect te bereiken, op in principe dezelfde methode als in de eerder getoonde Flex applicatie. Door in een Thread de setBounds methode met aangepaste waarden aan te roepen wordt er een animatie van gemaakt.

codebase="." archive="/roller/luminis/resource/flexvsjava/javavsflex.jar" width="400" height="400">

Conclusie

Door naar de bovenstaande voorbeelden te kijken wordt een al snel duidelijk dat beide platforms een rijk scala aan component effecten mogelijk maakt.

Een aspect springt echter meteen in het oog: de mate van complexiteit die vereist is bij de Java implementaties om een met Flex vergelijkbaar resultaat te krijgen. Het is allemaal wel mogelijk, maar het vraagt aardig wat inspanning van de ontwikkelaar.

Nu zijn er wel een hoop libraries beschikbaar die ‘t leven van een Java-ontwikkelaar een stuk makkelijker maken, maar feit is wel dat de Swing/AWT API’s duidelijk niet zijn geschreven met effect-rijke GUI in het oog.

Java sources: Download

,

Nog geen reacties

Flex vs Java Deel 1: Stijlen

<h2>Inleiding</h2> <p> De veronderstelling is dat je met Flex sneller een gelikte UI in elkaar kunt zetten, maar is dat ook echt zo? Misschien voor een simpel “hello world” appje, maar hoe zit het met complexere UI’s met “custom” vormgeving? </p> <p> In dit eerste deel van de vergelijking tussen Java en Flex zullen we ons richten op de mogelijkheden die Java i.c.m. de Synth LAF en Flex bieden om componenten te skinnen. </p> <h2>Skinnen in Flex</h2> <p> In Flex kan skinnen van componenten op twee manieren: direct in de MXML declaratie van een component of in aparte stylesheets. Skinnen met behulp van stylesheets heeft als voordeel herbruikbaarheid, en wordt daarom ook gezien als best practice.<br/> Stylesheets in Flex lijken verdacht veel op de <a href=”http://www.w3.org/Style/CSS/”>W3C CSS</a>; niet ontoevallig dat Adobe ze ook bij die naam noemt.<br/> Er zijn echter wat aspecten die afwijken van de W3C standaard, en specifiek met betrekking tot selectors: <ul> <li>Geen ondersteuning voor ID selectors. Hiermee kan je in CSS een selector voor een specifiek element declareren, b.v. <code>#pageTitle</code></li> <li>Geen ondersteuning voor nesting selectors. Hiermee kan je alle elementen binnen een ander element selecteren. Voorbeeld: <code>ul a</code>, alle links binnen een unordered list</li> <li>Geen ondersteuning voor pseudo-classes. Meest bekende pseudo-selector is <code>a:hover</code>, waarmee je een de stilering van een link waar de muispointer op ligt kan definiëren</li> </ul> </p> <p> Laten we om te beginnen eens kijken hoe het skinnen werkt als je dat direct op in de component declaratie doet. Als voorbeeld maken we een knop: <pre> &lt;mx:Button label=”Click Me” fontFamily=”Georgia” fontSize=”20″ fontStyle=”italic” textDecoration=”underline” color=”#0000FF”/&gt; </pre> Zoals eerder vermeld, om alle stileringsinformatie op de componenten zelf te plaatsen zou nogal wat werk kosten. Welnu, een klein voorbeeld van hoe dezelfde skin er in Flex CSS uitziet: <pre> Button { color: #0000FF; fontFamily: Georgia; fontSize: 20; fontStyle: italic; textDecoration: underline; } </pre> </p> <p> <div><object classid=”clsid:D27CDB6E-AE6D-11cf-96B8-444553540000″ codebase=”https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0″ data=”/roller/luminis/resource/flexvsjava/ButtonExample.swf” height=”60″ type=”application/x-shockwave-flash2-preview” width=”200″ ><param name=”data” value=”/roller/luminis/resource/flexvsjava/ButtonExample.swf”/><param name=”loop” value=”false”/><param name=”menu” value=”false”/><param name=”movie” value=”/roller/luminis/resource/flexvsjava/ButtonExample.swf”/><param name=”quality” value=”high”/><param name=”scale” value=”exactfit”/><param name=”src” value=”/roller/luminis/resource/flexvsjava/ButtonExample.swf”/><param name=”type” value=”application/x-shockwave-flash2-preview”/><embed height=”60″ pluginspage=”https://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash” quality=”high” src=”/roller/luminis/resource/flexvsjava/ButtonExample.swf” type=”application/x-shockwave-flash2-preview” width=”200″ /></object></div> </p> <p> Voor diegenen die vaker met HTML en CSS werken zal het redelijk bekend voorkomen. Nu een wat geavanceerder voorbeeld: <pre> Button { color: #0080c0; font-size: 12; corner-radius: 10; border-color: #006699; border-thickness: 3; fill-colors: #FCFFF4, #006699; fill-alphas: 0.5, 0.5; text-roll-over-color: #FFFFFF; } </pre> </p> <p> <div><object classid=”clsid:D27CDB6E-AE6D-11cf-96B8-444553540000″ codebase=”https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0″ data=”/roller/luminis/resource/flexvsjava/AdvancedButtonExample.swf” height=”60″ type=”application/x-shockwave-flash2-preview” width=”150″ ><param name=”data” value=”/roller/luminis/resource/flexvsjava/AdvancedButtonExample.swf”/><param name=”loop” value=”false”/><param name=”menu” value=”false”/><param name=”movie” value=”/roller/luminis/resource/flexvsjava/AdvancedButtonExample.swf”/><param name=”quality” value=”high”/><param name=”scale” value=”exactfit”/><param name=”src” value=”/roller/luminis/resource/flexvsjava/AdvancedButtonExample.swf”/><param name=”type” value=”application/x-shockwave-flash2-preview”/><embed height=”60″ pluginspage=”https://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash” quality=”high” src=”/roller/luminis/resource/flexvsjava/AdvancedButtonExample.swf” type=”application/x-shockwave-flash2-preview” width=”150″ /></object></div> </p> <p> Hier wordt al meteen duidelijk dat Flex een rijk scala aan stijl- en effectattributen biedt, o.a. voor hoekafrondingen, meerdere kleuren van borders, gradients, transparantie. Tevens is hier te zien dat het ontbreken van pseudo-selectors wordt opgevangen door attributen als <code>overFillColors</code>, <code>selectedFillColorRatios</code>, enz. </p> <p> Toewijzen van stijlen aan componenten in Flex kan op de volgende manieren: <ul> <li>Door middel van type selectors: de naam van de componentklasse wordt als selector gedefinieerd.</li> <li>Door middel van class selectors: een component een stijlnaam te geven, die vervolgens als selector in de style sheet te declareren.</li> </ul> Overigens zijn combinaties van bovenstaande methodes ook mogelijk, hierover later meer. </p> <h3>Type selectors</h3> <p> Indien we voor een componentklasse een stijl willen definieren, hoeven we slechts zijn naam als CSS selector met de bijbehorende stijlattributen te declareren: <pre> Button { color: red; cornerRadius: 12; textSize: 12; } Label { color: black; } </pre> </p> <p> Een leuk aspect is de manier waarop componentovererving is verweven met stijldefinities. Nemen we als voorbeeld de <code>Text</code> klasse: deze erft van <code>Label</code>, en erft ook alle gedefinieerde stijlelementen van <code>Label</code>. <pre> Label { color: red; cornerRadius: 12; textSize: 12; } Text { color: black; } </pre> </p> <h3>Class selectors</h3> <p> De andere manier om stijlkoppelingen te bewerkstelligen is een stijl direct toewijzen een component, d.m.v. een zogenaamde class selector.<br/> Neem het volgende code fragment: <pre> .myButton { color: red; textSize: 12; } </pre> <pre> &lt;mx:Button styleName=”myButton” label=”OK”/&gt; </pre> </p> <p> Zoals eerder vermeld, zijn type selectors en class selectors ook te combineren. Type selectors worden toegepast op alle instanties van een component klasse, maar daarnaast kan een instantie van een component klasse ook een eigen stijl selecteren. De gedeclareerde attributen in deze stijl zullen alle attributen overschrijven die in de type selector staan. Als voorbeeld: <pre> Button { color: yellow; textSize: 12; } .myButton { color: red; } </pre> <pre> &lt;mx:Button label=”Cancel”/&gt; &lt;!– Gele tekst –&gt; &lt;mx:Button styleName=”myButton” label=”OK”/&gt; &lt;!– Rode tekst –&gt; </pre> </p> <h2>Java Synth LAF</h2> <p> De Java Synth LAF is, zoals de naam al zegt, een Pluggable Look & Feel implementatie voor Swing. Dit wil ook zeggen dat Synth niet per definitie als skin voor een applicatie wordt gebruikt, maar dat deze, zoals alle Swing Look and Feels, eerst geïnstalleerd moet worden. <pre> URL file = …; // De URL naar de Synth style file SynthLookAndFeel synth = new SynthLookAndFeel(); try { synth.load(file); UIManager.setLookAndFeel(synth); } catch (Exception e) { e.printStackTrace(); } </pre> </p> <p> Een van de dingen die al meteen opvallen is dat Synth LAF een referentie naar een bestand nodig heeft. Dit is een bestand waar alle stijlen zijn gedefiniëerd; Synth heeft namelijk geen ‘default’ skin, dus de aanweizigheid van dit bestand is vereist. </p> <p> Wat staat er nou in zo’n Synth stijl bestand? Hier een klein voorbeeld: <pre> &lt;synth&gt; &lt;color id=”fg_color” value=”#00364e”/&gt; &lt;color id=”bg_color” value=”#0f79a9″/&gt; &lt;font id=”std_font” name=”Arial” size=”12″ style=”PLAIN”/&gt; &lt;style id=”default”&gt; &lt;font idref=”std_font/&gt; &lt;state&gt; &lt;color idref=”fg_color” type=”FOREGROUND”/&gt; &lt;color idref=”bg_color” type=”BACKGROUND”/&gt; &lt;/state&gt; &lt;/style&gt; &lt;bind style=”default” type=”region” key=”.*”/&gt; &lt;!– … overige stijlen… –&gt; &lt;/synth&gt; </pre> Dit is een voorbeeld van een Synth Style bestand in zijn meest kale vorm: er wordt alleen een standaard stijl gegeven die geldt voor alle componenten. Aan deze stijl wordt gekoppeld een voor- en achtergrondkleur en een font.<br/> Een interessant detail aan dit voorbeeld is dat het illustreert hoe stijlelementen, zoals kleuren en lettertypen, herbruikt kunnen worden. <br/> De functie van het bind element behandelen we verderop. </p> <p> Nu naar een wat ingewikkelder component, een button: <pre> &lt;style id=”defaultbutton”&gt; &lt;state id=”def”&gt; &lt;imagePainter method=”buttonBackground” path=”button_up.png” sourceInsets=”8 8 8 8″ paintCenter=”true” /&gt; &lt;insets top=”8″ left=”8″ bottom=”8″ right=”8″ /&gt; &lt;font name=”Arial” size=”12″ style=”BOLD” /&gt; &lt;color value=”#000000″ type=”TEXT_FOREGROUND” /&gt; &lt;/state&gt; &lt;state value=”PRESSED”&gt; &lt;imagePainter method=”buttonBackground” path=”button_pressed.png” sourceInsets=”8 8 8 8″ paintCenter=”true” /&gt; &lt;insets top=”8″ left=”8″ bottom=”8″ right=”8″ /&gt; &lt;/state&gt; &lt;state value=”MOUSE_OVER”&gt; &lt;imagePainter method=”buttonBackground” path=”button_over.png” sourceInsets=”8 8 8 8″ paintCenter=”true” /&gt; &lt;insets top=”8″ left=”8″ bottom=”8″ right=”8″ /&gt; &lt;/state&gt; &lt;state value=”DISABLED” clone=”def”&gt; &lt;color value=”#999999″ type=”TEXT_FOREGROUND” /&gt; &lt;/state&gt; &lt;/style&gt; &lt;bind style=”defaultbutton” type=”region” key=”BUTTON”/&gt; </pre> </p> <p> <applet code=”ButtonStyleExample.class” codebase=”.” archive=”/roller/luminis/resource/flexvsjava/flexvsjavastyles.jar” width=”300″ height=”100″></applet> </p> <p> In dit voorbeeld zijn eigenlijk al bijna alle kernconcepten van de Synth Style vervat. Een Synth Style bestand bevat een of meerdere style declaraties, die een unieke identificatie hebben (het id attribuut). Een style slaat in de praktijk op de skin van een specifieke componentklasse, in dit voorbeeld buttons. </p> <p> Binnen een style worden een of meerdere states gedefiniëerd. Een state is ook letterlijk de toestand waarin het component zich bevindt. In dit voorbeeld zijn een viertal states gedefinieerd: de standaard wanneer er niets bijzonders met het component gebeurt, wanneer hij ingedrukt wordt (<code>PRESSED</code>), wanneer hij disabled is (<code>DISABLED</code>) en wanneer de mousepointer er over zweeft (<code>MOUSE_OVER</code>). Overige beschikbare states zijn: <code>ENABLED</code>, <code>FOCUSSED</code>, <code>SELECTED</code>, <code>DEFAULT</code>. </p> <p> Een state bevat de stijlattributen van een component in een specifieke toestand. DIt kunnen zijn kleuren voor de voor- of achtergrond (het <code>color</code> element), lettertypes voor tekst (het <code>font</code> element), afbeeldingen (het <code>imagePainter</code> element), en de grensruimte van een component (het <code>insets</code> element). </p> <p> Overerving van stijlen is ook mogelijk, en wel door middel van het clone attribuut binnen het style element. <pre> &lt;style id=”textfield”&gt; &lt;state&gt; &lt;color value=”yellow” type=”BACKGROUND”/&gt; &lt;color value=”blue” type=”TEXT_FOREGROUND”/&gt; &lt;font name=”Arial” size=”14″ style=”BOLD” /&gt; &lt;insets top=”4″ left=”4″ bottom=”4″ right=”4″ /&gt; &lt;/state&gt; &lt;/style&gt; &lt;style id=”passwordfield” clone=”textfield”&gt; &lt;state&gt; &lt;color value=”orange” type=”BACKGROUND”/&gt; &lt;color value=”black” type=”TEXT_FOREGROUND”/&gt; &lt;/state&gt; &lt;/style&gt; &lt;bind style=”textfield” type=”region” key=”TextField”/&gt; &lt;bind style=”passwordfield” type=”region” key=”PasswordField”/&gt; </pre> </p> <p> <applet code=”TextFieldStyleExample.class” codebase=”.” archive=”/download/attachments/14812620/flexvsjavastyles.jar” width=”300″ height=”100″></applet> </p> <p> In Java Synth wordt een stijl gekoppeld aan een component door middel van het bind element. Dit element biedt de mogelijkheid om een stijl te binden aan een component. Het bind element heeft 3 attributen die van belang zijn: <ul> <li><code>style</code>: de referentie naar een elders gedeclareerde stijl</li> <li><code>type</code>: de soort koppeling, d.w.z. generieke of specifieke koppeling</li> <li><code>key</code>: de sleutel van de koppeling, d.w.z. de naam van een component of de naam van de klasse.</li> </ul> </p> <p> Beginnend met de declaratie van een stijl: <pre> &lt;style id=”buttonStyle”&gt;…&lt;/style&gt; </pre> Een bind element moet in ieder geval een referentie hebben naar de gedeclareerde stijl, in dit geval <code>buttonStyle</code>: <pre> &lt;bind style=”buttonStyle” … /&gt; </pre> Als voorbeeld willen we alle knoppen in onze applicatie binden aan de stijl <code>buttonStyle</code>. Dit doen we door als type attribuut ‘region’ op te geven, en als key attribuut een van de constanten uit de <code>javax.swing.plaf.synth.Region</code> klasse te gebruiken, in dit geval de waarde van <code>javax.swing.plaf.synth.Region.BUTTON</code>: <pre> &lt;bind style=”buttonStyle” type=”region” key=”Button” /&gt; </pre> </p> <p> Om een enkel component een afwijkende stijl te geven is wat meer werk nodig. Nu vertellen we de binding dat het moet kijken naar componenten die een specifieke naam hebben. Als voorbeeld maken we een binding die de stijl <code>buttonStyle</code> koppelt aan componenten met naam <code>XXX</code>: <pre> &lt;bind style=”buttonStyle” type=”name” key=”XXX” /&gt; </pre> Met deze binding hebben we in feite nog niets gekoppeld. Dit gebeurt pas als er een component wordt aangemaakt die <code>XXX</code> als naam heeft: <pre> JButton b = new JButton(”Klik mij”); b.setName(”XXX”); </pre> </p> <p> Een leuke feature van binden is dat er ook reguliere expressies gebruikt kunnen worden. Bijvoorbeeld: <pre> &lt;bind style=”knopStijl” type=”name” key=”knop.*” /&gt; </pre> Dit bindt alle componenten wiens naam begint met <code>knop</code> aan de stijl <code>knopStijl</code>. </p> <p> Een ander aspect van stijl koppelingen in Synth is dat er meerdere stijlen gebonden kunnen worden aan componentent. Neem de volgende stijldeclaraties en bindings: <pre> &lt;style id=”Globaal”&gt; &lt;state&gt; &lt;color value=”blue” type=”FOREGROUND”/&gt; &lt;/state&gt; &lt;/style&gt; &lt;style id=”Knop”&gt; &lt;state&gt; &lt;color value=”orange” type=”FOREGROUND”/&gt; &lt;/state&gt; &lt;/style&gt; &lt;style id=”SpecialeKnop”&gt; &lt;state&gt; &lt;color value=”red” type=”BACKGROUND”/&gt; &lt;/state&gt; &lt;/style&gt; &lt;bind style=”Globaal” type=”region” name=”*”/&gt; &lt;bind style=”Knop” type=”region” name=”Button”/&gt; &lt;bind style=”SpecialeKnop” type=”name” name=”speciale_knop.*”/&gt; </pre> Bovenstaande code geeft alle componenten een gele tekstkleur, behalve knoppen die een blauwe tekstkleur krijgen. Indien de naam van een knop (die reeds de <code>Knop</code> stijl heeft) begint met <code>speciale_knop</code>, zal deze ook de stijl <code>SpecialeKnop</code> toegewezen krijgen, wat inhoudt dat de achtergrond rood wordt. </p> <h2>Een vergelijking in de praktijk</h2> <p> Welnu, een praktijk voorbeeldje. We stellen ons als opdracht: het maken van een button met afgeronde hoeken en een achtergrond met een verticale 2-kleuren gradient (wit en groen). Tevens heeft de button een mouse-over effect waarbij de tweede gradient kleur wijzigt naar donkergroen. </p> <h3>Implementatie in Flex</h3> <p> In Flex is dit een fluitje van een cent. Om te beginnen met de ronde hoeken, welke te verwezenlijken zijn met het ‘corner-radius’ stijl attribuut: <pre> Button { cornerRadius: 12; /* De mate van afronding */ } </pre> Vervolgens de gradient, waar een tweetal stijattributen van belang zijn: fillColors en fillColorRatios. Het eerst attribuut definieert de kleuren, het tweede de relatieve posities (op een schaal van 0 tot 255) waar maximale verzadiging is. <pre> Button { cornerRadius: 12; /* De mate van afronding */ fillColors: #FFFFFF, #00FF00; /* wit, groen */ fillColorRatios: 0, 255; } </pre> Ten slotte het mouse-over effect, waarbij de overFillColors en overFillColorRatios worden gebruikt. <pre> Button { cornerRadius: 12; /* De waarde geeft de mate van hoekafronding */ fillColors: #FFFFFF, #33FF00; /* wit, groen */ fillColorRatios: 0, 255; overFillColors: #FFFFFF, #00CC00; /* wit, donkergroen */ overFillColorRatios: 0, 255; } </pre> </p> <p> <div><object classid=”clsid:D27CDB6E-AE6D-11cf-96B8-444553540000″ codebase=”https://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0″ data=”/roller/luminis/resource/flexvsjava/ButtonGradientExample.swf” height=”60″ type=”application/x-shockwave-flash2-preview” width=”150″ ><param name=”data” value=”/roller/luminis/resource/flexvsjava/ButtonGradientExample.swf”/><param name=”loop” value=”false”/><param name=”menu” value=”false”/><param name=”movie” value=”/roller/luminis/resource/flexvsjava/ButtonGradientExample.swf”/><param name=”quality” value=”high”/><param name=”scale” value=”exactfit”/><param name=”src” value=”/roller/luminis/resource/flexvsjava/ButtonGradientExample.swf”/><param name=”type” value=”application/x-shockwave-flash2-preview”/><embed height=”60″ pluginspage=”https://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash” quality=”high” src=”/roller/luminis/resource/flexvsjava/ButtonGradientExample.swf” type=”application/x-shockwave-flash2-preview” width=”150″ /></object></div> </p> <h3>Implementatie in Java Synth LAF</h3> Hier lopen we al meteen tegen 2 issues aan: Synth LAF heeft geen ondersteuning voor afgeronde hoeken in componenten (de zogenaamde corner radius) en gradients. In principe is een gradient achtergrond wel te maken, maar dit kost wat werk. Eerst moeten we een painter coderen die gradients kan maken: <pre> public class GradientPainter2 extends SynthPainter { public void paintTextFieldBackground(SynthContext context, Graphics g, int x, int y, int w, int h) { GradientPaint gp = new GradientPaint((float)(x + (w/2)), (float)y, Color.WHITE, (float)(x + (w/2)), (float)(y + h), new Color(0, 200, 0)); Graphics2D g2 = (Graphics2D)g; g2.setPaint(gp); g2.fillRect(x, y, w, h); g2.setPaint(null); } } </pre> Vervolgens declareren we deze in de Synth configuratie als een object, en maken een painter op basis van dit object: <pre> &lt;object id=”gradient”/&gt; &lt;style id=”button”&gt; &lt;painter method=”buttonBackground” idref=”gradient”/&gt; &lt;/style&gt; </pre> Hiermee hebben we helaas nog geen afgeronde hoeken. Een andere taktiek is dus vereist. De simpelste methode is het maken van achtergrondafbeeldingen die de gradient en afgeronde hoeken al hebben. Deze afbeeldingen worden door Synth geschaald op de grootte van de button <pre> &lt;style id=”button”&gt; &lt;state&gt; &lt;imagePainter method=”buttonBackground” path=”buttonUp.png” sourceInsets=”8 8 8 8″ paintCenter=”true”/&gt; &lt;/state&gt; &lt;state value=”MOUSE_OVER”&gt; &lt;imagePainter method=”buttonBackground” path=”buttonDown.png” sourceInsets=”8 8 8 8″ paintCenter=”true”/&gt; &lt;/state&gt; &lt;/style&gt; </pre> Interessant om te vermelden is dat de bovenstaande methode met achtergrondafbeeldingen ook in Flex toegepast kan worden: <pre> Button { up-skin: Embed(source=”buttonUp.png”); over-skin: Embed(source=”buttonDown.png”); } </pre> <h2>Conclusie</h2> <p> Als we de twee implementaties nu vergelijken, valt meteen op dat Synth erg afhankelijk is van externe graphics wil er een beetje leuk resultaat uit komen. Dit heeft tot gevolg dat er een grote afhankelijkheid op een designer is, en dat de grootte van de applicatie aanzienelijk toeneemt door alle extra graphics die erbij komen. </p> <p> Flex daarentegen biedt een developer wat meer ruimte en mogelijkheden om in de loop van een ontwikkeltraject zelf nog aanpassingen te kunnen doen. Hierbij moet opgemerkt worden dat bij ieder nadeel weer en voordeel zit: indien er voor wordt gekozen om de skin volledig met externe graphics op te bouwen, vereist het minder vaardigheden van de designer, die zich niet bezig hoeft te houden met MXML en/of Actionscript. </p> <p> Wel is het zo dat skins in Synth op een veel consistentere manier zijn op te bouwen. Vrijwel alle stijlelementen zijn herbruikbaar, dus er zullen minder snel foutjes insluipen. Tevens heeft dit tot gevolg dat skin refactorings (denk aan verandering van fonts of kleuren) een relatief eenvoudige operatie zullen zijn. Dit zou in Flex toch wat meer aandacht en tijd kosten, aangezien CSS geen hergebruik (anders gezegd: centrale declaratie) van stijlelementen mogelijk maakt. </p> <p> Wat verder opvalt is dat de de hoeveelheid CSS code voor een Flex applicatie t.o.v. Synth Style bestand vele malen compacter is. </p>


, ,

Nog geen reacties

AIR brings RIA to the desktop

When I was visiting the Adobe Live event in Amsterdam (May 2007) the session of Mike Downey about Apollo caught my attention. Adobe Integrated Runtime, code name Apollo, (see also AIR) is the runtime environment that brings rich internet applications to the desktop. This ‘desktop’ can exist on any platform where a runtime is available. At the moment of writing this blog entry, only Windows and Mac are supported but work is in progress to support linux as well as mobile phone platforms.

The idea is to use existing web technologies (HTML, Flash, Flex, Ajax) to create a RIA that runs in the AIR runtime and is able to use the AIR API, e.g. for caching purposes. This API is available through JavaScript or ActionScript. It is possible to manipulate entire HTML pages using DOM and subclassing the HTMLControl class. By doing so JavaScript actions can be manipulated or a submit can be intercepted. The AIR API offers functionality to detect if the application is online, to manipulate the window chrome (with transparency), clipboard, drag&drop, network functionality and local storage. JavaScript can call ActionScript and vice versa.
The deployable unit is a .AIR file that needs to be installed the first time. The API contains update functionality to get a latest version of an application.

When looking for cool AIR applications on the web it becomes clear that it is a challenge for developers to use cool graphics that make your rich internet application intuitive and really look good. From this perspective it does not matter if you use Flex 3 or e.g. Ajax. Flex 3 has some cool new animations (mx.effects package) but as a developer you better find a good interaction designer to cover that part. With AIR you can combine different RIA technologies and extend them with powerful desktop functionality. It is also interesting to see how fast we are able to build new applications based on the scripting environment(s) that AIR supports. More interesting AIR developer details with examples I will post in next entries in this blog.

Some applications start to show the real value of AIR; at this moment Adobe launched a contest to create the most unique Adobe AIR application. With actually some real cool prices; may we have your votes!

, , , , , ,

Nog geen reacties