自前 Dragger #03

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

前回に引き続き、自前ドラック機構を用いた Flex の Tips を解説します。

お題「回転ボタン」

描画ツールの変形機能などに存在する回転ボタンですが、Flex の Panel コンポーネントを拡張して、Panel 自身がクルクル回る機能を実装してみましょう。

Panel の回転は、行列 (Matrix) を用います。マウスが移動するたびに Panel の中心座標とマウス座標の 2 点から角度を算出し、この値を基に行列変換処理を行い、Panel の transform.matrix プロパティにセットすることで実現可能です。

実装コード (RotatablePanel.as)

package jp.classmethod.sample {
import flash.events.MouseEvent;
import flash.geom.Matrix;
import flash.geom.Point;

import spark.components.Button;
import spark.components.Panel;

public class RotatablePanel extends Panel {
    protected const PI:Number = Math.PI;
    [SkinPart]
    public var rotateButton         :Button;
    protected var startCenterX      :int;
    protected var startCenterY      :int;
    protected var startRadians      :Number;
    protected var startRotateMatrix :Matrix;
    public function RotatablePanel() {
        super();
        minWidth  = 200;
        minHeight = 200;
        maxWidth  = 500;
        maxHeight = 500;
        setStyle("skinClass", RotatablePanelSkin);
    }
    protected override function partAdded(partName:String, instance:Object):void {
        super.partAdded(partName, instance);
        if(instance == rotateButton) {
            rotateButton.addEventListener(MouseEvent.MOUSE_DOWN, rotateButtonMouseDownHandler);
        }
    }
    protected function rotateButtonMouseDownHandler(event:MouseEvent):void {
        var gp:Point;
        var dx:int;
        var dy:int
        startRotateMatrix = transform.matrix;
        gp                = localToGlobal( new Point(width  * 0.5, height * 0.5) );
        startCenterX      = gp.x;
        startCenterY      = gp.y;
        dx                = stage.mouseX - startCenterX;
        dy                = stage.mouseY - startCenterY;
        startRadians      = Math.atan2(dy, dx);
        stage.addEventListener(MouseEvent.MOUSE_MOVE, stageRotateMouseMoveHandler);
        stage.addEventListener(MouseEvent.MOUSE_UP, stageRotateMouseUpHandler);
    }
    protected function stageRotateMouseMoveHandler(event:MouseEvent):void {
        var dx    :int    = stage.mouseX - startCenterX;
        var dy    :int    = stage.mouseY - startCenterY;
        var angle :Number = Math.atan2(dy, dx) - startRadians;
        var m     :Matrix = startRotateMatrix.clone();
        var lp    :Point  = globalToLocal( new Point(startCenterX, startCenterY) );
        rotateAroundInternalPoint(m, width * 0.5, height * 0.5, angle);
        transform.matrix = m;
        event.updateAfterEvent();
    }
    protected function stageRotateMouseUpHandler(event:MouseEvent):void {
        stage.removeEventListener(MouseEvent.MOUSE_MOVE, stageRotateMouseMoveHandler);
        stage.removeEventListener(MouseEvent.MOUSE_UP, stageRotateMouseUpHandler);
    }
    protected function rotateAroundInternalPoint(m:Matrix, x:Number, y:Number, radians:Number):void {
        var p:Point;
        p    = new Point(x, y);
        p    = m.transformPoint(p);
        m.tx = m.tx - p.x;
        m.ty = m.ty - p.y;
        m.rotate(radians);
        m.tx = m.tx + p.x;
        m.ty = m.ty + p.y;
    }
}
}

実装コード (RotatablePanelSkin.mxml)

<?xml version="1.0" encoding="utf-8"?>
<s:Skin
xmlns:fx                     = "http://ns.adobe.com/mxml/2009"
xmlns:s                      = "library://ns.adobe.com/flex/spark"
mouseEnabled                 = "false"
minWidth                     = "131"
minHeight                    = "127"
alpha.disabled               = "0.5"
alpha.disabledWithControlBar = "0.5"
>

<fx:Metadata>
<![CDATA[
[HostComponent("jp.classmethod.sample.RotatablePanel")]
]]>
</fx:Metadata>

<s:states>
    <s:State name="normal" />
    <s:State name="disabled" />
    <s:State name="normalWithControlBar" stateGroups="withControls" />
    <s:State name="disabledWithControlBar" stateGroups="withControls" />
</s:states>

<s:Group left="0" right="0" top="0" bottom="0">

    <s:Group id="topGroupMask" left="1" top="1" right="1" bottom="1">
        <s:Rect id="topMaskRect" left="0" top="0" right="0" bottom="0">
            <s:fill>
                <s:SolidColor alpha="0" />
            </s:fill>
        </s:Rect>
    </s:Group>

    <s:Group id="bottomGroupMask" left="1" top="1" right="1" bottom="1"
             includeIn="normalWithControlBar, disabledWithControlBar">
        <s:Rect id="bottomMaskRect" left="0" top="0" right="0" bottom="0">
            <s:fill>
                <s:SolidColor alpha="0" />
            </s:fill>
        </s:Rect>
    </s:Group>

    <s:Rect id="border" left="0" right="0" top="0" bottom="0" >
        <s:stroke>
            <s:SolidColorStroke id="borderStroke" weight="1" />
        </s:stroke>
    </s:Rect>

    <s:Rect id="background" left="1" top="1" right="1" bottom="1">
        <s:fill>
            <s:SolidColor id="backgroundFill" color="#FFFFFF" />
        </s:fill>
    </s:Rect>

    <s:Group id="contents" left="1" right="1" top="1" bottom="1">

        <s:layout>
            <s:VerticalLayout gap="0" horizontalAlign="justify" />
        </s:layout>

        <s:Group id="topGroup" mask="{topGroupMask}">

            <s:Rect id="tbFill" left="0" right="0" top="0" bottom="1">
                <s:fill>
                    <s:LinearGradient rotation="90">
                        <s:GradientEntry color="0xE2E2E2" />
                        <s:GradientEntry color="0xD9D9D9" />
                    </s:LinearGradient>
                </s:fill>
            </s:Rect>

            <s:Rect id="tbHilite" left="0" right="0" top="0" bottom="0">
                <s:stroke>
                    <s:LinearGradientStroke rotation="90" weight="1">
                        <s:GradientEntry color="0xEAEAEA" />
                        <s:GradientEntry color="0xD9D9D9" />
                    </s:LinearGradientStroke>
                </s:stroke>
            </s:Rect>

            <s:Rect id="tbDiv" left="0" right="0" height="1" bottom="0">
                <s:fill>
                    <s:SolidColor color="0xC0C0C0" />
                </s:fill>
            </s:Rect>

            <s:Label
                id="titleDisplay" left="9" right="3" top="1" bottom="0" minHeight="30"
                maxDisplayedLines="1" verticalAlign="middle" textAlign="start" fontWeight="bold" />

        </s:Group>

        <s:Group id="contentGroup" width="100%" height="100%" minWidth="0" minHeight="0" />

    </s:Group>

    <s:Button
        id="rotateButton"
        width="30" height="30" bottom="0" right="0"
        buttonMode="true" chromeColor="0x000099" />

</s:Group>

</s:Skin>

実装コード (Main.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:cm        = "jp.classmethod.sample.*"
backgroundAlpha = "1"
backgroundColor = "0x333333"
>

<cm:RotatablePanel width="200" height="200" verticalCenter="0" horizontalCenter="-150">
    <cm:layout>
        <s:VerticalLayout paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="40" />
    </cm:layout>
    <s:Button width="100%" height="100%" />
    <s:Button width="100%" height="100%" />
    <s:Button width="100%" height="100%" />
    <s:Button width="100%" height="100%" />
</cm:RotatablePanel>

<cm:RotatablePanel width="200" height="200" verticalCenter="0" horizontalCenter="150">
    <cm:layout>
        <s:VerticalLayout paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="40" />
    </cm:layout>
    <s:Button width="100%" height="100%" />
    <s:Button width="100%" height="100%" />
    <s:Button width="100%" height="100%" />
    <s:Button width="100%" height="100%" />
</cm:RotatablePanel>

</s:Application>

出力結果

出力結果は次の通り

This movie requires Flash Player 9