アニメーションしたい

画面を、1秒ごととか1分ごとに書き換えることはできるようになったので、次は、なんかアニメーションしたいですね。
時計作るにしても、時刻が変わるときに、ちょっとした動きを入れたいですからね!でもどうやってやるんでしょう?
サンプルから、それっぽいプログラムを見てみましょう。watchapps/feature_property_animation が良さそうです。アニメーションだけを行う、シンプルなサンプルっぽいです。ソース取り込んで、そのまま動かしてみましょう。


こういう感じに、ボタンを押すごとに、アニメーションが発生します。
そういえば、ボタン操作のやり方も、まだ知りませんでした。せっかくなので一緒に見ときましょう。

ソースファイルの定義から見ていくと、さっそくアニメーションっぽいものがありました。

static PropertyAnimation *s_prop_animation;

PropertyAnimation の公式ドキュメントはこちら。

Writing Pebble Apps : Display & Animations
Pebble C : User Interface : Animation

いろいろ書かれていますが、ひとまずはサンプルを追っかけます。

main() /init() /deinit() のあたりは、いままで作ってきたプログラムと同じですね。

main_window_load() で、textLayerを作成して、大きさ(0,0,60,60)に指定、”Started!”とテキストを設定してます。

GRect to_rect = GRect(84, 92, 60, 60);

で、何やら場所を指定しています。プログラム起動後、最初に表示されるテキスト位置っぽい。

s_prop_animation = property_animation_create_layer_frame(text_layer_get_layer(s_text_layer), NULL, &to_rect);

で、最初に定義した s_prop_animation を作ってます。むむむ、なんか難易度高くなった。property_animation_create_layer_frame はナニモノか。

PropertyAnimation * property_animation_create_layer_frame(struct Layer * layer, GRect * from_frame, GRect * to_frame)

layerにアニメーションさせるLayer、from_frame に最初の位置、to_frameに移動先の位置を入れるようだ。初期位置は特に無いからNULLなのか。

animation_schedule((Animation*) s_prop_animation);

main_window_load 終了。アニメーションをスケジュールに登録しているということだな。ここまでで、起動して、右端に”started!”が表示されるところまでの動作ですね。


この後の動きは、ボタンを押したときに発生します。ボタン押したときの動作定義はここ。

static void click_config_provider(void *context) {
  window_single_click_subscribe(BUTTON_ID_UP, click_handler);
  window_single_click_subscribe(BUTTON_ID_SELECT, click_handler);
  window_single_click_subscribe(BUTTON_ID_DOWN, click_handler);
}

上、真ん中(セレクト)、下のボタンが押されたときに、click_handlerが呼び出されるように指定しています。全部、呼び先は同じか。

これを init() で登録してました。

window_set_click_config_provider(s_main_window, click_config_provider);

そしてclick_handlerでは…

static void click_handler(ClickRecognizerRef recognizer, void *context) {
  Layer *layer = text_layer_get_layer(s_text_layer);

  GRect to_rect = (s_toggle) ? GRect(4, 4, 120, 60) : GRect(84, 92, 60, 60);

  s_toggle = !s_toggle;

  destroy_property_animation(&s_prop_animation);

  s_prop_animation = property_animation_create_layer_frame(layer, NULL, &to_rect);
  animation_set_duration((Animation*) s_prop_animation, 400);
  switch (click_recognizer_get_button_id(recognizer)) {
    case BUTTON_ID_UP:
      animation_set_curve((Animation*) s_prop_animation, AnimationCurveEaseOut);
      break;

    case BUTTON_ID_DOWN:
      animation_set_curve((Animation*) s_prop_animation, AnimationCurveEaseIn);
      break;

    default:
    case BUTTON_ID_SELECT:
      animation_set_curve((Animation*) s_prop_animation, AnimationCurveEaseInOut);
      break;
  }

  /*
   // Example animation parameters:
   // Duration defaults to 250 ms
   animation_set_duration(&s_prop_animation->animation, 1000);
   // Curve defaults to ease-in-out
   animation_set_curve(&s_prop_animation->animation, AnimationCurveEaseOut);
   // Delay defaults to 0 ms
   animation_set_delay(&s_prop_animation->animation, 1000);
   */

  animation_set_handlers((Animation*) s_prop_animation, (AnimationHandlers) {
    .started = (AnimationStartedHandler) animation_started,
    .stopped = (AnimationStoppedHandler) animation_stopped,
  }, NULL /* callback data */);
  animation_schedule((Animation*) s_prop_animation);
}

うおっと、わりかし長かった。ここがこのプログラムのキモですね。

まず、layerにs_text_layerを設定した後、

  GRect to_rect = (s_toggle) ? GRect(4, 4, 120, 60) : GRect(84, 92, 60, 60);
  s_toggle = !s_toggle;

関数が呼ばれるごと、つまりボタンが押されるごとに、位置を入れ替えてます。

次に…

 destroy_property_animation(&s_prop_animation);

なんじゃこれ?なんでいきなりdestroy。ボタン連打とかでアニメーションが途中の時に呼ばれた対応と思われる。おそらくそうだ。ということにして、あとまわしにします。

s_prop_animation = property_animation_create_layer_frame(layer, NULL, &to_rect);

これ、さっきもあったやつですね。あ、そうか、from_frame を NULLにすると、layerの現在位置からのアニメーションになるのか。

animation_set_duration((Animation*) s_prop_animation, 400);

アニメーションをどれぐらいの時間をかけて行うかの指定ですね。400 ms を指定しています。

そして、どのボタンが押されたかによって、動作を変えていますね。AnimationCurveの説明によれば、

AnimationCurveEaseOut:だんだんゆっくりになる
AnimationCurveEaseIn:だんだん早くなる
AnimationCurveEaseInOut:だんだん早くなって終わる前にゆっくりに戻る

のようです。だた、エミュレータだとそこまで違いが判別できないような気もする。

後ろのコメントによれば、
・Durationを指定しなければデフォルトで250 ms
・Curveを指定しなければデフォルトでAnimationCurveEaseOut
・ここでは指定しませんでしたが、ディレイ指定しなければデフォルトで 0ms
になるとのこと。

その後、コールバック設定しています。

animation_set_handlers((Animation*) s_prop_animation, (AnimationHandlers) {
    .started = (AnimationStartedHandler) animation_started,
    .stopped = (AnimationStoppedHandler) animation_stopped,
  }, NULL /* callback data */);

アニメーション開始時、終了時に呼ばれる関数の設定ですね。

開始時には animation_started で

static void animation_started(Animation *animation, void *data) {
  text_layer_set_text(s_text_layer, "Started.");
}

テキストを “Started.” に設定

終了時には、 animation_stoppedで

static void animation_stopped(Animation *animation, bool finished, void *data) {
  text_layer_set_text(s_text_layer, finished ? "Hi, I'm a TextLayer!" : "Just Stopped.");
}

アニメーションの状態によって、終わってれば”Hi, I’m a TextLayer!”に設定し、終わってなければ(どういうときだろう?)”Just Stopped.”に設定してます。

あとはスケジュール登録

animation_schedule((Animation*) s_prop_animation);

でアニメーション開始ですね。

さきほどの destroy_property_animation の中も見ときましょう。

・prop_animation が NULL ならそのまま終了。
・スケジュールに登録されていたら、スケジュールから外す。
・そして property_animation_destroy で s_prop_animation の内容破棄、
・その後、s_prop_animation に NULL設定。
というかんじ。

これ main_window_unload からも呼ばれてるんですね。中身があるかどうか、スケジュール登録されてるかどうかで、処理違ってきますからね。

アニメーションによるレイヤー移動の基本的なやり方はわかりました。ついでにボタンクリック取得もOKだ。