Spark list vertical scroll setup that works like Flex 3 mx list item by item scrolling. The point is to setup s:VScrollBar with smoothScrolling false and to set step size that equals list item height. You will also want to set the list height in a way that list items don't get cut off at the bottom (list height = n x item height).
<s:DataGroup id="contentGroup" width="341" height="100">
<s:layout>
<s:TileLayout horizontalGap="9" verticalGap="0" columnWidth="69" requestedColumnCount="4"/>
</s:layout>
</s:DataGroup>
<s:VScrollBar viewport="{contentGroup}" top="0" left="330" height="100" smoothScrolling="false" stepSize="100"/>
Easy and quick setup of spark list with vertical scroll.
<s:DataGroup id="contentGroup" width="100%" height="100%">
<s:layout>
<s:VerticalLayout/>
</s:layout>
</s:DataGroup>
<s:VScrollBar viewport="{contentGroup}"/>
With Firefox and Chrome the handling of the ALT GR keyboards combination fails silently.
We found a workaround based on the usage of an instance of the class LocalConnection in an SWF file based on ActionScript 1.0 (available
here) but we strongly avoid to use AS1 also for workaraounds like this.
The solution we outlined is available
here, is based on JavaScript and ActionScript 3.0 and can be summarized as following
- Instead of using a input text filed use a dynamic text field
- Through a Cursor class (the one available in the following snippet) the standard cursor behavior is simulated
- The focus is immediately removed from the text field and moved to the body of the HTML page
- The HTML page handle the keyboard pressure and communicate with the SWF file through JavaScript
The source code is not fully implemented so consider it as a proof of concept (POC), in the demo you can take a look to the behavior of a dynamic text field and of a input text field. In order to download the code you have to create an account.
package {
import flash.display.Sprite;
import flash.text.TextField;
import flash.events.Event;
import flash.events.TextEvent;
import flash.events.FocusEvent;
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.display.Graphics;
public class Cursor extends Sprite {
private var target:TextField;
private var timer:Timer;
private const CURSOR_DELAY:int = 400;
private const PADDING_BOTTOM:int = 4;
private const PADDING_TOP:int = 4;
private const PADDING_LEFT:int = 2;
private var _cursorDelay:int;
private var drawable:Boolean;
private var _paddingTop:int;
private var _paddingLeft:int;
private var _paddingBottom:int;
public static const ACTIVATE_INTERACTION:String = "onActivateInteraction";
public static const DEACTIVATE_INTERACTION:String = "onDeActivateInteraction";
public function Cursor(tg:TextField) {
target = tg;
addEventListener(Event.ADDED_TO_STAGE, init);
addEventListener(Event.REMOVED_FROM_STAGE, dispose);
}
private function createCursor():void{
if(!timer){
timer = new Timer(cursorDelay);
timer.addEventListener(TimerEvent.TIMER, onBlink);
timer.start();
}
}
public function destroyCursor():void{
if(timer){
timer.stop();
timer.removeEventListener(TimerEvent.TIMER, onBlink);
timer = null;
graphics.clear();
}
}
protected function onBlink(e:TimerEvent):void{
drawable = !drawable;
var g:Graphics = graphics;
if(drawable){
g.lineStyle(1, 0x000000, 1);
g.moveTo(cursorX, target.y + paddingTop);
g.lineTo(cursorX, target.y + target.height - paddingBottom);
}else{
g.clear();
}
}
protected function handleInput(e:TextEvent):void{
}
protected function onFocusIn(e:FocusEvent):void{
dispatchEvent(new Event(ACTIVATE_INTERACTION));
createCursor();
stage.focus = parent;
}
protected function onFocusOut(e:FocusEvent):void{
dispatchEvent(new Event(DEACTIVATE_INTERACTION));
}
private function init(e:Event):void{
target.addEventListener(TextEvent.TEXT_INPUT, handleInput);
target.addEventListener(FocusEvent.FOCUS_IN, onFocusIn);
target.addEventListener(FocusEvent.FOCUS_OUT, onFocusOut);
}
private function dispose(e:Event):void{
target.removeEventListener(TextEvent.TEXT_INPUT, handleInput);
target.removeEventListener(FocusEvent.FOCUS_IN, onFocusIn);
target.removeEventListener(FocusEvent.FOCUS_OUT, onFocusOut);
}
public function set cursorDelay(value:int):void{
_cursorDelay = value;
}
public function get cursorDelay():int{
return _cursorDelay ? _cursorDelay : CURSOR_DELAY;
}
public function set paddingTop(value:int):void{
_paddingTop = value;
}
public function get paddingTop():int{
return _paddingTop ? _paddingTop : PADDING_TOP;
}
public function set paddingBottom(value:int):void{
_paddingBottom = value;
}
public function get paddingBottom():int{
return _paddingBottom ? _paddingBottom : PADDING_BOTTOM;
}
public function set paddingLeft(value:int):void{
_paddingLeft = value;
}
public function get paddingLeft():int{
return _paddingLeft ? _paddingLeft : PADDING_LEFT;
}
public function get currentTarget():TextField{
return target;
}
private function get cursorX():int{
return target.x + paddingLeft + target.textWidth;
}
}
}
If you want to listen for events from events created by a DataGroup, you can use the group's events renderAdd and renderRemove to be notified when items are being created or recycled.
From those event handlers you can setup your items or store them somewhere, like a Dictionary, to be easily accessed later.
<fx:Script>
<![CDATA[
import mx.core.IVisualElement;
import spark.events.RendererExistenceEvent;
private var map:Dictionary = new Dictionary(true);
private function onRendererAdd(event:RendererExistenceEvent):void {
var index:int = event.index;
var renderer:IVisualElement = event.renderer;
if (!renderer) return;
map[index] = renderer;
MyRenderer(renderer).addEventListener(MyCustomEvent.ACTION, onItemRendererAction);
}
private function onRendererRemove(event:RendererExistenceEvent):void {
var index:int = event.index;
var renderer:IVisualElement = event.renderer;
if (!renderer) return;
delete map[index];
MyRenderer(renderer).removeEventListener(MyCustomEvent.ACTION, onItemRendererAction);
}
]]>
</fx:Script>
<s:DataGroup id="buttons" top="300" height="100%"
rendererAdd="onRendererAdd(event)"
rendererRemove="onRendererRemove(event)"
itemRenderer="MyRenderer">
<s:layout>
<s:VerticalLayout gap="12" horizontalAlign="left" paddingLeft="20"/>
</s:layout>
</s:DataGroup>
Usually flash movies needs to be clicked to make them gain focus before they can be interacted via keyboard. If you want to gain automagically when your movie starts, you can do it via javascript. It's just a matter of calling the focus() method on the right html element at the right time.
You can find here an example made using SWFObject. The setFocusOnFlash() is used as a callback by the embedSWF() method. An event object is sent along the function callback, holding the operation status, the html element id and a direct reference to the html element.
Notes:
- make sure to use wmode opaque or transparent, otherwise this trick won't work on chrome and safari
- make sure to set the tabIndex property on the html element you're going to focus, otherwise it won't be in the "tabbing list" and some browsers (chrome and safari) will ignore the focus() call
- on firefox (both 3.6 and 4.0) the callback is fired a bit too early, hence the need for a small timer before the actual call
<script type="text/javascript">
function setFocusOnFlash(e) {
setTimeout(function() {
if (e.success) {
e.ref.tabIndex = 0;
e.ref.focus();
}
},125);
}
var swfVersionStr = "10.0.0";
var xiSwfUrlStr = "playerProductInstall.swf";
var flashvars = {};
var params = {};
params.quality = "high";
params.bgcolor = "#000000";
params.allowscriptaccess = "sameDomain";
params.allowfullscreen = "true";
params.wmode = "opaque";
var attributes = {};
attributes.id = "MyMovie";
attributes.name = "MyMovie";
attributes.align = "middle";
swfobject.embedSWF(
"MyMovie.swf", "flashContent",
"300", "200",
swfVersionStr, xiSwfUrlStr,
flashvars, params, attributes, setFocusOnFlash);
swfobject.createCSS("#flashContent", "display:block;text-align:left;");
</script>
If you have your Google Maps JavaScript API overlayed over a Flash content in iframe you may experience really slow performance in IE. Solution is to set the wmode to "opaque" for IE. Note that for other browsers this might cause some issues. Our experience shown that it's best to set wmode to "opaque" exclusively for IE and for other browsers set it to "window", which is default wmode.
/* Set wmode to "opaque" in your HTML wrapper for Object and Embed tags. */
Using blend modes you can show your dynamic textfields with a nice gradient instead of the usual boring color.
With this method you can make a text transparent without embedding the font.
ps: you can also try with input text field by setting the appropriate value to the type property and changing selectable to true
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFieldType;
import flash.display.Shape;
import flash.display.GradientType;
import flash.display.SpreadMethod;
import flash.display.InterpolationMethod;
import flash.display.BlendMode;
import flash.text.TextFormat;
import flash.text.TextFormatAlign;
import flash.geom.Matrix;
/*
* Dynamically created textfields seems to mantain a constant 2px
* border around the text, no matter the font size. If we don't
* handle it in the gradient size and position, we'll see a gradient
* border around out text
*/
const TEXTFIELD_BORDER:int = 2;
/*
* Let's build the container of out text
* and set it in blend mode LAYER.
*/
var container:Sprite = new Sprite();
container.blendMode = BlendMode.LAYER;
addChild(container);
/*
* Now we add out textfield to the container
* and we set its blend mode to ALPHA
*/
var field:TextField = new TextField();
field.blendMode = BlendMode.ALPHA;
field.defaultTextFormat = new TextFormat("_sans", 14, 0xFF0000, true, false, false, null, null, TextFormatAlign.LEFT, 0, 0, 0, 0);
field.text = "Lorem ipsum dolor sit amet";
field.autoSize = TextFieldAutoSize.LEFT;
field.wordWrap = false;
field.multiline = false;
field.selectable = false;
container.addChild(field);
/*
* Finally we can create a shape with a gradient box inside
* we'll use the sizes from the field to define its dimensions
* and make sure to put it BEHIND the text field, inside the container
*/
var colors:Array = [0xFF00000, 0xFF00FF, 0xFFFFFF, 0xFFFF00];
var alphas:Array = [1, 1, 0, 1];
var ratios:Array = [0x00, 0x43, 0x7F, 0xFF];
var matrix:Matrix = new Matrix();
matrix.createGradientBox(field.width-TEXTFIELD_BORDER*2, field.height-TEXTFIELD_BORDER*2, 0, TEXTFIELD_BORDER, TEXTFIELD_BORDER);
var gradient:Shape = new Shape();
gradient.graphics.beginGradientFill(GradientType.LINEAR, colors, alphas, ratios, matrix, SpreadMethod.PAD, InterpolationMethod.RGB, 0);
gradient.graphics.drawRect(TEXTFIELD_BORDER, TEXTFIELD_BORDER, field.width-TEXTFIELD_BORDER*2, field.height-TEXTFIELD_BORDER*2);
gradient.graphics.endFill();
container.addChildAt(gradient, 0);
If you draw a resized 9-slice scale Sprite to a BitmapData, the result will be non-resized original bitmap data of your Sprite.
To take a bitmap capture from a resized 9-slice scale Sprite, add it to a Sprite container and draw the Sprite container to a BitmapData.
Note that non of the Sprites need to be added to the display list to accomplish this.
[Embed(source="/../assets/nine_slice_frame.png", scaleGridTop="100", scaleGridBottom="1100", scaleGridLeft="100", scaleGridRight="1500")]
private const NINE_SLICE_FRAME:Class;
var nineSliceFrame:Sprite = new NINE_SLICE_FRAME() as Sprite;
nineSliceFrame.width = 400;
nineSliceFrame.height = 400;
// Use dummy Sprite instance to take a bitmap capture of resized Sprite with 9 slice scale applied.
var dummySprite:Sprite = new Sprite();
dummySprite.width = 400;
dummySprite.height = 400;
dummySprite.addChild(nineSliceFrame);
var nineSliceFrameBitmapData:BitmapData = new BitmapData(400, 400, true, 0x00000000);
nineSliceFrameBitmapData.draw(dummySprite);
var nineSliceFrameBitmap:Bitmap = new Bitmap(nineSliceFrameBitmapData);
addChild(nineSliceFrameBitmap);
Using the BitmapData class is quite easy and fast copy or manipulate an external image loaded into a Flex application, it's quite interesting the difference between the data type of the content property of an Image component because it changes if a bitmap or a SWF has been loaded:
- the first one is a Bitmap
- the second on is a MovieClipLoaderAsset
In order to let you copy the content also with a SWF file loaded into the Image component you can use a Loader and the loadBytes() method.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical"
verticalAlign="middle"
backgroundColor="white">
<mx:Script>
<![CDATA[
import mx.core.MovieClipLoaderAsset;
import mx.core.BitmapAsset;
import mx.collections.ArrayCollection;
[Embed(source="placeholder.swf")]
private const ARTWORK_PLACE_HOLDER_SWF:Class;
[Embed(source="placeholder.png")]
private const ARTWORK_PLACE_HOLDER_PNG:Class;
[Bindable]
private var collection:ArrayCollection = new ArrayCollection();
private function handleImage(e:Event):void{
if(e.currentTarget.selectedValue == "PNG"){
img.source = ARTWORK_PLACE_HOLDER_PNG;
}else{
img.source = ARTWORK_PLACE_HOLDER_SWF;
}
}
private function dumpImage(source:Image):void {
var asset:* = source.content;
var data:BitmapData;
var bitmap:Bitmap;
try{
data = Bitmap(source.content).bitmapData;
bitmap = new Bitmap(data);
collection.addItem({image:bitmap, label:"item #" + (collection.length + 1)});
}catch(error:Error){
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onBitmapData);
var swf:MovieClipLoaderAsset = asset as MovieClipLoaderAsset;
loader.loadBytes(swf.movieClipData);
}
}
private function onBitmapData(e:Event):void{
e.target.removeEventListener(e.type, arguments.callee);
var content: MovieClip = MovieClip((e.currentTarget as LoaderInfo).content)
var data:BitmapData = new BitmapData(content.width, content.height);
data.draw(content, null, null, null, null, true)
var bitmap:Bitmap = new Bitmap(data)
collection.addItem({image:bitmap, label:"item #" + (collection.length + 1)});
}
]]>
</mx:Script>
<mx:HBox>
<mx:Panel title="Source image">
<mx:HBox verticalAlign="middle" horizontalAlign="center" width="100%" height="100%">
<mx:Image id="img" source="{ARTWORK_PLACE_HOLDER_SWF}" />
</mx:HBox>
<mx:RadioButtonGroup id="imageSelector" change="handleImage(event)" />
<mx:RadioButton label="SWF" selected="true" group="{imageSelector}" />
<mx:RadioButton label="PNG" group="{imageSelector}" />
<mx:ControlBar>
<mx:Button label="Copy image" click="dumpImage(img)" />
</mx:ControlBar>
</mx:Panel>
<mx:TileList id="tileList" dataProvider="{collection}" width="450" height="500" columnCount="4" verticalScrollPolicy="on">
<mx:itemRenderer>
<mx:Component>
<mx:VBox>
<mx:Image source="{data.image}" />
<mx:Label text="{data.label}" />
</mx:VBox>
</mx:Component>
</mx:itemRenderer>
</mx:TileList>
</mx:HBox>
</mx:Application>
If you would like consume some services through RemoteObject from Flash in the same fashion you do in Flex, you will need to follow these step:
- Add the rpc.swc and framework.swc from your {FLEX_HOME}/frameworks/libs to you ptoject or .fla classpath
- Optional: If you are compiling an Actionscript Project in Flash Builder you'll also need the resource bundles for those swcs. So add also the rpc_rb.swc and framework_rb.swc from {FLEX_HOME}/frameworks/locale to your classpath.
- Look at the code below, the initializeRPC and registerClasses methods are what you need to make things work.
You are now ready to call remote methods via amf like this simple example.
Notes:
You will bring nearly 100kb of footprint to your swf due to dependencies from the flex framework. You reduce that footprint by registering a custom collection class instead of the ArrayCollection. The only requirement is that your collection class implements the
IExternalizable interface.
import mx.collections.ArrayCollection;
import mx.core.mx_internal;
import mx.logging.Log;
import mx.logging.targets.TraceTarget;
import mx.messaging.ChannelSet;
import mx.messaging.channels.AMFChannel;
import mx.messaging.config.ConfigMap;
import mx.messaging.config.LoaderConfig;
import mx.messaging.messages.AcknowledgeMessage;
import mx.messaging.messages.AcknowledgeMessageExt;
import mx.messaging.messages.CommandMessage;
import mx.messaging.messages.CommandMessageExt;
import mx.messaging.messages.ErrorMessage;
import mx.messaging.messages.RemotingMessage;
import mx.rpc.AbstractOperation;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.RemoteObject;
import mx.utils.ObjectProxy;
use namespace mx_internal;
const ID:String = "my-amf";
const ENDPOINT:String = "http://your.domain/amf/gateway";
const DEFAULT_SOURCE:String = "your.service";
const DEFAULT_SOURCE:String = "your.service";
var remoteObject:RemoteObject;
/**
* This initializes the rpc library.
* After this you should see the requests going through.
* Note: LoaderConfig won't show up in the code hints, so you have to write down the import manually.
*/
function initializeRPC():void {
LoaderConfig.mx_internal::_url = loaderInfo.url;
LoaderConfig.mx_internal::_parameters = loaderInfo.parameters;
}
/**
* We register a bunch of class aliases that are needed by the rpc methods.
* The list *may be* incomplete, if you get some TypeCoertion error try registering that class.
*/
function registerClasses():void {
registerClassAlias("flex.messaging.messages.ErrorMessage", ErrorMessage);
registerClassAlias("flex.messaging.messages.CommandMessage", CommandMessage);
registerClassAlias("flex.messaging.messages.RemotingMessage", RemotingMessage);
registerClassAlias("flex.messaging.messages.AcknowledgeMessage", AcknowledgeMessage);
registerClassAlias("DSC", CommandMessageExt);
registerClassAlias("DSK", AcknowledgeMessageExt);
registerClassAlias("flex.messaging.config.ConfigMap", ConfigMap);
registerClassAlias("flex.messaging.io.ObjectProxy", ObjectProxy);
registerClassAlias("flex.messaging.io.ArrayCollection", ArrayCollection);
}
/**
* Prepare the RemoteObject, creating the appropriate AMF channel
* and setting up the default event handlers.
*/
function prepareDefaultRemoteObject():void {
var channel:AMFChannel = new AMFChannel(ID, ENDPOINT);
var channelSet:ChannelSet = new ChannelSet();
channelSet.addChannel(channel);
remoteObject = new RemoteObject();
remoteObject.source = DEFAULT_SOURCE;
remoteObject.destination = DEFAULT_DESTINATION;
remoteObject.channelSet = channelSet;
remoteObject.addEventListener(FaultEvent.FAULT, onDefaultServiceFault);
remoteObject.addEventListener(ResultEvent.RESULT, onDefaultServiceResult);
}
/**
* Create and execute the remote method.
*/
function makeAnonymousRemoteCall():void {
var operation:AbstractOperation = ro.getOperation("getProduct");
operation.send();
}
/**
* Handles faulty replies from the remote methods.
*/
function onDefaultServiceFault(event:FaultEvent):void {
trace(event.fault.faultDetail);
}
/**
* Handles successful replies from the remote methods.
*/
function onDefaultServiceResult(event:ResultEvent):void {
trace(event.result.toString());
}
// All RPC classes use the Flex logging API so enabling it in the Flash project could be helpful.
Log.addTarget(new TraceTarget());
initializeRPC();
registerClasses();
prepareDefaultRemoteObject();
makeAnonymousRemoteCall();