Core Animation でフェードアウト

新エンジンでは、入力モードの変更や入力セッションがアクティブになったタイミングで、Core Animation を使ってカーソル位置に入力モードアイコンを表示します。アニメーション仕様はごく簡単に「入力モードアイコンを 1 秒間表示したら 1 秒でフェードアウト」です。Core Animation はまだほとんど把握できていないので、今回のフェードアウトも実はもっと簡単に書ける方法があるのかもしれません。ご存知の方は突っ込みくれると助かります。

フェードアウトの実装

Core Animation の中心的な構成要素はレイヤーです。レイヤーは画像や文字、動画などのコンテンツを保持し、位置や不透明度といった「アニメーション可能プロパティ」を備えています。これらのプロパティを変更すると、直前の値との変化量がアニメーションとして自動的に非同期再生される仕組みです。この手軽さが Core Animation のウリの一つですね。

単純なアニメーションに満足できなければ、より緻密なインタフェースを使うこともできます。複数のレイヤーを重ねあわせたり、レイヤーに Core Image フィルターを適用することも可能。入口は広く、奥は深く、という設計思想がうかがえます。

さて、1 秒表示して 1 秒でフェードアウトという効果は、事前に予想していたよりも難題でした。

というのも、アニメーション可能プロパティを変更した時の値変化は直線的です。例えば全体が 2 秒のアニメーションで不透明度を 1.0 から 0.0 に変更すると、1 秒後の不透明度は 0.5 まで落ち込んでしまいます。これは嬉しくない。最初の 1 秒間は不透明度 1.0 をキープして次の 1 秒ですっとフェードアウトする、いわば二段階変化のアニメーションが欲しいのです。

ところが、不透明度 1.0 をキープというのはすなわち、値の変化がないということなので、レイヤーのアニメーションにならないわけです。無理矢理やるなら、

- (void)show:(NSPoint)topleft level:(int)level {
    [[self window] setFrameTopLeftPoint:topleft];
    [[self window] setLevel:level];
    [self showWindow:nil];

    // 最初の 1 秒間は普通に表示(フェードインはしない)
    [CATransaction begin];
    [CATransaction setValue:[NSNumber numberWithFloat:0.0]
                   forKey:kCATransactionAnimationDuration];
    rootLayer_.opacity = 1.0;
    [CATransaction commit];

    sleep(1); // おっとっと!

    // 次の 1 秒間でフェードアウト
    [CATransaction begin];
    [CATransaction setValue:[NSNumber numberWithFloat:1.0]
                   forKey:kCATransactionAnimationDuration];
    rootLayer_.opacity = 0.0;
    [CATransaction commit];
}

とできなくもないですが、当然 sleep 中は入力がブロックされてしまいます。これではせっかくの Core Animation が台無しです。そこで、二段階変化するアニメーションを作成する必要がありました。

        animation_ = [[CABasicAnimation animationWithKeyPath:@"opacity"] retain];
        animation_.duration = 2.0;
        animation_.fromValue = [NSNumber numberWithFloat:1.0];
        animation_.toValue = [NSNumber numberWithFloat:0];
        animation_.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.5 :0.0 :0.5 :0.0];

@"opacity"、すなわち不透明度を操作するアニメーションを生成し、全体の尺は 2 秒間で、その値を 1.0 から 0.0 まで変化させます。ただ、この変化が直線的では意味がありません。最初の 1 秒は不透明度 1.0 をキープし、次の 1 秒ですっとフェードアウトする。この要求に応えるのが、timingFunction プロパティです。

ここでは CAMediaTimingFunction の functionWithControlPoints:::: メソッドで、時間毎の値変化をベジエ曲線として指定します。始点と終点の座標はそれぞれ (0.0,0.0) と (1.0,1.0) と決められているため、引数に指定するのは制御点 1 と 2 の座標です。

こうしてできた CABasicAnimation オブジェクトを CALayer に追加すれば、一連の効果を持つアニメーションが走るわけです。もちろん、アニメーションの再生中も入力は継続できます。

- (void)show:(NSPoint)topleft level:(int)level {
    [[self window] setFrameTopLeftPoint:topleft];
    [[self window] setLevel:level];
    [self showWindow:nil];

    [rootLayer_ addAnimation:animation_ forKey:@"fadeOut"];
}

再生が終わると、CABasicAnimation オブジェクトは自動的に取り除かれます。これで、望み通りの効果が実現しました。

新エンジンの進捗

IMK 版コードを公開しました。これで今年の目標の半分は達成 ;-)

  • 移行用スクリプトの作成
  • コア入力処理部分を Facade 化
  • インストーラの作成
  • 単体テストの修正
  • プラットフォーム依存部分の練り直し
  • キーマップやかな変換ルール等の再初期化サポート

次は移行用スクリプトに着手します。