2010年5月4日星期二

於 Flex 使用 Flash 素材,與製作動態載入 Theme

最近工作上,花了很多時間在處理 Flash 與 Flex 之間的合作。

目標是,美術製作的 Flash 素材,Flex 可以直接使用,不用重新依照 Flash 的設計而在 Flex 中重新以 MXML 組合 layout;並且要能支援動態載入外部 *.swf 素材,達到切換 theme 的機制。

步驟一:準備數款 Flash 設計的檔案,並輸出 *.swf

/asset/asset1.fla -> /asset/asset1.swf


/asset/asset2.fla -> /asset/asset2.swf


/asset/asset3.fla -> /asset/asset3.swf


步驟二,將 Flash 素材,轉變成 Flex 素材

/css/asset1.css -> /css/asset1.swf
.MyBtns{
class: Embed(source="../asset/asset1.swf", symbol="ui.Btns");
}


/css/asset2.css -> /css/asset2.swf
.MyBtns{
class: Embed(source="../asset/asset2.swf", symbol="ui.Btns");
}


/css/asset3.css -> /css/asset3.swf
.MyBtns{
class: Embed(source="../asset/asset3.swf", symbol="ui.Btns");
}


步驟三,開一 MXML 專案

畫面中只放一個自訂 UIComponent : MyBtnsUIComponent

Theme.xml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
<mx:creationComplete>
<![CDATA[
StyleManager.loadStyleDeclarations("Users/ben/Documents/Flex Builder 3/Theme/css/asset1.swf");
]]>
</mx:creationComplete>
<local:MyBtnsUIComponent /> 
</mx:Application>


這個 MyBtnsUIComponent 作的事情是,從 CSS 中,取得 ui.Btns 類別,建立視覺物件,放到畫面上;因為從 Flash 產生的物件是 Sprite 或 MovieClip,無法直接放到 Application 中,所以要放在自訂的 UIComponent 中。

MyBtnsUIComponent.as
package
{
import flash.display.Sprite;
import flash.events.Event;

import mx.core.UIComponent;
import mx.events.FlexEvent;

public class MyBtnsUIComponent extends UIComponent
{
private var Cls:Class;
private var _view:Sprite;
private var _viewProxy:MyBtnsProxy;

public function MyBtnsUIComponent()
{
super();

this.addEventListener(FlexEvent.INITIALIZE, onInitialize, false, 0, true);
}

private function onInitialize(e:Event):void{
this.removeEventListener(FlexEvent.INITIALIZE, onInitialize);

this.styleName = "MyBtns";
}

override public function styleChanged(styleProp:String):void{
Cls = this.getStyle("class") as Class;
if(Cls!=null)
createView();
}

private function createView():void{
if(_view!=null && _view.parent!=null)
_view.parent.removeChild(_view);

_view = new Cls as Sprite;
this.addChild(_view);

if(_viewProxy==null)
_viewProxy = new MyBtnsProxy(_view);
else
_viewProxy.updateView(_view);
}
}
}


另外,設計了一個 FlashUIComponentProxy 用來包裝所有來自 Flash 的 MovieClip / Sprite。

FlashUIComponentProxy.as
package
{
import flash.display.Sprite;
import flash.events.Event;

public class FlashUIComponentProxy
{
static private var _updateController:Sprite = new Sprite();
static private var _updateProxies:Array = [];
static private function invalidateDisplayList(proxy:FlashUIComponentProxy):void{
_updateProxies.push(proxy);

if(!_updateController.hasEventListener(Event.ENTER_FRAME))
_updateController.addEventListener(Event.ENTER_FRAME, onUpdateController_update, false, 0, true);
}
static private function onUpdateController_update(e:Event):void{
_updateController.removeEventListener(Event.ENTER_FRAME, onUpdateController_update);

var ary:Array = _updateProxies.splice(0, _updateProxies.length);
for(var i:int=0; i<ary.length; i++){
(ary[i] as FlashUIComponentProxy).updateDisplayList();
}
}

protected var view:Sprite;

public function FlashUIComponentProxy(view:Sprite)
{
updateView(view);
}

private var _childProxies:Object = new Object();
public function setChildProxy(view:Sprite, ProxyCls:Class):FlashUIComponentProxy{
if(_childProxies[view.name] == null)
_childProxies[view.name] = new ProxyCls(view);
else
(_childProxies[view.name] as FlashUIComponentProxy).updateView(view);

return _childProxies[view.name] as FlashUIComponentProxy;
}

public function updateView(view:Sprite):void{
this.view = view;

makeLink();

invalidateDisplayList();
}

protected function makeLink():void{
}

protected function invalidateDisplayList():void{
FlashUIComponentProxy.invalidateDisplayList(this);
}

protected function updateDisplayList():void{

}
}
}


以這個例子來說,我有一個 Sprite 中,放了三個按鈕、一個文字欄位、一個 Sprite,因為這一整組 Sprite 會有自己的邏輯,所以建立了一個 MyBtnsProxy 來包裝。

MyBtnsProxy.as
package
{
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.text.TextField;

import mx.styles.StyleManager;

public class MyBtnsProxy extends FlashUIComponentProxy
{
private var _b1:SimpleButton;
private var _b2:SimpleButton;
private var _b3:SimpleButton;

private var _txtData:TextField;

private var _mcInner:Sprite;
private var _mcInnerProxy:MyInnerProxy;



private var _myData:String = "";
public function get myData():String{return _myData;}
public function set myData(value:String):void{
if(_myData==value)
return;
_myData=value;
this.invalidateDisplayList();
}



public function MyBtnsProxy(view:Sprite)
{
super(view);
}

override protected function makeLink():void{
_b1 = view.getChildByName("b1") as SimpleButton;
_b2 = view.getChildByName("b2") as SimpleButton;
_b3 = view.getChildByName("b3") as SimpleButton;

_txtData = view.getChildByName("txtData") as TextField;

_mcInner = view.getChildByName("mcInner") as Sprite;
_mcInnerProxy = this.setChildProxy(_mcInner, MyInnerProxy) as MyInnerProxy;


_b1.addEventListener(MouseEvent.CLICK, on_b1_click, false, 0, true);
_b2.addEventListener(MouseEvent.CLICK, on_b2_click, false, 0, true);
_b3.addEventListener(MouseEvent.CLICK, on_b3_click, false, 0, true);
}

private function on_b1_click(e:MouseEvent):void{
myData = "on_b1_click";
_mcInnerProxy.myData = "inner 1";

StyleManager.loadStyleDeclarations("Users/ben/Documents/Flex Builder 3/Theme/css/asset1.swf");
}

private function on_b2_click(e:MouseEvent):void{
myData = "on_b2_click";
_mcInnerProxy.myData = "inner 2";

StyleManager.loadStyleDeclarations("Users/ben/Documents/Flex Builder 3/Theme/css/asset2.swf");
}

private function on_b3_click(e:MouseEvent):void{
myData = "on_b3_click";
_mcInnerProxy.myData = "inner 3";

StyleManager.loadStyleDeclarations("Users/ben/Documents/Flex Builder 3/Theme/css/asset3.swf");
}

override protected function updateDisplayList():void{
_txtData.text = myData;
}
}
}


在上述的 Sprite 中,又另外有一個叫作 mcInner 的 Sprite,內含一個文字欄位,所以這裡也建立了另一個 MyInnerProxy 來包裝這個 Sprite。

MyInnerProxy.as
package
{
import flash.display.Sprite;
import flash.text.TextField;

public class MyInnerProxy extends FlashUIComponentProxy
{
private var _myData:String = "";
public function get myData():String{return _myData;}
public function set myData(value:String):void{
if(_myData==value)
return;
_myData=value;
this.invalidateDisplayList();
}



private var _txtInner:TextField;

public function MyInnerProxy(view:Sprite)
{
super(view);
}

override protected function makeLink():void{
_txtInner = view.getChildByName("txtInner") as TextField;
}

override protected function updateDisplayList():void{
_txtInner.text = myData;
}

}
}


以上,是所有程式碼,執行畫面如下:

按下按鈕一,動態載入 asset1.swf


按下按鈕二,動態載入 asset2.swf


按下按鈕三,動態載入 asset3.swf


整套程式的目的在於,動態換掉視覺後,文字欄位、資料的部分仍要能保存下來;決定是否要建立一個 FlashUIComponentProxy 來包裝一個 Sprite 在於這個 Sprite 有多複雜。

2 意見:

{id: "Ticore"}; // 提到...

我覺得嵌套的 Component 還是用 Flex 組件來組合會比較好。
每個單獨的 Component 應該都能找到相近的 Flex 組件來客制化,除非它只是一個 Flash 動畫~

最重要還是要能利用 DataBinding 的能力~
可以去掉很多資料更新的問題

至於 Layout 部分,用 Flex MXML 容器實作 IDataRenderer
這樣就會有統一的 data 資料物件
容器內所有子 Component 都透過 DataBinding 向 data 物件取用資料

最後想辦法用 CSS 作到切換 Layout 容器~

Ben Chang 提到...

基於 ART 天馬行空,譬如,時常在很細小的地方都有使用漸層色,若是 RD 要將 ART 的設計實現在 Flex Component 上時,有時候不是設定一些預設的顏色屬性或 CSS 就足夠了,可能還要改寫 Flex Component 在重繪畫面的部分,這樣就變成 ART 作一次、RD 還必須再作一次。

所以我的另一個目的也在於,"懶惰" 的時候、不見得需要發揮所有 Flex 功能(如 layout)的時候,就可以用這種作法將 ART 的設計直接拿來使用。