LayoutBaseを拡張してカスタムレイアウトを作成する #1

2012.02.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今回は、Flex4のSparkコンポーネントで下図のように弧の軌跡上にカードのようなアイテムが並んでいるリストを作成したいと思います。

Flexのコンポーネントにはこのようなものはありませんので実現方法を考える必要があるのですが、その前にSparkコンポーネントのコンテナ関連について簡単におさらいしておきましょう。

Sparkコンポーネントにおけるコンテナ

Flex3以前のHaloコンポーネントでは、コンテナコンポーネントの様々な機能の実装は基本的に自クラスといくつかのサポートクラスで完結しており、一つのコンポーネントとして提供されていました。しかし、Sparkコンポーネントではいくつかのコンポーネントに責務が分散されています。

  • スクロールコンポーネント
  • IViewportを実装するコンポーネントのスクロールを管理します。スクロールやビューポートについてはFlex 4ベータ版のビューポートとスクロールの基礎で分かりやすく解説されています。
    例:Scroller

  • コンテナコンポーネント(GroupBaseサブクラス)
  • 表示リスト上の子となるコンポーネントを管理します。スクロールコンポーネントにスクロールを管理してもらうために、IViewportを実装します。
    例:Group, DataGroup

  • レイアウトコンポーネント(LayoutBaseサブクラス)
  • コンテナコンポーネントに代わって子エレメントのレイアウトを行います。
    例:BasicLayout, VerticalLayout等

また、コンテナコンポーネントは背景やボーダーなどの外観を定義する機能を持っていません。Sparkコンポーネントにおいて外観を定義する機能を持っているコンテナであるBorderContainerやListは、スキンパーツとして内部でGroupやDataGroupを利用することによってコンテナとしての機能を実現しています。

LayoutBaseを拡張したレイアウトクラスの作成

さて、話を戻しましょう。今回作成したいコンポーネントは、スクロールコンポーネントはScroller、コンテナコンポーネントはDataGroupを使用すれば問題なさそうです。ただし、弧の軌跡上に子エレメントを配置するレイアウトコンポーネントはFlexSDKには用意されていません。そこで、LayoutBaseを拡張して弧の軌跡上に子エレメントが並ぶレイアウトコンポーネントを作成します。

レイアウトコンポーネントは以下のような条件で作成します。

  • アイテムレンダラが弧の軌跡上に配置される
  • 一番上までスクロールした状態では、先頭のアイテムレンダラがリスト内の垂直位置で中心にあること
  • 一番下までスクロールした状態では、末尾のアイテムレンダラがリスト内の垂直位置で中心にあること
  • リスト内の垂直位置で中心にあるアイテムレンダラは、デフォルトでは水平位置で中心にあること
  • リスト内の垂直位置が中心に最も近いアイテムレンダラが一番手前に表示されること
  • リスト内の垂直位置の中心から遠ざかるにつれてアイテムレンダラが奥に表示されること
  • プロパティで弧の半径を設定できること
  • プロパティでアイテムレンダラの水平方向の表示位置について、リスト内の水平位置の中心からのオフセットを設定できること
  • 180°の弧の範囲内に表示されるアイテムレンダラの数をプロパティで設定できること
  • アイテムレンダラは固定サイズのものを使用

しかし、いきなり弧を描く形のレイアウトを実装しようとすると混乱してしまいますので、まずは下図のように直線上に並ぶ形のレイアウトを実装したいと思います。

今回の実装ではFlexSDK4.6.0.23201Bを利用します。

垂直方向に並べるレイアウトクラス

LayoutBaseを継承したVerticalLinearLayoutクラスを作成します。VerticalLinearLayoutのソースコードです。

VerticalLinearLayout.as

package jp.classmethod.layouts
{
    import mx.core.IVisualElement;
    import mx.core.UIComponent;
    
    import spark.layouts.supportClasses.LayoutBase;
    
    /**
     * レイアウトターゲットの子エレメントを垂直方向に一定の間隔で配置するレイアウトクラスです。
     */
    public class VerticalLinearLayout extends LayoutBase
    {
        //--------------------------------------------------------------------------
        //
        //  Constructor
        //
        //--------------------------------------------------------------------------
        
        public function VerticalLinearLayout()
        {
            super();
        }
        
        //--------------------------------------------------------------------------
        //
        //  Properties
        //
        //--------------------------------------------------------------------------
        
        //----------------------------------
        //  spacing
        //----------------------------------
        
        private var _spacing:Number = 50;
        
        [Inspectable(category="General", defaultValue="50")]

        /**
         * 子エレメントを並べる際の子エレメントの中心点の間隔です。
         * 
         * @default 50
         */
        public function get spacing():Number
        {
            return _spacing;
        }

        /**
         * @private
         */
        public function set spacing(value:Number):void
        {
            _spacing = value;
            
            if (target)
            {
                target.invalidateDisplayList();
            }
        }
        
        //--------------------------------------------------------------------------
        //
        //  Overridden methods
        //
        //--------------------------------------------------------------------------
        
        /**
         * @inheritDoc
         */
        override public function updateDisplayList(width:Number, height:Number):void
        {            
            for (var i:int = 0; i < target.numElements; i++)
            {
                var element:IVisualElement = target.getElementAt(i);
                if (!element)
                {
                    continue;
                }
                
                var elementCenterX:Number = target.width / 2;
                var elementCenterY:Number = target.height / 2 + spacing * i;
                var x:Number = elementCenterX - element.width / 2;
                var y:Number = elementCenterY - element.height / 2;
                element.setLayoutBoundsPosition(x, y);
            }
            
            var scrollHeight:Number = (target.numElements - 1) * spacing;
            var contentHeight:Number = scrollHeight + height;
            target.setContentSize(width, contentHeight);
        }
    }
}
[/as3]</p>

<p>updateDisplayListメソッドがオーバーライドされています。このメソッドは、レイアウトターゲットのupdateDisplayListから呼び出され、レイアウトターゲットの代わりに子エレメントの配置処理を行うことを目的としています。</p>

<h4>子エレメントの配置</h4>
<p>まず、子エレメントを配置する部分のコードを見ていきます。</p>
<p></p>
<p>3行目のコンテナのgetElementAtメソッドで子エレメントを取得してコンテンツ内に並べています。垂直方向は、子エレメントの中心点を基準に上から順に一定間隔で並べています。先頭のエレメントはscrollPosition=0の状態のコンテナの可視領域の中心になるよう配置され、末尾のエレメントはscrollPosition=maxScrollPositionの状態のコンテナの可視領域の中心になるよう配置されます。水平方向はセンタリングしています。この処理を子エレメントの数だけ繰り返します。</p>

<h4>コンテンツサイズの設定</h4>
<p>次に、コンテンツのサイズを設定する部分です。</p>
<p>
var scrollHeight:Number = (target.numElements - 1) * spacing;
var contentHeight:Number = scrollHeight + height;
target.setContentSize(width, contentHeight);

ここでは、コンテンツの大きさの測定とコンテナへの設定を行っていますが、これもレイアウトクラスの役割です。

まず、コンテナの垂直方向の最大スクロール量を求めます。今回作成するレイアウトは、先頭の子エレメントがコンテナの可視領域の中心にある状態から、末尾の子エレメントがコンテナの可視領域の中心にある状態までスクロールしますので、先頭の子エレメントの中心から末尾の子エレメントの中心までの距離が垂直方向の最大スクロール量となります。1行目で子エレメントの数とエレメント間の間隔から値を求めています。その値に、コンテナの可視領域の高さを加算してコンテンツの高さを算出しています。

最後にコンテンツの大きさをコンテナに設定しています。コンテナはcontentWidthとcontentHeightというコンテンツの大きさを表す読み取り専用のプロパティを持っており、レイアウトクラスはコンテナのsetContentSizeというメソッドを通してこのプロパティにコンテンツの大きさを設定することになります。コンテナにコンテンツの大きさを設定することによって、それがスクロールコンポーネントに通知され、スクロールコンポーネントの保持しているスクロールバー等が適切に設定されます。

これでレイアウトクラスの実装が終わりました。なお、子エレメントのレイアウトがコンテナのサイズに影響を与える場合はmeasureメソッドを実装する必要がありますが、今回はコンテナのサイズに影響を与えることはないので実装しません。

動作確認

さて、レイアウトクラスを作成したのでテストアプリケーションを作成して動作を確認しましょう。

LayoutTest.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:layouts="jp.classmethod.layouts.*"
               applicationComplete="application1_applicationCompleteHandler(event)">
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayList;
            import mx.collections.IList;
            import mx.events.FlexEvent;
            
            protected function application1_applicationCompleteHandler(event:FlexEvent):void
            {
                var data:IList = new ArrayList();
                for (var i:int = 0; i < 100; i++)
                {
                    data.addItem(i);
                }
                dataGroup.dataProvider = data;
            }
        ]]>
    </fx:Script>
    <s:BorderContainer width="400" height="400">
        <s:Scroller width="100%" height="100%">
            <s:DataGroup id="dataGroup" itemRenderer="jp.classmethod.itemrenderers.VerticalArcListRenderer">
                <s:layout>
                    <layouts:VerticalLinearLayout spacing="100"/>
                </s:layout>
            </s:DataGroup>
        </s:Scroller>
    </s:BorderContainer>
</s:Application>

VerticalArcListRenderer.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
                xmlns:s="library://ns.adobe.com/flex/spark"
                xmlns:mx="library://ns.adobe.com/flex/mx"
                width="250" height="150"
                mouseChildren="false">
   
    <s:Rect top="0" left="0" right="0" bottom="0">
        <s:fill>
            <s:LinearGradient>
                <s:GradientEntry color="0xaaaaaa"/>
                <s:GradientEntry color="0xcccccc"/>
            </s:LinearGradient>
        </s:fill>
        <s:filters>
            <s:DropShadowFilter knockout="true" blurX="5" blurY="5" alpha="0.32" distance="5" /> 
        </s:filters>
    </s:Rect>
   
    <s:Rect top="0" left="0" right="0" bottom="0">
        <s:stroke>
            <s:SolidColorStroke color="0xaaaaaa"/>
        </s:stroke>
        <s:fill>
            <s:LinearGradient>
                <s:GradientEntry color="0xffffff" ratio="0.75"/>
                <s:GradientEntry color="0xeeeeee"/>
            </s:LinearGradient>
        </s:fill>       
    </s:Rect>
   
    <s:Label horizontalCenter="0" verticalCenter="0" text="{data}" fontWeight="bold" fontSize="60"/>
   
    <s:Rect top="0" left="0" right="0" height="50%">
        <s:fill>
            <s:LinearGradient rotation="90">
                <s:GradientEntry color="0xffffff" alpha="0.1"/>
                <s:GradientEntry color="0xffffff" alpha="0.25"/>
            </s:LinearGradient>
        </s:fill>
    </s:Rect>
   
</s:ItemRenderer>

[SWF]http://public-blog-dev.s3.amazonaws.com/wp-content/uploads/2012/02/LayoutTest.swf, 400, 400[/SWF]

きちんとアイテムレンダラが直線上に等間隔で並んでいますね。

あとは直線上ではなく弧の軌跡上に並べるよう修正すればよさそうですが、この実装でエレメントの配置位置を弧の軌跡上に変えてしまうといくつか問題が出てきてしまいます。次回は、この問題と解決方法について考えたいと思います。