2013年2月5日火曜日

Feathers - デスクトップとモバイルで使えるUIコントロールフレームワーク

Stage3D
FlashPlayer11.0(AIR 3.0)よりGPUによる2D, 3Dのハードウェアアクセラレーション
レンダリング機能が付きました。
この機能により、描画処理の向上に期待ができるようになります。
Stage3DはFlashPlayerとAIRで使う事ができます。

Starling Framework
Stage3Dの機能を使うにはAGALというアセンブラ命令をActionScriptに書きます。
Starling FrameworkではAGALを意識する事なく、普段ActionScriptを書くようなスタイルでスプライト機能やアニメーションを利用できます。

Feathers
Starling Frameworkの機能を利用したUIフレームワークです。
ボタンやリストのようなUI、画面の管理を行う事ができます。

動作パフォーマンス
Feathersを利用した動作パフォーマンスですがNexus7で検証した所
おおむね良好だと思います。動画を撮影しましたのでご覧下さい。
AIR3.5 Feathers UISample

導入編
Feathersを導入してアプリケーションを開発する方法はいくつかありますが
FlashBuilderを利用した開発手法を取り上げます。
Feathersについて簡単な概要とStarling FrameworkとFeathersの
ライブラリプロジェクトの設定方法を記載しています。



UIコンポーネント ボタン編
ボタンの組み込み方法とスキンの変更処理を記載しています。

ボタンとリストのサンプルソースコード作成と素材を組み込みます。
サンプルソースコードを元にボタンの概要とスキンの変更処理について解説しています。

UIコンポーネント リスト編
リストコンポーネントとアイテムレンダラについて記載しています。

画面の処理 スクリーン編
複数の画面間の移動を行う処理を記載しています。

スクリーンのサンプルソースコードを作成します。

サンプルソースコードを元にスクリーンの解説をしています。

Feathers Example まとめ

はじめに

Feathers ListとButtonのサンプル

Feathers Screenのサンプル

Feathers ScreenExample Screenについて

このサンプルはFeathersのサイト
「How to use the Feathers Screen Component(リンク先へ) 」
「How to use the Feathers ScreenNavigator component(リンク先へ) 」
「Feathers ScreenNavigator Transitions(リンク先へ) 」
を参考にしています。

Feathers ScreenExampleのソースコード(リンク先へ)

Main.as
this._navigator.addScreen(MAIN_MENU,new ScreenNavigatorItem(MainMenuScreen,MAIN_MENU_EVENTS));
addScreenメソッド
ScreenNavigatorItemコンストラクタで必要とされる最初の引数は、 画面のIDと画面のインスタンスまたはすでにインスタンス化されているのDisplayObjectです。

イベントを利用した画面のナビゲーション
ScreenNavigatorItemクラスの第2引数(Object型)でイベントを定義します。
 イベントを送出すると、ScreenNavigatorは自動的に別の画面に移動する処理を
行います。

MainMenuScreen.as
   this._list.dataProvider = new ListCollection(
    [
    { text: "Sub 1", event: SHOW_SUB1_SCREEN },
    { text: "Sub 2", event: SHOW_SUB2_SCREEN },
    ]);
Main.asで定義したイベントをリストのdataProviderに設定します。
  private function list_changeHandler(event:Event):void
  {
   const eventType:String = this._list.selectedItem.event as String;
   this.dispatchEventWith(eventType);
  }
リストのアイテムが選択された時に画面に移動する為のイベントを送出します。

ownerプロパティによる画面のナビゲーション
Sub1Screen.as
  private function onBackButton():void
  {
   this.owner.showScreen(Main.MAIN_MENU);
  }
Screenクラスが持つownerプロパティのshowScreenメソッドで別画面の移動を行います。

トランジションの設定
Main.as
   this._transitionManager = new ScreenSlidingStackTransitionManager(this._navigator);
   this._transitionManager.duration = 0.4;
画面の移動を行う時のトランジションとしてScreenSlidingStackTransitionManagerを使用します。

Feathers ScreenExample 作成3

Sub2ChildScreen.as
package screens
{
 import feathers.controls.Button;
 import feathers.controls.Header;
 import feathers.controls.Label;
 import feathers.controls.Screen;
 import feathers.controls.ScrollContainer;
 import feathers.layout.AnchorLayout;
 import feathers.layout.VerticalLayout;

 import starling.display.DisplayObject;
 import starling.events.Event;

 public class Sub2ChildScreen extends Screen
 {
  private var _container:ScrollContainer;
  private var _header:Header;
  private var _backButton:Button;

  public var horizontalAlign:String = VerticalLayout.HORIZONTAL_ALIGN_LEFT;
  public var verticalAlign:String = VerticalLayout.VERTICAL_ALIGN_TOP;
  public var gap:Number = 2;
  public var paddingTop:Number = 0;
  public var paddingRight:Number = 0;
  public var paddingBottom:Number = 0;
  public var paddingLeft:Number = 0;

  public function Sub2ChildScreen()
  {
   super();
  }

  override protected function initialize():void
  {
   this._container = new ScrollContainer();
   this._container.layout = new AnchorLayout();
   this._container.verticalScrollPolicy = ScrollContainer.SCROLL_POLICY_ON;
   this._container.snapScrollPositionsToPixels = true;
   this.addChild(this._container);

   this._header = new Header();
   this._header.title = "Sub2 ChildScreen";
   this.addChild(this._header);

   this._backButton = new Button();
   this._backButton.label = "Back";
   this._backButton.addEventListener(Event.TRIGGERED, backButton_triggeredHandler);

   this._header.leftItems = new &ltdisplayobject>
    [
    this._backButton
    ];

   this.backButtonHandler = this.onBackButton;

   var label:Label = new Label();
   label.text = "Sub2のサブビューです";
   this._container.addChild(label);
  }

  override protected function draw():void
  {
   this._header.width = this.actualWidth;
   this._header.validate();

   this._container.y = this._header.height;
   this._container.width = this.actualWidth;
   this._container.height = this.actualHeight - this._container.y;
  }

  private function onBackButton():void
  {
   this.owner.showScreen(Main.SUB2_SCREEN);
  }

  private function backButton_triggeredHandler(event:Event):void
  {
   this.onBackButton();
  }
 }
}
Sub2Screen.as
package screens
{
 import feathers.controls.Button;
 import feathers.controls.Header;
 import feathers.controls.Screen;
 import feathers.controls.ScrollContainer;
 import feathers.layout.VerticalLayout;

 import starling.display.DisplayObject;
 import starling.display.Quad;
 import starling.events.Event;

 public class Sub2Screen extends Screen
 {
  public static const SHOW_CHILD_SCREEN:String = "showChildScreen";

  private var _container:ScrollContainer;
  private var _header:Header;
  private var _backButton:Button;
  private var _childButton:Button;

  public var horizontalAlign:String = VerticalLayout.HORIZONTAL_ALIGN_LEFT;
  public var verticalAlign:String = VerticalLayout.VERTICAL_ALIGN_TOP;
  public var gap:Number = 2;
  public var paddingTop:Number = 0;
  public var paddingRight:Number = 0;
  public var paddingBottom:Number = 0;
  public var paddingLeft:Number = 0;

  public function Sub2Screen()
  {
   super();
  }

  override protected function initialize():void
  {
   const layout:VerticalLayout = new VerticalLayout();
   layout.gap = this.gap;
   layout.paddingTop = this.paddingTop;
   layout.paddingRight = this.paddingRight;
   layout.paddingBottom = this.paddingBottom;
   layout.paddingLeft = this.paddingLeft;
   layout.horizontalAlign = this.horizontalAlign;
   layout.verticalAlign = this.verticalAlign;

   this._container = new ScrollContainer();
   this._container.layout = layout;
   this._container.verticalScrollPolicy = ScrollContainer.SCROLL_POLICY_ON;
   this._container.snapScrollPositionsToPixels = true;
   this.addChild(this._container);

   this._header = new Header();
   this._header.title = "Sub2 Screen";
   this.addChild(this._header);

   this._backButton = new Button();
   this._backButton.label = "Back";
   this._backButton.addEventListener(Event.TRIGGERED, backButton_triggeredHandler);

   this._header.leftItems = new &ltdisplayobject>
    [
    this._backButton
    ];

   this._childButton = new Button();
   this._childButton.label = "ChildScreen";
   this._childButton.addEventListener(Event.TRIGGERED, childButton_triggeredHandler);

   this._header.rightItems = new &ltdisplayobject>
    [
    this._childButton
    ];

   this.backButtonHandler = this.onBackButton;

   var redQuad:Quad = new Quad(100,100,0xff0000);
   var greenQuad:Quad = new Quad(100,100,0x00ff00);
   var blueQuad:Quad = new Quad(100,100,0x0000ff);

   this._container.addChild(redQuad);
   this._container.addChild(greenQuad);
   this._container.addChild(blueQuad);
  }

  private function childButton_triggeredHandler():void
  {
   this.dispatchEventWith(SHOW_CHILD_SCREEN);
  }

  override protected function draw():void
  {
   this._header.width = this.actualWidth;
   this._header.validate();

   this._container.y = this._header.height;
   this._container.width = this.actualWidth;
   this._container.height = this.actualHeight - this._container.y;
  }

  private function onBackButton():void
  {
   this.owner.showScreen(Main.MAIN_MENU);
  }

  private function backButton_triggeredHandler(event:Event):void
  {
   this.onBackButton();
  }
 }
}
StarlingFramework,Feathers,FeathersThemeの
Flexライブラリプロジェクトの登録を行います。(リンク先へ)
ビルドを行います。

Feathers ScreenExample 作成2

MainMenuScreen.as
package screens
{
 import feathers.controls.Header;
 import feathers.controls.List;
 import feathers.controls.Screen;
 import feathers.data.ListCollection;
 import feathers.skins.StandardIcons;

 import starling.events.Event;
 import starling.textures.Texture;

 public class MainMenuScreen extends Screen
 {
  public static const SHOW_SUB1_SCREEN:String = "showSub1Screen";
  public static const SHOW_SUB2_SCREEN:String = "showSub2Screen";

  private var _header:Header;
  private var _list:List;

  public function MainMenuScreen()
  {
   super();
  }

  override protected function initialize():void
  {
   this._header = new Header();
   this._header.title = "Screen Test";
   this.addChild(this._header);

   this._list = new List();
   this._list.dataProvider = new ListCollection(
    [
    { text: "Sub 1", event: SHOW_SUB1_SCREEN },
    { text: "Sub 2", event: SHOW_SUB2_SCREEN },
    ]);
   this._list.itemRendererProperties.labelField = "text";
   this._list.addEventListener(Event.CHANGE,list_changeHandler);
   this._list.itemRendererProperties.accessorySourceFunction = accessorySourceFunction;
   this.addChild(this._list);
  }

  override protected function draw():void
  {
   this._header.width = this.actualWidth;
   this._header.validate();

   this._list.y = this._header.height;
   this._list.width = this.actualWidth;
   this._list.height = this.actualHeight - this._list.y;
  }

  private function accessorySourceFunction(item:Object):Texture
  {
   return StandardIcons.listDrillDownAccessoryTexture;
  }

  private function list_changeHandler(event:Event):void
  {
   const eventType:String = this._list.selectedItem.event as String;
   this.dispatchEventWith(eventType);
  }
 }
}
Sub1Screen.as
package screens
{
 import feathers.controls.Button;
 import feathers.controls.Header;
 import feathers.controls.Screen;
 import feathers.controls.ScrollContainer;
 import feathers.layout.HorizontalLayout;

 import starling.display.DisplayObject;
 import starling.display.Quad;
 import starling.events.Event;

 public class Sub1Screen extends Screen
 {
  private var _container:ScrollContainer;
  private var _header:Header;
  private var _backButton:Button;
  private var _settingsButton:Button;

  public var horizontalAlign:String = HorizontalLayout.HORIZONTAL_ALIGN_LEFT;
  public var verticalAlign:String = HorizontalLayout.VERTICAL_ALIGN_TOP;
  public var gap:Number = 2;
  public var paddingTop:Number = 0;
  public var paddingRight:Number = 0;
  public var paddingBottom:Number = 0;
  public var paddingLeft:Number = 0;

  public function Sub1Screen()
  {
   super();
  }

  override protected function initialize():void
  {
   const layout:HorizontalLayout = new HorizontalLayout();
   layout.gap = this.gap;
   layout.paddingTop = this.paddingTop;
   layout.paddingRight = this.paddingRight;
   layout.paddingBottom = this.paddingBottom;
   layout.paddingLeft = this.paddingLeft;
   layout.horizontalAlign = this.horizontalAlign;
   layout.verticalAlign = this.verticalAlign;

   this._container = new ScrollContainer();
   this._container.layout = layout;
   this._container.horizontalScrollPolicy = ScrollContainer.SCROLL_POLICY_ON;
   this._container.snapScrollPositionsToPixels = true;
   this.addChild(this._container);

   this._header = new Header();
   this._header.title = "Sub1 Screen";
   this.addChild(this._header);

   this._backButton = new Button();
   this._backButton.label = "Back";
   this._backButton.addEventListener(Event.TRIGGERED, backButton_triggeredHandler);

   this._header.leftItems = new 
    [
    this._backButton
    ];

   this.backButtonHandler = this.onBackButton;

   var redQuad:Quad = new Quad(100,100,0xff0000);
   var greenQuad:Quad = new Quad(100,100,0x00ff00);
   var blueQuad:Quad = new Quad(100,100,0x0000ff);

   this._container.addChild(redQuad);
   this._container.addChild(greenQuad);
   this._container.addChild(blueQuad);
  }

  override protected function draw():void
  {
   this._header.width = this.actualWidth;
   this._header.validate();

   this._container.y = this._header.height;
   this._container.width = this.actualWidth;
   this._container.height = this.actualHeight - this._container.y;
  }

  private function onBackButton():void
  {
   this.owner.showScreen(Main.MAIN_MENU);
  }

  private function backButton_triggeredHandler(event:Event):void
  {
   this.onBackButton();
  }
 }
}

Feathers ScreenExample 作成1

プロジェクト名はFeathersScreenです。
パッケージの構成です。

FeathersScreenExample.as
package
{
 import flash.display.Sprite;
 import flash.display.StageAlign;
 import flash.display.StageScaleMode;
 import flash.events.Event;
 import flash.geom.Rectangle;

 import starling.core.Starling;

 [SWF(width="960",height="640",frameRate="60",backgroundColor="#2f2f2f")]
 public class FeathersScreenExample extends Sprite
 {
  private var _starling:Starling;        

  public function FeathersScreenExample()
  {
   if(this.stage)
   {
    this.stage.scaleMode = StageScaleMode.NO_SCALE;
    this.stage.align = StageAlign.TOP_LEFT;
   }
   this.mouseEnabled = this.mouseChildren = false;
   this.loaderInfo.addEventListener(Event.COMPLETE, loaderInfo_completeHandler);
  }

  private function loaderInfo_completeHandler(event:Event):void
  {
   Starling.handleLostContext = true;
   Starling.multitouchEnabled = true;
   this._starling = new Starling(Main, this.stage);
   this._starling.enableErrorChecking = false;
   this._starling.start();

   this.stage.addEventListener(Event.RESIZE, stage_resizeHandler, false, int.MAX_VALUE, true);
   this.stage.addEventListener(Event.DEACTIVATE, stage_deactivateHandler, false, 0, true);
  }

  private function stage_resizeHandler(event:Event):void
  {
   this._starling.stage.stageWidth = this.stage.stageWidth;
   this._starling.stage.stageHeight = this.stage.stageHeight;

   const viewPort:Rectangle = this._starling.viewPort;
   viewPort.width = this.stage.stageWidth;
   viewPort.height = this.stage.stageHeight;
   try
   {
    this._starling.viewPort = viewPort;
   }
   catch(error:Error) {}
  }

  private function stage_deactivateHandler(event:Event):void
  {
   this._starling.stop();
   this.stage.addEventListener(Event.ACTIVATE, stage_activateHandler, false, 0, true);
  }

  private function stage_activateHandler(event:Event):void
  {
   this.stage.removeEventListener(Event.ACTIVATE, stage_activateHandler);
   this._starling.start();
  }
 }
}
Main.as
package 
{
 import feathers.controls.ScreenNavigator;
 import feathers.controls.ScreenNavigatorItem;
 import feathers.controls.ScrollContainer;
 import feathers.motion.transitions.ScreenSlidingStackTransitionManager;
 import feathers.themes.MetalWorksMobileTheme;

 import screens.MainMenuScreen;
 import screens.Sub1Screen;
 import screens.Sub2ChildScreen;
 import screens.Sub2Screen;

 import starling.display.Sprite;
 import starling.events.Event;

 public class Main extends Sprite
 {
  public static const MAIN_MENU:String = "mainMenu";
  public static const SUB1_SCREEN:String = "sub1Screen";
  public static const SUB2_SCREEN:String = "sub2Screen";
  public static const SUB2_CHILD_SCREEN:String = "sub2ChildScreen";

  private var _theme:MetalWorksMobileTheme;
  private var _container:ScrollContainer;
  private var _navigator:ScreenNavigator;
  private var _menu:MainMenuScreen;
  private var _transitionManager:ScreenSlidingStackTransitionManager;

  private static const MAIN_MENU_EVENTS:Object =
   {
    showSub1Screen: SUB1_SCREEN,
    showSub2Screen: SUB2_SCREEN
  }

  public function Main()
  {
   super();
   this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
  }

  private function addedToStageHandler(event:Event):void
  {
   this._theme = new MetalWorksMobileTheme(this.stage);

   this._navigator = new ScreenNavigator();

   this._navigator.addScreen(SUB1_SCREEN,new ScreenNavigatorItem(Sub1Screen));

   this._navigator.addScreen(SUB2_SCREEN,
    new ScreenNavigatorItem(Sub2Screen,{showChildScreen:SUB2_CHILD_SCREEN}));
   this._navigator.addScreen(SUB2_CHILD_SCREEN,new ScreenNavigatorItem(Sub2ChildScreen));

   this._navigator.addScreen(MAIN_MENU,new ScreenNavigatorItem(MainMenuScreen,MAIN_MENU_EVENTS));

   this.addChild(this._navigator);

   this._transitionManager = new ScreenSlidingStackTransitionManager(this._navigator);
   this._transitionManager.duration = 0.4;

   this._navigator.showScreen(MAIN_MENU);
  }
 }
}

Feathers Screenサンプルについて

画面の管理や切り替えを行う為のScreenクラスを使ったサンプルを作成します。

サンプルアプリ概要

  • メインはリスト形式でビューを遷移する為のリストアイテムが2件あります。
  • Sub1をタップすると、Sub1 Screenに遷移します。
  • Sub2をタップすると、Sub2 Screenに遷移します。
  • ChildScreenボタンを押すとSub2 Child Screenへ遷移します。
  • Backボタンを押す事で1つ前のScreenへ戻ります。

Feathers Example ItemRendererについて

このサンプルはFeathersのサイト
「Creating custom item renderers for the Feathers List control(リンク先へ) 」
を参考にしています。
FeathersExampleのソースコード(リンク先へ)

アイテムレンダラクラスでは、ListクラスからdataProviderで設定した配列の情報を
元に1件毎に表示するリストアイテムのレイアウトを行う事ができます。
アイテムレンダラーはapache Flex(旧Adobe Flex)でも採用しています。

BasicItemRenderer.as
  override protected function initialize():void
  {
   if(!this.itemBG)
   {
    this.itemBG = new Quad(1,1,Math.floor(Math.random() * 0xffffff));
    this.addChild(this.itemBG);
   }

   if(!this.itemLabel)
   {
    this.itemLabel = new Label();
    this.addChild(this.itemLabel);
   }
  }
アイテム内のBGとラベルの初期化をします

BasicItemRenderer.as
  protected function layout():void
  {
   this.itemBG.width = this.actualWidth;
   this.itemBG.height = this.actualHeight;

   this.itemLabel.width = this.actualWidth;
   this.itemLabel.height = this.actualHeight;
   this.itemLabel.y = (this.actualHeight / 2) - (this.itemLabel.y / 2);
  }
アイテム内のBGとラベルの大きさ、配置を設定します

BasicItemRenderer.as
  protected function commitData():void
  {
   if(this._data)
   {
    this.itemLabel.text = this._data.text.toString();
   } else {
    this.itemLabel.text = "";
   }
  }
アイテム内のラベルの表示情報を設定します

Feathers Example Listについて

このサンプルはFeathersのサイト
「How to use the Feathers List component(リンク先へ) 」
を参考にしています。
FeathersExampleのソースコード(リンク先へ)

FeathersExampleを実行すると以下の画面になります

Listの表示と設定の流れ
ボタンサンプルからListサンプルへ表示を切り替える為に
exampleIndexの値を変更します。
FeathersExample.as
  private function loaderInfo_completeHandler(event:Event):void
  {
   var exampleIndex:int;
   Starling.handleLostContext = true;
   Starling.multitouchEnabled = true;

   exampleIndex = 1;
ListExample.as
   this.removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);

   this.theme = new MetalWorksMobileTheme(this.stage);

   var list:List = new List();
   list.width = this.stage.stageWidth;
   list.height = this.stage.stageHeight;
   this.addChild(list);

   var listData:Object;
   var listTable:Array = [];
   for(var i:int=0;i<1000;i++)
   {
    listData = { text : i.toString()+":"+"アイテム" };
    listTable.push(listData);
   }
   var groceryList:ListCollection = new ListCollection(listTable);
   list.dataProvider = groceryList;
   list.itemRendererProperties.labelField = "text";
   list.itemRendererType = BasicItemRenderer;

  • テーマの設定
  • インスタンスの作成
  • レイアウト、大きさの設定
  • 画面に対してaddChildを行う
  • リストに表示する1件単位のリストデータを作成
  • ListCollectionの作成
  • dataProviderの設定
  • labelFieldプロパティの設定
  • アイテムレンダラの設定


  • Feathers Example ボタンスキンの変更

    このサンプルはFeathersのサイト
    「Extending Feathers Themes(リンク先へ)
    を参考にしています。

    スキンの状態を赤青緑の矩形に変更してみます。


    スキン用の矩形




    FeathersThemeライブラリプロジェクトにスキン用の矩形と
    カスタムテーマExtendMetalWorksThemeを追加します。

    ExtendMetalWorksTheme.as
    package feathers.themes
    {
     import feathers.controls.Button;
     import feathers.themes.MetalWorksMobileTheme;
    
     import starling.display.DisplayObjectContainer;
     import starling.display.Image;
     import starling.textures.Texture;
    
     public class ExtendMetalWorksTheme extends MetalWorksMobileTheme
     {
      public static const ALTERNATE_NAME_MY_CUSTOM_BUTTON:String = "my-custom-button";
    
      [Embed(source="./assets/buttons/blue.png")]
      public var blueClass:Class;        
    
      [Embed(source="./assets/buttons/green.png")]
      public var greenClass:Class;        
    
      [Embed(source="./assets/buttons/red.png")]
      public var redClass:Class;        
    
      public function ExtendMetalWorksTheme(root:DisplayObjectContainer, scaleToDPI:Boolean=true)
      {
       super(root, scaleToDPI);
      }
    
      override protected function initialize():void
      {
       super.initialize();
       this.setInitializerForClass(Button, myCustomButtonInitializer, ALTERNATE_NAME_MY_CUSTOM_BUTTON);
      }        
    
      private function myCustomButtonInitializer(button:Button):void
      {
       button.defaultSkin = new Image( Texture.fromBitmap(new blueClass()) );
       button.downSkin = new Image( Texture.fromBitmap(new greenClass()) );
       button.hoverSkin = new Image( Texture.fromBitmap(new redClass()) );
    
       button.defaultLabelProperties.textFormat = this.smallUILightTextFormat;
       button.disabledLabelProperties.textFormat = this.smallUIDisabledTextFormat;
       button.selectedDisabledLabelProperties.textFormat = this.smallUIDisabledTextFormat;
      }
     }
    }
    
    FeathersThemeライブラリプロジェクトをビルドします。

    FeathersExampleプロジェクトのButtonExample.as内でカスタムテーマの変更をします。
    ButtonExample.as
    protected var theme:ExtendMetalWorksTheme;
    
       themeIndex = 2;
       switch(themeIndex)
       {
        case 0:
         //this.theme = new MetalWorksMobileTheme(this.stage);
         break;
        case 1:
         //this.theme = new AeonDesktopTheme(this.stage);
         break;
        case 2:
         this.theme = new ExtendMetalWorksTheme(this.stage);
         break;
       }
    

    Feathers Example Buttonについて

    Buttonサンプルについて
    このサンプルはFeathersのサイト
    「How to use the Feathers Button component(リンク先へ)
    を参考にしています。
    FeathersExampleのソースコード(リンク先へ)

    FeathersExampleを実行すると以下の画面になります。
    テーマの設定
    Buttonの処理を行う前にテーマを設定します。
    このサンプルでは、FeathersThemeライブラリ(リンク先へ) を利用しています。
    ButtonExample.as
        case 0:
         this.theme = new MetalWorksMobileTheme(this.stage);
         break;
    
    テーマはMetalWorksMobileThemeを使用します。

    テーマの変更
    MetalWorksMobileThemeからAeonDesktopThemeに切り替えてみます。

    ButtonExample.as
    protected var theme:AeonDesktopTheme;
    
       themeIndex = 1;
       switch(themeIndex)
       {
        case 0:
         //this.theme = new MetalWorksMobileTheme(this.stage);
         break;
        case 1:
         this.theme = new AeonDesktopTheme(this.stage);
         break;
    

    Buttonの表示と操作の流れ

    ButtonExample.as
       this.button = new Button();
       this.button.label = "クリック";
       this.button.nameList.add(ExtendMetalWorksTheme.ALTERNATE_NAME_MY_CUSTOM_BUTTON);
       this.button.defaultIcon = new Image(Texture.fromBitmapData(icon.bitmapData));
       this.button.iconPosition = Button.ICON_POSITION_TOP;
       this.button.gap = 0;
       this.button.horizontalAlign = Button.HORIZONTAL_ALIGN_LEFT;
       this.button.verticalAlign = Button.VERTICAL_ALIGN_MIDDLE;
       this.button.width = 200;
       this.button.height = 80;
    
       this.button.labelOffsetX = this.button.iconOffsetX + icon.width;
       this.button.labelOffsetY = -(this.button.height/2);
    
       this.button.addEventListener(Event.TRIGGERED, button_triggeredHandler);
       this.button.addEventListener(Event.CHANGE, button_changeHandler);
       this.addChild(this.button);
    
    • インスタンスの作成
    • labelプロパティの設定
    • レイアウト、大きさの設定
    • タッチイベントの設定
    • 画面に対してaddChildを行う

    タッチイベントハンドラーの作成 ButtonExample.as
      protected function button_triggeredHandler(event:Event):void
      {
       const label:Label = new Label();
       label.text = "Hi, I'm Feathers!\nHave a nice day.";
       Callout.show(label, this.button);
      }
    

    labelOffsetについて
    labelOffsetを利用しない場合は以下のような表示になります。
    アイコンとラベルを水平に表示させる為、labelOffsetで調整をしています。

    ButtonExample.as
       this.button.labelOffsetX = this.button.iconOffsetX + icon.width;
       this.button.labelOffsetY = -(this.button.height/2);
    

    Feathers Example プロジェクトの作成3


    プロジェクトの追加に上記3つのFlexライブラリプロジェクトを選択します。

    icon.png
    プロジェクトファイルにicon.pngを追加します。


    FeathersExample-app.xml
            
    
            direct
    
    renderMode設定をdirectに設定します。

    ビルドを行います。

    Feathers Example プロジェクトの作成2

    ListExample.as
    package
    {
     import feathers.controls.List;
     import feathers.data.ListCollection;
     import feathers.themes.MetalWorksMobileTheme;
    
     import starling.display.Sprite;
     import starling.events.Event;
    
     public class ListExample extends Sprite
     {
      public function ListExample()
      {
       this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
      }
    
      protected var theme:MetalWorksMobileTheme;
    
      protected var list:List;
    
      protected function addedToStageHandler(event:Event):void
      {
       this.removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
    
       this.theme = new MetalWorksMobileTheme(this.stage);
    
       var list:List = new List();
       list.width = this.stage.stageWidth;
       list.height = this.stage.stageHeight;
       this.addChild(list);
    
       var listData:Object;
       var listTable:Array = [];
       for(var i:int=0;i<1000;i++)
       {
        listData = { text : i.toString()+":"+"アイテム" };
        listTable.push(listData);
       }
       var groceryList:ListCollection = new ListCollection(listTable);
       list.dataProvider = groceryList;
       list.itemRendererProperties.labelField = "text";
       list.itemRendererType = BasicItemRenderer;
      }
     }
    }
    
    BasicItemRenderer.as
    package
    {
     import feathers.controls.Label;
     import feathers.controls.List;
     import feathers.controls.Scroller;
     import feathers.controls.renderers.IListItemRenderer;
     import feathers.core.FeathersControl;
    
     import flash.geom.Point;
     import flash.text.TextFormat;
    
     import starling.display.Quad;
     import starling.events.Event;
     import starling.events.Touch;
     import starling.events.TouchEvent;
     import starling.events.TouchPhase;
    
     public class BasicItemRenderer extends FeathersControl implements IListItemRenderer
     {
      public function BasicItemRenderer()
      {
       this.addEventListener(TouchEvent.TOUCH,touchHandler);
       this.addEventListener(Event.REMOVED_FROM_STAGE,removedFromStageHandler);
      }
    
      protected var itemLabel:Label;
    
      protected var itemBG:Quad;
    
      protected var _index:int = -1;
    
      protected var touchPointID:Number;
    
      private static const HELPER_POINT:Point = new Point();
    
      protected var hasScrolled:Boolean = false;
    
      public function get index():int
      {
       return this._index;
      }
    
      public function set index(value:int):void
      {
       if(this._index == value) return;
       this._index = value;
       this.invalidate(INVALIDATION_FLAG_DATA);
      }
    
      protected var _owner:List;
    
      public function get owner():List
      {
       return List(this._owner);
      }
    
      public function set owner(value:List):void
      {
       if(this._owner == value) return;
       if(this._owner) this._owner.removeEventListener(Event.SCROLL, owner_scrollHandler);
       this._owner = value;
       if(this._owner) this._owner.addEventListener(Event.SCROLL, owner_scrollHandler);
       this.invalidate(INVALIDATION_FLAG_DATA);
      }
    
      protected var _data:Object;
    
      public function get data():Object
      {
       return this._data;
      }
    
      public function set data(value:Object):void
      {
       if(this._data == value)
       {
        return;
       }
       this._data = value;
       this.invalidate(INVALIDATION_FLAG_DATA);
      }
    
      protected var _isSelected:Boolean;
    
      public function get isSelected():Boolean
      {
       return this._isSelected;
      }
    
      public function set isSelected(value:Boolean):void
      {
       if(this._isSelected == value) return;
       this._isSelected = value;
       this.invalidate(INVALIDATION_FLAG_SELECTED);
       this.dispatchEventWith(Event.CHANGE);
      }
    
      override protected function initialize():void
      {
       if(!this.itemBG)
       {
        this.itemBG = new Quad(1,1,Math.floor(Math.random() * 0xffffff));
        this.addChild(this.itemBG);
       }
    
       if(!this.itemLabel)
       {
        this.itemLabel = new Label();
        this.addChild(this.itemLabel);
       }
      }
    
      override protected function draw():void
      {
       const dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
       const selectionInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SELECTED);
       var sizeInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SIZE);
    
       if(dataInvalid) this.commitData();
       sizeInvalid = this.autoSizeIfNeeded() || sizeInvalid;
       if(dataInvalid || sizeInvalid) this.layout();
      }
    
      protected function autoSizeIfNeeded():Boolean
      {
       const needsWidth:Boolean = isNaN(this.explicitWidth);
       const needsHeight:Boolean = isNaN(this.explicitHeight);
       if(!needsWidth && !needsHeight) return false;
       this.itemLabel.width = NaN;
       this.itemLabel.height = NaN;
       this.itemLabel.validate();
       var newWidth:Number = this.explicitWidth;
       if(needsWidth) newWidth = this.itemLabel.width;
       this.explicitHeight = 150;
       var newHeight:Number = this.explicitHeight;
       if(needsHeight) newHeight = this.itemLabel.height;
    
       this.itemBG.width = newWidth;
       this.itemBG.height = newHeight;
    
       return this.setSizeInternal(newWidth, newHeight, false);
      }
    
      protected function commitData():void
      {
       if(this._data)
       {
        this.itemLabel.text = this._data.text.toString();
       } else {
        this.itemLabel.text = "";
       }
      }
    
      protected function layout():void
      {
       this.itemBG.width = this.actualWidth;
       this.itemBG.height = this.actualHeight;
    
       this.itemLabel.width = this.actualWidth;
       this.itemLabel.height = this.actualHeight;
       this.itemLabel.y = (this.actualHeight / 2) - (this.itemLabel.y / 2);
      }
    
      protected function touchHandler(event:TouchEvent):void
      {
       const touches:Vector.<touch> = event.getTouches(this);
       if(touches.length == 0)
       {
        //hover has ended
        return;
       }
       if(this.touchPointID >= 0)
       {
        var touch:Touch;
        for each(var currentTouch:Touch in touches)
        {
         if(currentTouch.id == this.touchPointID)
         {
          touch = currentTouch;
          break;
         }
        }
        if(!touch)
        {
         return;
        }
        if(touch.phase == TouchPhase.ENDED)
        {
         this.touchPointID = -1;
         this.itemBG.visible = true;
         if(!this.hasScrolled)
         {                    
          touch.getLocation(this, HELPER_POINT);
          //check if the touch is still over the target
          //also, only change it if we're not selected. we're not a toggle.
          if(this.hitTest(HELPER_POINT, true) != null && !this._isSelected)
          {
           this.isSelected = true;
           trace(_index);
          }
         }
    
         return;
        }
       }
       else
       {
        for each(touch in touches)
        {
         if(touch.phase == TouchPhase.BEGAN)
         {
          this.touchPointID = touch.id;
          this.hasScrolled = false;
    
          return;
         }
        }
       }
      }
    
      protected function removedFromStageHandler(event:Event):void
      {
       this.touchPointID = -1;
      }
    
      protected function owner_scrollHandler(event:Event):void
      {
       this.hasScrolled = true;
      }        
    
     }
    }
    

    Feathers Example プロジェクトの作成1

    ボタンとリストのサンプルを作成します。
    4つのソースコードと画像を1つ用意します。
    FeathersExample.as
    package
    {
     import flash.display.Sprite;
     import flash.display.StageAlign;
     import flash.display.StageScaleMode;
     import flash.events.Event;
     import flash.geom.Rectangle;
    
     import starling.core.Starling;
     import starling.utils.HAlign;
     import starling.utils.VAlign;
    
     [SWF(width="960",height="640",frameRate="60",backgroundColor="#2f2f2f")]
     public class FeathersExample extends Sprite
     {
      public function FeathersExample()
      {
       if(this.stage)
       {
        this.stage.scaleMode = StageScaleMode.NO_SCALE;
        this.stage.align = StageAlign.TOP_LEFT;
       }
       this.mouseEnabled = this.mouseChildren = false;
       this.loaderInfo.addEventListener(Event.COMPLETE, loaderInfo_completeHandler);
      }
    
      private var _starling:Starling;
    
      private function loaderInfo_completeHandler(event:Event):void
      {
       var exampleIndex:int;
       Starling.handleLostContext = true;
       Starling.multitouchEnabled = true;
    
       exampleIndex = 0;
       switch(exampleIndex)
       {
        case 0:
         this._starling = new Starling(ButtonExample, this.stage);
         break;
        case 1:
         this._starling = new Starling(ListExample, this.stage);
         break;
       }
       this._starling.enableErrorChecking = false;
       this._starling.showStatsAt(HAlign.RIGHT,VAlign.BOTTOM,2);
       this._starling.start();
    
       this.stage.addEventListener(Event.RESIZE, stage_resizeHandler, false, int.MAX_VALUE, true);
       this.stage.addEventListener(Event.DEACTIVATE, stage_deactivateHandler, false, 0, true);
      }
    
      private function stage_resizeHandler(event:Event):void
      {
       this._starling.stage.stageWidth = this.stage.stageWidth;
       this._starling.stage.stageHeight = this.stage.stageHeight;
    
       const viewPort:Rectangle = this._starling.viewPort;
       viewPort.width = this.stage.stageWidth;
       viewPort.height = this.stage.stageHeight;
       try
       {
        this._starling.viewPort = viewPort;
       }
       catch(error:Error) {}
      }
    
      private function stage_deactivateHandler(event:Event):void
      {
       this._starling.stop();
       this.stage.addEventListener(Event.ACTIVATE, stage_activateHandler, false, 0, true);
      }
    
      private function stage_activateHandler(event:Event):void
      {
       this.stage.removeEventListener(Event.ACTIVATE, stage_activateHandler);
       this._starling.start();
      }
    
     }
    }
    

    ButtonExample.as
    package 
    {
     import feathers.controls.Button;
     import feathers.controls.Callout;
     import feathers.controls.Label;
     import feathers.themes.*;
    
     import flash.display.Bitmap;
    
     import starling.display.Image;
     import starling.display.Sprite;
     import starling.events.Event;
     import starling.textures.Texture;
    
     public class ButtonExample extends Sprite
     {
      [Embed(source='./icon.png')]
      public var ButtonIconSkin:Class;
    
      public function ButtonExample()
      {
       this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
      }
    
      protected var theme:MetalWorksMobileTheme;
    
      protected var button:Button;
    
      protected function addedToStageHandler(event:Event):void
      {
       this.removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
    
       var icon:Bitmap = new ButtonIconSkin();
       var themeIndex:int;
    
       themeIndex = 0;
       switch(themeIndex)
       {
        case 0:
         this.theme = new MetalWorksMobileTheme(this.stage);
         break;
        case 1:
         //this.theme = new AeonDesktopTheme(this.stage);
         break;
        case 2:
         //this.theme = new ExtendMetalWorksTheme(this.stage);
         break;
       }
       this.button = new Button();
       this.button.label = "クリック";
       this.button.nameList.add(ExtendMetalWorksTheme.ALTERNATE_NAME_MY_CUSTOM_BUTTON);
       this.button.defaultIcon = new Image(Texture.fromBitmapData(icon.bitmapData));
       this.button.iconPosition = Button.ICON_POSITION_TOP;
       this.button.gap = 0;
       this.button.horizontalAlign = Button.HORIZONTAL_ALIGN_LEFT;
       this.button.verticalAlign = Button.VERTICAL_ALIGN_MIDDLE;
       this.button.width = 200;
       this.button.height = 80;
    
       this.button.labelOffsetX = this.button.iconOffsetX + icon.width;
       this.button.labelOffsetY = -(this.button.height/2);
    
       this.button.addEventListener(Event.TRIGGERED, button_triggeredHandler);
       this.button.addEventListener(Event.CHANGE, button_changeHandler);
       this.addChild(this.button);
       this.button.validate();
    
       this.button.x = (this.stage.stageWidth - this.button.width) / 2;
       this.button.y = (this.stage.stageHeight - this.button.height) / 2;
      }
    
      protected function button_triggeredHandler(event:Event):void
      {
       const label:Label = new Label();
       label.text = "Hi, I'm Feathers!\nHave a nice day.";
       Callout.show(label, this.button);
      }
    
      private function button_changeHandler(event:Event):void
      {
       var button:Button = event.currentTarget as Button;
       trace( "button.isSelected has changed:",button.isSelected);
      }
     }
    }
    

    Starling FrameworkとFeathersの導入

    FeathersのUIサンプルを作る為の準備をします。
    開発環境はFlashBuilder 4.6です。

    Starling FrameworkとFeathersを
    Flexライブラリプロジェクトとして導入します。
    AIR3.5SDKを利用します。(リンク先へ)

    Starling Framework(リンク先へ)

    プロジェクト名はStarlingFrameworkとして作成します。
    StarlingFrameworkのソースコードをsrcフォルダにコピーします。
    ビルドを行った後、binフォルダにStarlingFramework.swcが出来上がります。

    プロジェクト名はFeathersとして作成します。
    Feathersのソースコードをsrcフォルダにコピーします。
    プロジェクトの追加からStarlingFrameworkを指定します。
    ビルドを行った後、binフォルダにFeathers.swcが出来上がります。

    Feathers Theme(リンク先へ)
    Feathersはテーマを1つ設定する必要があります。
    テーマはUI全体の見た目を定義する機能です。

    プロジェクト名はFeathersThemeとして作成します。
    使用したいテーマのソースコードをライブラリに追加します。
    プロジェクトの追加からStarlingFrameworkとFeathersを指定します。
    ビルドを行った後、binフォルダにFeathersTheme.swcが出来上がります。

    Feathersについて

    FeathersはStarling Frameworkを利用したUIコンポーネントです。


    用意されているUIコンポーネント一覧
    • Button
    • ButtonGroup
    • Callout
    • Check
    • GroupedList
    • Header
    • Label
    • List
    • Custom Item Renderers
    • PageIndicator
    • PickerList
    • ProgressBar
    • Radio
    • Screen
    • ScreenNavigator
    • ScreenNavigator Transitions
    • ScrollBar
    • ScrollContainer
    • Scroller
    • ScrollText
    • SimpleScrollBar
    • Slider
    • TabBar
    • TextInput
    • ToggleSwitch

    導入条件
    ・Flash PlayerまたはAdobe AIRが動作する環境 Flash(リンク先へ) AIR(リンク先へ)
    ・Flash Proffesional (リンク先へ)またはas3で開発ができる環境
    ・Starling Frameworkライブラリを用意する (リンク先へ)
    ・Feathersライブラリを用意する(リンク先へ)

    この作成手順は2013年2月時点のものです。今後仕様変更の可能性があります。
    ソースと関連ファイルの使用結果について、一切の保証と責任は負いません。

    AIR3.5 Feathers UISample

    Feathersを利用したUIのサンプル動画を作成しました。
    端末はNexus7のバージョン4.2.1です。