ポケット暗算 リリース

脳トレアプリポケット暗算をリリースしました。

あたまのなかで計算を繰り返し、100以上になったときの値を解答する暗算脳トレアプリです。

  • 例)初期値:6、加算値:7
  • 6+7=13
  • 13+7=20
  • 20+7=27
  • 27+7=34
  • 90+7=97
  • 97+7=104(>=100)
  • 解答:104

元ネタは昔テレビ番組でやっていた脳トレです。移動中などによくやっていたのですが、最後に答えがあっているのか確かめることができないため、モヤモヤしつつも気にしないようにしていました。

今回Vue.jsの勉強中にふと思い出したことがきっかけとなり、アプリにしてみました。フレームワークはOnsen UI + Vue、スロットの部分はbounty、アニメーションはAnimate.cssGSAPPixiJS、多言語化はVue I18nを利用しました。これらのライブラリは(Animate.cssを除いて)初めて使ったのですが、結果的にVueの勉強と同じくらいたいへんなことになってしまった気がします。。。

本アプリの脳トレは単発の暗算と違いあたまのなかで計算を繰り返すため、計算結果をあたまにとどめながら更新していく必要があります。これがけっこうたいへんで、気を抜くと途中で計算結果を忘れたり桁がずれたりしてしまいます。地味でハードですが、しっかりワーキングメモリを鍛えることができます。また、最後の答え合わせもできるのでモヤモヤせずに済みますし、ほどよい緊張感が生まれたと思います。

Google Playストアで公開中ですので、よろしければご利用ください。

Google Play で手に入れよう

Cordova AdMob Freeプラグインで広告を表示

はじめに

最近知ったのですが、以前記事にしたCordova AdMobプラグインcordova-admob-proにはちょっとした問題があるようです。プラグイン作成者は無料で利用する場合は2%のユーザートラフィックを共有させてもらうだけで大丈夫ですと公表しているのですが、ユーザーが調べたところ30%共有されていたケースがあったそうです。(ユーザーに入る広告費が30%減るということみたいです)

cordova-plugin-admobpro issues #354

そこでRatsonさんが完全無料のAdMobプラグインcordova-plugin-admob-freeを開発しました(開発へ至るまでの経緯はこちら)。

Ratsonさんはcordova-plugin-admob-freeプラグインの後継となるadmob-plusプラグインも開発中です。

環境

参考にした情報

実装手順

MonacaのOnsen UI + VueプロジェクトでAdMob Freeプラグインを利用します。本内容はteratailで解説されていますので詳細についてはそちらをご参照ください。

1.AdMob SDKプラグインをダウンロード

https://gitlab.com/ratson/cordova-admob-sdk からzipファイルをダウンロードします。

2.AdMob SDKプラグインをインポート

Cordovaプラグインの設定画面からさきほどダウンロードしたzipファイル(cordova-admob-sdk-master.zip)を選択してAdMob SDKプラグインをインポートします。

3.AdMob Freeプラグインをインポート

Cordovaプラグインの設定画面からパッケージ名を入力してAdMob Freeプラグインをインポートします。

パッケージ名/URL :cordova-plugin-admob-free@0.27.0

4. ADMOB_APP_ID の設定

AdMob FreeプラグインのインストールパラメータにAdMobのアプリIDを設定します。

インストールパラメータ :ADMOB_APP_ID=ca-app-pub-1234567890…

以上でプラグインの設定は完了です。

5.サンプルコード

AdMob Freeプラグインのexamplesを参考にしています。

App.Vue
<template>
    <v-ons-page>
      <v-ons-toolbar>
        <div class="center">{{ title }}</div>
        <div class="right">
          <v-ons-toolbar-button>
            <v-ons-icon icon="ion-navicon, material: md-menu"></v-ons-icon>
          </v-ons-toolbar-button>
        </div>
      </v-ons-toolbar>
      <div style="text-align: center; padding-top:10px">Hello World!</div>
      <p style="text-align: center">
        <v-ons-button @click="banner">Banner</v-ons-button>
        <v-ons-button @click="interstitial">Interstitial</v-ons-button>
      </p>
    </v-ons-page>
</template>
<script>
  export default{
    data() {
      return {
        title: 'My app'
      };
    },
    beforeCreate () {
      this.$ons.ready(function() {
        var admobid = {
          banner: 'ca-app-pub-3940256099942544/6300978111',
          interstitial: 'ca-app-pub-3940256099942544/1033173712'
        }
        // Banner
        admob.banner.config({
          id: admobid.banner,
          isTesting: true,
          autoShow: false,
        })
        admob.banner.prepare()
          .then (() => {
            console.log('banner prepare success!')
          })
          .catch (err => {
            console.log('banner prepare error!', err)
          })

        // Interstitial
        admob.interstitial.config({
          id: admobid.interstitial,
          isTesting: true,
          autoShow: false,
        })
        admob.interstitial.prepare()
          .then (() => {
            console.log('interstitial prepare success!')
          })
          .catch (err => {
            console.log('interstitial prepare error!', err)
          })

      })  // ons.ready

      document.addEventListener('admob.banner.events.LOAD_FAIL', function(event) {
        console.log("LOAD_FAIL banner", event)
      })

      document.addEventListener('admob.interstitial.events.LOAD_FAIL', function(event) {
        console.log("LOAD_FAIL interstitial", event)
      })

      document.addEventListener('admob.interstitial.events.LOAD', function(event) {
        console.log("LOAD", event)
      })

      document.addEventListener('admob.interstitial.events.CLOSE', function(event) {
        console.log("CLOSE", event)

        admob.interstitial.prepare()
      })
    },
    methods: {
      banner () {
        admob.banner.show()
      },
      interstitial () {
        admob.interstitial.show()
      }
    }
  }
</script>

6.AdMob広告の表示確認

プロジェクトをデバッグビルドしAndroid端末にインストールして確認しました。

バナー広告

インタースティシャル広告

テスト広告について

Google AdMobのガイドには次のように記載されています。

アプリの開発やテストでは実際の広告を使用せず、必ずテスト広告を使ってください。実際の広告でテストすると、アカウントが停止される場合があります。

https://developers.google.com/admob/android/banner?hl=ja

GoogleはAdMobアカウントと関連付けられていないテスト用の広告ユニットを提供しています。以下のリンク先に記載しているサンプル広告ユニットIDを自由に利用することができます。

テスト広告 – Google AdMob > Mobile Ads SDK(Android)

広告フォーマットサンプル広告ユニット ID
バナーca-app-pub-3940256099942544/6300978111
インタースティシャルca-app-pub-3940256099942544/1033173712
インタースティシャル動画ca-app-pub-3940256099942544/8691691433
動画リワードca-app-pub-3940256099942544/5224354917
ネイティブ アドバンスca-app-pub-3940256099942544/2247696110
ネイティブ アドバンス動画ca-app-pub-3940256099942544/1044960115

特記事項(2019/9/8追記)

当時の私のケースになりますが、アプリのリリース直前にisTestingfalseにして最終確認を行ったところ、広告が表示されませんでした。原因が分からず不安だったのですが、いったんGoogle Play Consoleでベータ版としてリリースし動作確認したところ、広告が表示されました。製品版としてリリースしたアプリでも表示されました。本番用の広告を表示するためにはGoogle Playストアを介す必要があるのかもしれません。ご参考まで。

[Onsen UI + Vue] v-ons-card トランジション

Onsen UIのカードコンポーネントv-ons-cardにVueのトランジションを適用したサンプルを作成しました。

See the Pen Onsen UI Vue by senmyou (@senmyou) on CodePen.

リスト移動トランジションについて

本サンプルのトランジションはVue公式マニュアルのリスト移動トランジションを参考にしました。要素を削除すると、対象要素の透過トランジションと周辺要素の移動トランジションが同時に発生します。

じつは私はこのマニュアルの解説をなんど読んでも理解できませんでした。この直前で説明していたv-moveクラスはなぜか使われていないですし、いきなりv-enter-active/v-leave-activeクラスからtransitionプロパティが消え、かわりに謎のposition: absoluteが追加されていたりします。

なにか違うコードを載せちゃったのかなと思ったりもしましたが、なぜかうまく動きます。解せません。そこでDOM 変更ブレークポイントを使って調べてみました。

DOM変更ブレークポイント

attributes modificationsを選択すると属性の変更を追跡できます。

FLIP

transition-groupコンポーネントのトランジション処理にはFLIPが使われています。

内部で Vue は transforms を使って、前の位置から新しい位置へ要素を滑らかにトランジションさせるために FLIP と呼ばれる単純なアニメーションテクニックを使っています。

https://jp.vuejs.org/v2/guide/transitions.html#リスト移動トランジション

FLIPはFirst、Last、Invert、Playの頭文字で、アニメーションの実行前にJavaScriptで事前計算(First、Last、Invert)を行い、CSSにアニメーション処理をさせる(Play)技術です。

  • First
    • 要素の初期状態を取得
  • Last
    • 要素をアニメーション実行後の状態に設定
    • 要素の状態を取得
  • Invert
    • FirstとLastの変化量(差分)を算出
    • 要素に変化量を適用(Firstの状態に戻る)
  • Play
    • 要素にアニメーション用のクラスを追加
    • Invertで適用した変化量を削除(FirstからLastへのアニメーションが起動)

事前計算のコストは発生しますが、その分アニメーションの実行中にかかるコストがなくなるため、スムーズなアニメーションを実現できます。

デバッグ

本サンプルの3番目(真ん中)のカードを削除するケースを調べます。3番目と5番目のカードにDOM変更ブレークポイントを付け、3番目のカードの削除ボタンを押します。

3番目のカードが消えると4番目と5番目のカードがひとつ上に移動します。このとき3番目のカードにはv-leave、v-leave-to、v-leave-activeクラス、4番目と5番目のカードにはv-moveクラスが追加・削除されました。以下にブレークポイントにより停止したポイントの内容を示します。

丸数字:カード番号

  1. ③ にv-leaveを追加
  2. ③ にv-leave-activeを追加(追加した瞬間 ④ と ⑤ がひとつ上に移動。③ は ④ に隠れる)
  3. ⑤ のtransformプロパティに”translate(0px, 115.2px)”、transitionDurationプロパティに”0s”を代入
  4. ⑤ にv-moveを追加
  5. ⑤ のtransformプロパティに空文字を代入(代入した瞬間 ⑤ がひとつ上に移動)
  6. ⑤ からv-moveを削除
  7. ③ にv-leave-toを追加
  8. ③ からv-leaveを削除
  9. ③ からv-leave-toを削除
  10. ③ からv-leave-activeを削除

実際は3番目のカードと4番目&5番目のカードで別々のトランジションが同時に発生しているので、ブレークで止めることでお互いのタイミングがずれてみえることがあるかもしれませんが、それぞれのトランジションの流れは把握できそうです。

この内容をFLIPにあてはめると、No.2はアニメーションの最終的な状態になっているのでLast、No.3は5番目のカードを下に移動して元の位置に戻しているのでInvert、No.4とNo.5はアニメーション用のクラスを追加しtransformプロパティの値を空文字で消してアニメーションを発生させているのでPlayの処理に関連していると言えそうです。

次に、v-leave-activeクラスのposition: absoluteをマスクして、同じように3番目のカードを削除してみます。

  1. ③ にv-leaveを追加
  2. ③ にv-leave-activeを追加
  3. ③ にv-leave-toを追加
  4. ③ からv-leaveを削除
  5. ③ からv-leave-toを削除
  6. ③ からv-leave-activeを削除

4番目と5番目のカードはアニメーションせずに移動しました。FLIPが適用されなかったようです。v-moveクラスの追加も行われていません。

考察

以下はデバッグの結果から私が個人的に考察した内容になります。間違っている可能性がありますのでご注意ください。

要素を追加する場合はアニメーション開始前に実際に要素が追加されます。その後opacityを0から1に変化させていきます。それに対し、要素を削除する場合はアニメーション開始前に要素を削除しません。opacityを1から0に変化させていき最後に削除します。削除のケースでは、FLIPの事前計算のときに要素が実際には削除されていないため、このままでは変化量が0となりFLIPが適用されません。そこで削除する要素のpositionプロパティをabsoluteに設定してレイアウトから外すことで、実際のアニメーション実行後の状態を取得し、算出した変化量をFLIPでアニメーションさせることができるようになります。

ちなみに、v-leave-activeクラスのかわりにv-leaveクラスにposition: absoluteを記述して動作確認してみました。結果、FLIPが適用され、変化量(移動量)も正しいように見えました。ですが、v-leaveクラスはトランジション開始前に削除される仕様なので、そのタイミングでレイアウトから外していた要素が元に戻り、カードひとつ分下の位置からアニメーションが開始されているようにみえました。その点、v-leave-activeクラスはトランジション終了時に削除される仕様なので問題ないといえます。

リスト移動トランジションについての当初の疑問に回答を付けてみます。

  • 追加・削除対象要素に追加されるv-enter-active/v-leave-activeクラスと、周辺要素に追加されるv-moveクラスのtransitionプロパティの値が同じということで、共通で使用するクラスにtransitionプロパティを記述した。よって、v-enter-active/v-leave-activeクラスとv-moveクラスにtransitionプロパティを記述する必要がなくなった。(それぞれのクラスにtransitionプロパティを記述しても問題なし)
  • v-leave-activeクラスでposition: absoluteとしているのは周辺要素にFLIPを適用させるため。 FLIPの事前計算を行うときに削除する要素をabsoluteによりレイアウトから外すことでアニメーション実行後の状態を取得できるようになる。

サンプルのソースコード

本サンプルのソースコードについて2点説明します。

1.createdフックでkey属性に設定する文字列を作成しています。要素の追加や削除を行う場合、key属性に配列のインデックスを設定するとトランジションが正しくかからなくなります。配列の追加や削除を行うとインデックスが振り直され、要素とkey値の組み合わせが変更されるためです。配列内の値や文字列が使えればいいのですが、本サンプルでは一意の値がない(同じ値が使われる可能性がある)場合に対応するためcounterを使用して一意のkey値を作成しています。

created () {
  this.$ons.platform.select('android')
  this.items.forEach(item => {
    item.key = "res_" + this.counter++
  })
}

2.FLIPを適用させるためpositionプロパティをabsoluteに設定しました。このため対象となる要素やコンポーネントによっては副作用が発生することが考えられます。カードコンポーネントはmargin: 8pxに設定されているのですが、absoluteを設定すると8px下に移動します。おそらく相殺されていた上下のmarginが相殺されなくなったためと考え、最初から相殺が起きないようにmargin-topを0に設定しました。

.mycard {
  position: relative;
  transition: all 500ms ease-in;
  margin-top: 0;
}

また、absoluteにするとカードコンポーネントの幅がコンテンツの幅に縮小されるため、leftとrightプロパティを0にして対策しました。

.cards-leave-active {
  position: absolute;
  left: 0;
  right: 0;
}