Technote

by sizuhiko

Flex2でCoverFlowクローンを作る (6) ~アニメーション効果を付ける

<!– more –>現在、オブジェクト倶楽部のメールマガジンで連載している「Flexで体験するリッチクライアント」を補足するTechnoteです。

第六回は、アニメーション効果を付けて表示する手順を整理します。

■今回サンプルの完成イメージ

画像をクリックしたり、スクロールバーを動かすと、CoverFlowのようにアニメーションで動きます。

前回は画像を台形加工して表示するサンプルを紹介しました。今回はその画像を左右にスクロールさせながら、アニメーションで選択・非選択の表示を変更していきます。今回も、カスタムコントロール(ImageReflector.as)とmxmlファイルの両方を修正していきます。

■ファイルの準備

プロジェクトファイルをダウンロードして、任意のディレクトリで解凍します。

  • reflector.as3proj
  • reflector.mxml
  • ImageReflector.as
  • TransformUtil.as

reflector.as3projをダブルクリックすると、FlashDevelopが実行されます。

■アニメーション効果を簡単にするAnimateProperty

Flashでは、AnimatePropertyを使うと簡単にアニメーション効果を実現することができます。AnimatePropertyには、指定したプロパティの値を開始値から終了値まで変化させる機能があります。つまり位置を移動させたり、定型的に図形を変形したい場合など、特定のプロパティを非同期に操作することができるのです。

refrector.mxmlでは以下のように記述しています。

// アニメーションを開始する
private function startAnimation(to:int):void
{
    if(_animation != null && _animation.isPlaying) {
        _animation.end();
    }
    _animation = new AnimateProperty(h_frame);
    _animation.property = "horizontalScrollPosition";
    _animation.toValue = to;
    _animation.target = h_frame;
    _animation.duration = Math.max(300,Math.abs(to - h_frame.horizontalScrollPosition));
    _animation.easingFunction = mx.effects.easing.Quadratic.easeOut;
    _animation.play();
}

_animation.propertyに指定したhorizontalScrollPositionという変数を、toの値まで変化させると記述することができます。しかもアニメーションはhorizontalScrollPositionが1ずつ加算されていくといった単純なものでなく、easingFunctionに値を指定することで増減する値をコントロールできます。easingFunctionにはFlexが用意してくれているものがあり、イーズアウト・イーズインと呼ばれています。これは変数の変化が加速なのか減速なのかといった指定ができ、イーズアウトはtoValueに近づくと値の変化が少なくなり減速感を与え、イーズインは変化が大きくなっていくことで加速感を与えることができます。AnimatePropertyクラスの値を設定したら、play()関数を呼ぶだけで勝手にhorizontalScrollPositionの値が変化していきます。

このアニメーション効果は、表示領域を滑らかにスクロールするために使用しています。具体的には、

  • 最初に中央の画像を選択状態にして、センターに表示する
  • 画像がクリックされたら、その位置を選択状態にして、センターに表示する

という機能で使っています。

// 表示領域をスクロールする
private function scrollItem(pos:int):int
{
    var scrollValue:int = 0;
    // 選択された画像の左端位置に、選択画像の幅/2を足して、表示領域の半分の幅戻ると、センターになる。
    scrollValue = imageReflector[pos].x + (imageReflector[pos].getChildAt(0).width / 2) - (h_frame.width / 2);
    startAnimation(scrollValue);
    return pos;
}

■最初に中央の画像を選択状態にして、センターに表示する

各ImageReflectorが描画されたときに呼ばれるonCreationCompleteでは、これまで最後のImageReflectorが描画されたときに、中央の選択画像を決定していました。今度のサンプルでは、initScroll()関数を呼ぶように変更しています。initScroll()関数は、中央の画像を選択状態にして、そこまでスクロールさせる役割があるのですが、中央までスクロールさせるのには、そこまでの位置が確定していなければなりません。つまり、画像の横幅が固定であれば良いのですが、CoverFlowの中で縦横サイズがバラバラな写真に対応しようと思うと、描画が完了していなければ、位置を取得することができません。そこで、

for(var i:int = 0; i < dp.length; i++) {
    if(!imageReflector[i].isCompleted()) {
        return callLater(initScroll); // まだ描画が終わってないので、遅延呼び出しする。
    }
}
selectedIndex = scrollItem(dp.length / 2);

と書くことで、全てのImageReflectorが描画完了するまで、待つことができます。callLaterはイベントキューの最後に、再度関数呼び出しを入れることで後で処理をさせることができます。そして、すべて描画できていたらscrollItemを呼んで、中央までスクロールしてやれば良いのです。

ImageReflectorの中でisCompleted関数は、

public function isCompleted():Boolean {
    if (!_invalidatedReflection) return false

    return true;
}

と書くことで、invalidatedReflectionフラグがonになるまで待ってくれます。invalidatedReflectionフラグは、amazonより画像を呼び出して、実際の描画が開始されたときにonとなるので、この時点で画像サイズなどが決定しています。

■選択画像が変わったら、パラパラ漫画のように

選択画像がカーソルで前後にしか変化しないのであれば、中央位置と左右の画像だけを変化させれば良いのですが、マウスで2つ先、3つ先が選択されるケースを想定すると、現在位置から選択位置までパラパラ漫画のように画像をめくっていく必要があります。まず移動先が右なのか、左なのか判断して、どちら方向へスクロールを展開するか決めてやります。

public function set selectedIndex(value:Number):void
{
    if(!initFlag) {
        if(_selectedIndex > value) {
            leftScroll(_selectedIndex, value);
        }
        if(_selectedIndex < value) {
            rightScroll(_selectedIndex, value);
        }
    }
}

後は実際にleftScrollやrightScroll関数で処理させます。例えば、右方法へアニメーションするには、

// 右方向へアニメーションスクロールする
private function rightScroll(from:int, to:int):void
{
    var oldSelectIndex:int = from;
    for(var i:int = from+1; i <= to; i++) {
        _selectedIndex = i;
        imageReflector[0].selectedIndex = i;
        imageReflector[oldSelectIndex].doAnimate(i - oldSelectIndex);
        imageReflector[i].doAnimate(i-i);
        oldSelectIndex = i;
    }
}

のように記述し、選択位置を変更(imageReflector[0].selectedIndex = i;)し、1つ前の選択画像(oldSelectIndex)を台形へ変形させ、新しい選択画像を台形から正面表示画像へ変形させます。変形させるときもアニメーション効果を付けるため、ImageReflectorクラスにdoAnimate()関数を用意しました。

doAnimate()関数では、mxmlの横スクロールと同じように、AnimatePropertyを使っています。ただし台形を正面表示に変更したりといった、ある変数の値を単純に変更するだけではうまくいかないケースは多々あります。そこで、ここでのギミックは、animateTransformというプロパティのsetter/getterをfunctionで定義してしまうというものです。これは前回(第5回)でもふれましたが、クラスのメンバ変数を直接操作させないための手段である一方で、トゥーイングで変化する値を受け取って処理させることもできるのです。具体的に、animateTransformの値が変化するときには、以下の関数が呼ばれます。

public function set animateTransform(value:Number):void
{
    _animateTransform = value;

    if (repeaterIndex == selectedIndex) {
        measuredWidth = value;
    }
    else {
        measuredWidth = tiltingWidth;
    }
    invalidateSize();
    transformImage(value);

}

選択画像なら、横幅をアニメーション値にし、そうでなければ台形変化後の幅にしてしまいます。後は、前回作成したtransformImage()関数へ値をアニメーション変化値を渡します。

■実際に描画する

transformImage関数を修正して、アニメーション描画を行います。

前回までと違うこところは、

var percent:Number = (contentWidth - value) / (contentWidth - tiltingWidth);

のように、どのくらいの割合で値が変化しているか%値であらわし、それをtiltingMarginにかけて変化させています。

実際、個別の描画シーンで、

x1 = xm;
x2 = value;
y1 = (maxImageSize - contentHeight) + tiltingMargin * percent;
y2 = (maxImageSize - contentHeight) + tiltingMargin * 2 * percent;
y3 = (maxImageSize - contentHeight) + contentHeight - tiltingMargin * 2 * percent;
y4 = (maxImageSize - contentHeight) + contentHeight - tiltingMargin * percent;
y5 = y3 + _reflectionShapeBitmap.height - tiltingMargin * 2 * percent;
y6 = y4 + _reflectionShapeBitmap.height - tiltingMargin;

のように、%値をかけてy1からy6までの高さを制御しています。横幅は関数の引数(アニメーション値)をそのまま使用します。 選択画像の場合は、どちらからめくったかという効果を出すために、前回の選択画像位置を_prevSelectに覚えておき、左から中央、右から中央といった分岐をします。

■クロスドメインの注意事項

前回も書いたのですが、amazonの画像サーバのcrossdomain.xmlは画像サーバ内からだけしか参照できないので、ここで作成したSWFファイルをサーバにアップロードしてもうまく表示できません。

本記事上の完成イメージは、PHPサイトでプロキシしています。

まずPHPのコードは過去記事を参考にしてください。

■次回予告

マウスイベントを取得して、画像をめくる方法を増やしてみたいと思います。

CakePHP1.2がRC2になった

<!– more –>投げていたパッチが全て取り込まれていた版になったけど、fixtureのcreateとdropにはまだ対応していないようだ。

オレオレ解決策はみつけたのだけれど、コードがかなり汚くなるし、思想的に微妙なので、これではパッチにはできない・・・

でもあのチケット見つからないので、自動create機能は将来のバージョンアップ項目になったのかしらね?

CakeにTestがやってきた

<!– more –>というタイトルでLTをしてきました。

第3回CakePHP勉強会です。

第1回には参加できていたのに、前回は既に予定があり、残念ながら参加できず、久々の参加です。

発表イメージはほぼ完成していたのに、結局スライドが完成したのは当日昼:ko:

しかも当初枠の10分ギリギリの内容&進行がおしていたこともあり、超特急でやらせてもらいました。

聴いている人は速過ぎるかな?と思いながら懇親会では暖かい声をかけていただき、ありがとうございました:cheers:

もうちょっと時間の枠があれば詳しくテストについて話せそうなので、そういう機会があればまたお邪魔したいと思います。

他の人の発表もたいへん興味深い内容ばかりで、参考になりました。エディターのプラグインは確かにあったら便利そう。PCのリソースを使いまくってブラウザでもエディタでもファイルを開きっぱなしというのは良くないのですかね?:p

あっ、SimpleTestなどについて書かれたテストのまとめ本も、もうしばらくで出ると思いますので、よろしくお願いします。:rolleyes:

それとCake1.2本にご協力いただけるかたがいましたら、こちらもよろしくお願いします。

しかしいつもながらにSlideShareにUpすると、ヒラギノ文字がダメダメになりますね。何とかして欲しい・・・・

CakePHP 1.2 RC1でTreeBehaviorの絞込み範囲指定が変更となった

<!– more –>TreeBehavior自体1.2から追加された機能なのに、変更になるなんて。。。

1.2βでは

$this->Category->setScope(array('company_id'=>'1'));
$categories = $this->Category->children();

のように書くと、company_idが1のカテゴリだけ階層構造で取得できていたのだが、RC1ではsetScopeがDeprecatedになってしまった。で、上記方法を使っている画面を表示すると、「(TreeBehavior::setScope) Deprecated - Use BehaviorCollection::attach() to re-attach with new settings」というエラーメッセージが出るが、お世辞にもどう変更していいかわかるメッセージじゃないし、ドキュメントにも書いてない。

なので、ソースを読み込むことに。まぁソース読めばわかります。結果としては、

$this->Category->$this->Behaviors->attach('Tree', array('scope'=>array('company_id'=>'1')));
$categories = $this->Category->children();

のように記述するのですが、これを使用箇所で全部変更するのは大変ですね。そこで、Categoryのモデルクラスに以下のようなメソッドを追加してあげました。

function setScope($scope = array()) {
    return $this->Behaviors->attach('Tree', array('scope'=>$scope));
}

ま、AppModelに追加しても良かったんですが、全部がTreeBehavior使っている訳ではないと思うので、その場合は、setTreeScopeのような名前に変更した方が良いかな?と思っています。

CakePHP 1.2 RC1からfindの条件指定方法が変更となった

<!– more –>CakePHP1.2 も RC1 となり、現在開発中のプロジェクトでβからRC1に変更したところ、様々なトラブルが。。。

1.2βでは1.1の検索方法を踏襲しており、以下のように記述できる。

$conditions = array("Post.title" => "This is a post");

//Example usage with a model:
$this->Post->find($conditions);

この例は完全一致(=)なので問題はなかったのだが、それ以外の<や>、LIKEやBETWEENなど記述方法が変更となっている。 なおINの場合は、

$conditions = array("Post.id" => array(1,2,3,4,5));

//Example usage with a model:
$this->Post->find($conditions);

のように記述でき、1.1などからの変更はない。 ではLIKEについてこれまでの記法をおさらいすると、

$conditions = array("Post.title" => "LIKE %post%");

//Example usage with a model:
$this->Post->find($conditions);

となっていた。しかしこれを1.2 RC1で実行すると、WHERE句は「'Post.title’ = ‘LIKE %post%'」のようになってしまう。そこで、dbo_source.phpを見て解析してみた。結果、LIKEなどの記法は

$conditions = array("Post.title LIKE" => "%post%");

//Example usage with a model:
$this->Post->find($conditions);

のように変更となっていた。この変更はかなり驚きだが、カラム名の方にLIKEや<=などの条件を移動することで、様々なSQLインジェクションを考えると、従来のようにvalue側を正規表現で切り分けるより安全な気がする。

値が1つの場合は良さそうだ。ではBETWEENの場合はどうだろう?従来は、

$conditions = array("Post.date" => "BETWEEN 2008-1-1 AND 2009-1-1");

//Example usage with a model:
$this->Post->find($conditions);

のように記述していたが、これもそのままだとWHERE句は「'Post.data’ = 'BETWEEN 2008-1-1 AND 2009-1-1'」のようになってしまう。ではLIKEと同じようにしてみると、

$conditions = array("Post.date BETWEEN" => "2008-1-1 AND 2009-1-1");

//Example usage with a model:
$this->Post->find($conditions);

WHERE句は「'Post.data’ BETWEEN '2008-1-1 AND 2009-1-1'」のようになってしまう。これも正しくない。このようなケースはさらに変更されており、以下のようになる。

$conditions = array("Post.date BETWEEN ? AND ?" => array("2008-1-1", "2009-1-1"));

//Example usage with a model:
$this->Post->find($conditions);

BETWEENなど値が複数ある場合は、カラム名上で?を使うと配列の値を順番にセットしてくれる。WHERE句は「'Post.data’ BETWEEN '2008-1-1’ AND '2009-1-1'」のように期待通りの結果となる。

なおLIKEなどでも

$conditions = array("Post.title LIKE ?" => array("%post%"));

//Example usage with a model:
$this->Post->find($conditions);

と書くことが可能だが、そこまで見栄えに統一感を持たせなくても良いかな?と思う。