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

2012.03.06

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

直線から弧に変更する上での問題点

前回、直線上に一定間隔でエレメントを並べるレイアウトクラスを作成しました。あとはこれを直線上ではなく弧の上に並べるように変更すればよさそうです。しかし、この実装のまま弧の軌跡に直してしまうといくつか問題が出てきます。

子エレメントの配置位置が一周すると重なって表示されてしまう

先ほど、VerticalLinearLayout#updateDisplayList()内で全ての子エレメントを配置する処理をしてしまったため、実際にデータ分だけ子エレメントが生成されて配置されている状態になっています。直線上に並べる分には問題はないのですが、弧の上に並べていくと最後は円になり、最初に子エレメントを配置した場所に再度別のエレメントが配置されてしまいます。

スクロールが実現できない

前回は縦長のコンテンツにすべての子エレメントを配置し、コンテナのクリッピング矩形を垂直方向に移動することによってスクロールを実現していました。通常スクロールを実現させる際にはコンテンツ内でクリッピング矩形を移動させる方法を用いるのですが、弧の軌跡上に子エレメントを配置した場合は、軌跡に沿ってクリッピング矩形を移動させてしまうと下図のように子エレメントの並ぶ方向が移動につれて変化してしまうことになります。

仮想レイアウト対応

まず、エレメントが一周して重なってしまう問題から解決します。この問題は、仮想レイアウトを利用することで解決します。仮想レイアウトを利用する場合、現在のスクロール位置からクリッピング矩形内に表示される子エレメントを割り出し、それらの子エレメントのみを実際に配置します。スクロール位置周辺の子エレメントのみが配置されるので、子エレメントが重なる心配がなくなります。

では、先ほどの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
        {            
            if (useVirtualLayout)
            {
                updateDisplayListVirtual(width, height);
            }
            else
            {
                updateDisplayListReal(width, height);
            }
        }
        
        /**
         * @inheritDoc
         */
        override protected function scrollPositionChanged():void
        {
            super.scrollPositionChanged();
            
            if (useVirtualLayout && target)
            {
                target.invalidateDisplayList();
            }
        }
        
        //--------------------------------------------------------------------------
        //
        //  Methods
        //
        //--------------------------------------------------------------------------
        
        /**
         * 仮想レイアウトを使用しない場合の子エレメントレイアウト処理です。
         */
        private function updateDisplayListReal(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, false);
            }
            
            var scrollHeight:Number = (target.numElements - 1) * spacing;
            var contentHeight:Number = scrollHeight + height;
            target.setContentSize(width, contentHeight);
        }
        
        /**
         * 仮想レイアウトを使用する場合の子エレメントレイアウト処理です。
         */
        private function updateDisplayListVirtual(width:Number, height:Number):void
        {
            var centerElementIndex:Number = Math.round(verticalScrollPosition / spacing);
            var visibleElementRange:int = Math.ceil((Math.ceil(height / spacing) + 3) / 2) ;
            var startIndex:int = centerElementIndex - visibleElementRange;
            startIndex = Math.max(startIndex, 0);
            startIndex = Math.min(startIndex, target.numElements - 1);
            var endIndex:int = centerElementIndex + visibleElementRange;
            endIndex = Math.max(endIndex, 0);
            endIndex = Math.min(endIndex, target.numElements - 1);
            
            for (var i:int = startIndex; i <= endIndex; i++)
            {
                var element:IVisualElement = target.getVirtualElementAt(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, false);          
            }
            
            var scrollHeight:Number = (target.numElements - 1) * spacing;
            var contentHeight:Number = scrollHeight + height;
            target.setContentSize(width, contentHeight);
        }
    }
}
[/as3]</p>
<p>ソースコードを順に見ていきます。</p>

<h3>子エレメントの配置</h3>
<p>useVirtualLayoutプロパティは仮想レイアウトを使用するかを表すプロパティで、レイアウトクラスはこのプロパティを参照して仮想レイアウトを使用する場合のロジックとそうでない場合のロジックを分岐させる実装をする必要があります。updateDisplayListメソッド内でもこのプロパティによって処理を分岐させています。仮想レイアウトを使用しない場合は、updateDisplayListRealメソッドが呼び出されます。このメソッドの処理は先ほどupdateDisplayListに記述してあったものをそのまま移動しています。仮想レイアウトを使用する場合はupdateDisplayListVirtualメソッドが呼び出されます。このメソッド内の処理を詳しく見ていきましょう。</p>
<p>まずは描画対象の子エレメントのインデックスを取得する部分です。</p>
<p></p>
<p>この部分は仮想レイアウトを使用しないときとほぼ同じ処理になりますが、一部処理が異なります。まず、先ほど求めた開始インデックスから終了インデックスの間のみがfor文内部で処理されるよう変更しています。これにより、startIndexからendIndexまでの間の表示対象エレメントのみ配置処理が行なわれるようになっています。また、3行目のレイアウトターゲットからの子エレメント取得メソッドが仮想レイアウト用のgetVirtualElementAtに変更されています。DataGroupでは、このメソッドはアイテムレンダラを再利用するよう実装されています。</p>

<h3>スクロール時の再配置</h3>
<p>また、仮想レイアウトを使用する場合、スクロール位置が変化する度に再配置をする必要があります。そこで、scrollPositionChangedメソッドをオーバーライドして、仮想レイアウトの場合はレイアウトターゲットの描画更新をトリガしてupdateDisplayListが呼び出されるようにします。scrollPositionChangedメソッドは、メソッド名のとおりスクロールコンポーネントによってスクロールポジションが変更された際に呼び出されます。</p>
<p></p>
<h3>子エレメントの配置</h3>
<p>updateDisplayListVirtualメソッドを見ていきましょう。最初の描画対象の子エレメントのインデックスを取得する部分は同じです。子エレメントを配置する部分です。</p>
<p></p>
<p>ここの実装も先ほどとほぼ変わりません。10行目で、エレメントのY座標位置を現在のスクロール位置を基準に設定するよう変更しているだけです。スクロールコンポーネントによって下方向にスクロールされた際に、その分、子エレメントの配置位置を上に移動させることによってスクロールを表現しています。</p>
<p>ついでに、要件の一つにある中央のエレメントが一番手前に表示されるという使用を満たすために15行目でエレメントのdepthプロパティの設定を行っています。中央のエレメントのインデックスと対象エレメントのインデックスの差が大きいほど、奥に表示されるようになっています。</p>
<p>コンテンツの大きさの測定とコンテナへの設定処理は、先ほどと全く同じです。今回はクリッピング矩形を動かさないので実質的なコンテンツ領域はクリッピング矩形とその周辺の領域になります。しかし、スクロールコンポーネントはコンテンツの大きさとコンテナの大きさから最大スクロール量を計算していますので、コンテンツの大きさを実際にエレメントを配置した場合に必要な分を設定してスクロールコンポーネントがうまく機能するようにしています。</p>

<h3>クリッピング矩形の固定</h3>
<p>スクロール処理部分のソースコードです。まずはupdateScrollRectメソッドです。</p>
<p>
override public function updateScrollRect(w:Number, h:Number):void
{
    var g:GroupBase = target;
    if (!g)
    {
        return;
    }

    if (clipAndEnableScrolling && !useVirtualLayout)
    {
        var vsp:Number = verticalScrollPosition;
        g.scrollRect = new Rectangle(0, vsp, w, h);
    }
    else if (clipAndEnableScrolling && useVirtualLayout)
    {
        g.scrollRect = new Rectangle(0, 0, w, h);
    }
    else
    {
        g.scrollRect = null;
    }
}

updateScrollRectメソッドでは、通常スクロールポジションに応じてクリッピング矩形を移動させる処理をしています。仮想レイアウトを利用しない場合は、スクロールポジションが変更されたときは実際にクリッピング矩形を移動させる必要があります。RectangleのY座標にverticalScrollPostionをセットして垂直位置をスクロールポジションの状態に合わせています。ただし、このレイアウトでは水平スクロールはサポートしないので、RectangleのX座標は常に0に設定しています。そして、今回は子エレメントを動かすことによってスクロールを実現しますので、仮想レイアウトを利用する場合はクリッピング矩形は一切動かさないよう実装しています。clipAndEnableScrollingプロパティがfalseの場合は、クリップもスクロールもしないので、クリッピング矩形は設定しません。scrollRectにnullをセットすると子エレメントがクリッピングされなくなります。

scrollPositionChangedメソッドです。

override protected function scrollPositionChanged():void
{
    if (useVirtualLayout)
    {
        if (target)
        {
            target.invalidateDisplayList();
        }
    }
    else
    {
        super.scrollPositionChanged();
    }
}

LayoutBaseでは、このメソッドが呼び出されるとupdateScrollRectメソッドを呼び出すよう実装されています。これにより、通常のレイアウトではスクロールポジションが変化するとスクロール矩形が移動され、コンテンツがスクロールされています。しかし、今回仮想レイアウトを使用する場合はスクロール矩形を移動しません。ただし、子エレメントの移動が必要なので描画更新をトリガします。

動作確認

これでスクロール部分の修正も完了しました。サンプルアプリケーションで動作を確認してみます。

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

きちんとスクロールしていることが確認できると思います。

これで弧の軌跡上に子エレメントを配置する際の課題をクリアすることができました。次回はいよいよ弧の軌跡上を移動するレイアウトを作成したいと思います。