プログラミング」カテゴリーアーカイブ

ぷよぷよプログラミングで右回転を実装する

前回の記事で、セガが提供している「ぷよぷよプログラミング」を紹介しました。

公式のソースコードでは、左回転はできますが右回転ができません。右回転できるようソースコードを改変します。

ぷよぷよのゲーム画面

左回転の処理を理解する

右回転できるようにするためには、すでに実装されている左回転のコードをコピーして修正すれば実現できるはずです。まずは、ソースコードを読んで左回転の処理を理解します。

前の記事でも説明していますが、game.js の loop() の中で、mode が”playing”のとき(プレイヤーの操作を判定している箇所)と、 “rotating”のとき(ぷよを回転している箇所)のコードを調べます。Player.playing() が “rotating” を返した場合に、Player.rotating() で回転していることがわかります。Player.playing() と Player.rotating() の中を見てみましょう。

function loop() {
    switch (mode) {
     ...中略...
        case "playing":
            // プレイヤーが操作する
            const action = Player.playing(frame);
            mode = action; // 'playing' 'moving' 'rotating' 'fix' のどれかが帰ってくる
            break;
     ...中略...
        case "rotating":
            if (!Player.rotating(frame)) {
                // 回転が終わったので操作可能にする
                mode = "playing";
            }
            break;

Player.playing() の中を見てみると、左回転を行う↑キーの入力判定があります。そして、操作するぷよの現在の回転角度に応じて cx, cy, canRotate をセットしています。cx は回転後の横移動の量を、cy は縦移動の量を表します。例えば、操作するぷよが左端にあるときに左回転を行うと、操作するぷよ全体を右に1つずらす必要があり、このときに cx が 1 になります。操作するぷよの左右が壁や別のぷよで囲まれている場合は、回転できないと判断し、canRotate を false にしています。canRotate が true の場合のみ、”rotating” を呼び出し元に返しています。

    static playing(frame) {
        ...中略...
        } else if (this.keyStatus.up) {
            ...中略...
            const rotation = this.puyoStatus.rotation;
            let canRotate = true;

            let cx = 0;
            let cy = 0;
            if (rotation === 0) {
                ...中略...
            }

            if (canRotate) {
                ...中略...
                return "rotating";
            }
        }

Player.rotating() の中を見ると、ぷよの現在の回転角度である this.puyoStatus.rotation に 90° を足していることがわかります。ratio が 1 になるまでは回転の途中のため false を返しており、ratio が 1 になり 90° 回転し終わったときに true を返しています。

    static rotating(frame) {
        // 回転中も自然落下はさせる
        this.falling();
        const ratio = Math.min(1, (frame - this.actionStartFrame) / Config.playerRotateFrame);
        this.puyoStatus.left = (this.rotateAfterLeft - this.rotateBeforeLeft) * ratio + this.rotateBeforeLeft;
        this.puyoStatus.rotation = this.rotateFromRotation + ratio * 90;
        this.setPuyoPosition();
        if (ratio === 1) {
            this.puyoStatus.rotation = (this.rotateFromRotation + 90) % 360;
            return false;
        }
        return true;
    }

ここまでで、左回転を行っている処理をざっくり理解できました。ここからは、右回転の処理を実装していきます。必要な処理を一度に実装するとミスに気づきにくいため、以下の手順で段階的に実装していきます。

  1. 左回転を↑キーからzキーに変更する
  2. xキーでも左回転できるようにする
  3. xキーで右回転できるようにする
  4. 単純に右回転できない場合の処理を追加する

左回転を↑キーからzキーに変更する

元のソースコードでは、↑キーを押すとぷよが左回転します。これをzキーで左回転、xキーで右回転するよう実装します。まずは↑キーの入力判定しているところを、zキーに置き換えます。zキーのキーコードは仕様によると 90 です。keyStatusオブジェクトのキーであるupをrotateLeftに名称変更し、e.keyCodeの条件を 38 から 90 に変更します。下記のソースコード以外の箇所もすべて変更します。

    static initialize() {
        // キーボードの入力を確認する
        this.keyStatus = {
            right: false,
            left: false,
            rotateLeft: false,
            down: false,
        };
        // ブラウザのキーボードの入力を取得するイベントリスナを登録する
        document.addEventListener("keydown", (e) => {
            // キーボードが押された場合
            switch (e.keyCode) {
                ...中略...
                case 90: // zキー
                    this.keyStatus.rotateLeft = true;
                    e.preventDefault();
                    return false;
                ...中略...

変更したソースコードを動かしてみましょう。↑キーは反応しなくなり、zキーで左回転できれば成功です。

xキーでも左回転できるようにする

左回転の既存の処理をそのまま利用して、xキーでも左回転できるようにします。this.keyStatus.rotateLeft に関するコードをコピーして、その直後に貼り付けます。xキーのキーコードは 88 です。下記のソースコード以外の箇所もすべて変更します。

    static initialize() {
        // キーボードの入力を確認する
        this.keyStatus = {
            right: false,
            left: false,
            rotateLeft: false,
            rotateRight: false,
            down: false,
        };
        // ブラウザのキーボードの入力を取得するイベントリスナを登録する
        document.addEventListener("keydown", (e) => {
            // キーボードが押された場合
            switch (e.keyCode) {
                ...中略...
                case 90: // zキー
                    this.keyStatus.rotateLeft = true;
                    e.preventDefault();
                    return false;
                case 88: // xキー
                    this.keyStatus.rotateRight = true;
                    e.preventDefault();
                    return false;
                case 39: // 右向きキー
                ...中略...

以下の this.keyStatus.rotateLeft で条件分岐している箇所も、まずはコピーして貼り付け、rotateRight に書き換えます。

    static playing(frame) {
        ...中略...
        } else if (this.keyStatus.rotateLeft) {
          ...中略...
        } else if (this.keyStatus.rotateRight) {
          ...中略...
        }
        ...中略...

変更したソースコードを動かしてみましょう。xキーを入力しても、zキーと同じように左回転すれば成功です。

xキーで右回転できるようにする

左回転は 90° ですが、右回転は -90° です。回転する角度を変えられるよう、Player.rotating() で 90° と記載されている箇所を変数 this.rotateAngle に置き換えます。以下のコードで this.puyoStatus.rotation に360の余りを代入していることから、this.puyoStatus.rotation は 0 ~ 360° である必要があるようです。this.rotateAngle が -90°のときに this.puyoStatus.rotation がマイナスにならないように、360を足す処理を加えています。

    static playing(frame) {
            ...中略...
        } else if (this.keyStatus.rotateLeft) {
                ...中略...
                this.puyoStatus.x += cx;
                this.rotateAngle = 90;
                const distRotation = (this.puyoStatus.rotation + this.rotateAngle) % 360;
                // 変更前:const distRotation = (this.puyoStatus.rotation + 90) % 360;
                ...中略...
    }
    static rotating(frame) {
        ...中略...
        this.puyoStatus.rotation = this.rotateFromRotation + ratio * this.rotateAngle;
        // 変更前:this.puyoStatus.rotation = this.rotateFromRotation + ratio * 90;
        this.setPuyoPosition();
        if (ratio === 1) {
            this.puyoStatus.rotation = (this.rotateFromRotation + this.rotateAngle + 360) % 360;
            // 変更前:this.puyoStatus.rotation = (this.rotateFromRotation + 90) % 360;
            return false;
        }
        return true;
    }

変更したソースコードを動かしてみましょう。zキーで左回転、xキーで右回転すれば成功です。

これで完成かと思いきや、この状態だと問題があります。ぷよを一番右に動かしてから、xキーを教えてみましょう。なんと、右回転したぷよが隠れてしまいます。期待する動きとしては、右回転時にぷよ全体が左に動くべきです。他にも、ぷよが一番左にあるときに右回転すると、ぷよ全体が右に移動してしまうといった問題もあります。次はこれらの問題を修正します。

右回転前の画面

xキー入力

右回転後の画面

単純に右回転できない場合の処理を追加する

ぷよの現在の回転角度に応じて、ぷよを左右や上下にずらす処理が必要です。以下のように、もともと左回転の処理であるコードを変更します。右回転の処理は、左回転の処理を左右反対にすればいいので、角度は左右反対に(0°→180°、180°→0°)、x軸はプラスマイナス反対に(x-1→x+1、x+1→x-1、cx=1→cx=-1)変更します。

    static playing(frame) {
        ...中略...
        } else if (this.keyStatus.rotateRight) {
            ...中略...
            if (rotation === 180) {
                // 右から上には100% 確実に回せる。何もしない
            } else if (rotation === 90) {
                // 上から右に回すときに、右にブロックがあれば左に移動する必要があるのでまず確認する
                if (y + 1 < 0 || x + 1 < 0 || x + 1 >= Config.stageCols || Stage.board[y + 1][x + 1]) {
                    if (y + 1 >= 0) {
                        // ブロックがある。右に1個ずれる
                        cx = -1;
                    }
                }
                // 左にずれる必要がある時、左にもブロックがあれば回転出来ないので確認する
                if (cx === -1) {
                    if (y + 1 < 0 || x - 1 < 0 || y + 1 >= Config.stageRows || x - 1 >= Config.stageCols || Stage.board[y + 1][x - 1]) {
                        if (y + 1 >= 0) {
                            // ブロックがある。回転出来なかった
                            canRotate = false;
                        }
                    }
                }
            } else if (rotation === 0) {
                // 右から下に回す時には、自分の下か右下にブロックがあれば1個上に引き上げる。まず下を確認する
                if (y + 2 < 0 || y + 2 >= Config.stageRows || Stage.board[y + 2][x]) {
                    if (y + 2 >= 0) {
                        // ブロックがある。上に引き上げる
                        cy = -1;
                    }
                }
                // 右下も確認する
                if (y + 2 < 0 || y + 2 >= Config.stageRows || x + 1 < 0 || Stage.board[y + 2][x + 1]) {
                    if (y + 2 >= 0) {
                        // ブロックがある。上に引き上げる
                        cy = -1;
                    }
                }
            } else if (rotation === 270) {
                // 下から左に回すときは、左にブロックがあれば右に移動する必要があるのでまず確認する
                if (y + 1 < 0 || x - 1 < 0 || x - 1 >= Config.stageCols || Stage.board[y + 1][x - 1]) {
                    if (y + 1 >= 0) {
                        // ブロックがある。左に1個ずれる
                        cx = 1;
                    }
                }
                // 右にずれる必要がある時、右にもブロックがあれば回転出来ないので確認する
                if (cx === 1) {
                    if (y + 1 < 0 || x + 1 < 0 || x + 1 >= Config.stageCols || Stage.board[y + 1][x + 1]) {
                        if (y + 1 >= 0) {
                            // ブロックがある。回転出来なかった
                            canRotate = false;
                        }
                    }
                }
            }

            if (canRotate) {
            ...中略...

これで右回転に必要な処理はすべて実装できました。ぷよが縦の状態で一番右にあるとき、一番左にある時、ぷよが下に回転する状態で別のぷよが右下にある時、などおかしな挙動にならないか確認しましょう。

おわりに

実装済みの左回転を参考にすれば、右回転も簡単に実装できるかと思っていましたが、意外と必要な処理が多く、理解することや変更する箇所が多くて大変でした。今回の変更内容は以下のGitHubのコミット履歴で確認できます。

https://github.com/shoarai/puyopuyo-programing/commit/1a1484d7f56bdaa1ad2c917320817d84a92436df

おまけ:VSCode・フォーマッタ・Webサーバ設定

公式の開発環境ではMonacaが利用できます。自分は使い慣れたVSCodeを使いたかったため、右回転の処理を追加する前に以下の設定を行いました。

以下のVSCodeのワークスペースファイルを、ルートフォルダに作成します。ファイル保存時に自動フォーマットできるよう、フォーマッタであるPrettierを使用します。このファイルをVSCodeで開いて実装します。

{
    "folders": [
        {
            "path": "."
        }
    ],
    "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "extensions": {
        "recommendations": ["esbenp.prettier-vscode"]
    }
}

以下のPrettierの設定ファイルもルートフォルダに作成します。自分のPCのディスプレイ幅に合わせて一行の文字数上限を設定し、元のソースコードと同じタブ幅を設定します。

printWidth: 150
tabWidth: 4

アプリの起動のために、簡易的なWebサーバであるhttp-serverを使います。以下のファイルに”start”コマンドとして追加します。

{
    ...中略...
    "scripts": {
        "monaca:preview": "npm run dev",
        "dev": "browser-sync start -s www/ --watch --port 8080 --ui-port 8081",
        "start": "npx http-server www"
    },
    ...中略...
}

ターミナルで以下のコマンドを実行すると、アプリが起動します。

npm start

ぷよぷよプログラミングのソースコードを理解する

作りたい落ちゲーがあり、落ちゲーの作り方を調べていました。自分にとっての落ちゲーといえば、ぷよぷよ。そこで見つけたのが、公式のセガが提供している「ぷよぷよプログラミング」です。落ちゲーの作り方を学ぶため、ぷよぷよプログラミング」のソースコードを理解したいと思います。

ソースコードは以下の公式サイトからダウンロードできます。

https://puyo.sega.jp/program_2020

アプリ画面

以下は実際のアプリ画面です。←/→キーで左/右に移動でき、↑キーで左回転できます。右回転はできないようです。アプリを起動すると思わずやってしまいますね。やっぱりぷよぷよは楽しい。

ぷよぷよのゲーム画面

起動時の処理

www/src/game.jsで、Webページの読み込みが完了したときに initialize() と loop() を実行しています。initialize() はデータの初期化であったり、キーボード入力時の処理の登録を行っているようです。loop() でゲームを開始しています。

// 起動されたときに呼ばれる関数を登録する
window.addEventListener("load", () => {
    // まずステージを整える
    initialize();

    // ゲームを開始する
    loop();
});

loop() の最後の行で、requestAnimationFrame() を実行しています。これにより、loop() が繰り返し実行されます。1/60秒後に呼び出すとコメントがありますが、仕様によると、多くのブラウザではディスプレイのリフレッシュレートに合わせて呼び出されるようです。

loop() の繰り返しの中で行う処理を、modeの値に応じて切り替えています。

function loop() {
    switch (mode) {
        case "start":
            // 最初は、もしかしたら空中にあるかもしれないぷよを自由落下させるところからスタート
            mode = "checkFall";
            break;
        case "checkFall":
            // 落ちるかどうか判定する
            if (Stage.checkFall()) {
                mode = "fall";
            } else {
        ...中略...
    }
    frame++;
    requestAnimationFrame(loop); // 1/60秒後にもう一度呼び出す
}

modeの状態遷移

modeの値がどのように変化するのかわかりやすくするため、状態遷移図に起こしてみました。

modeの状態遷移図

アプリ実行時には、ぷよはステージ上に一つもありません。よって、start → checkFall → checkErase → newPuyoへと特に何も処理をせずに遷移します。newPuyo で新しいぷよを作成し、playing でプレイヤーの操作を受け付ける状態になります。操作がなければ playing のままですが、移動や回転操作があれば moving か rotating に変わります。操作するぷよがステージの床に着いたら fix になり、checkFall に戻ります。

checkFall では、自由落下するぷよがあるかを判定し、fall で自由落下の描画を行います。自由落下とは、下段のぷよを消して上段のぷよが降ってくるなどの状態のことです。checkErase では、削除するぷよがあるかを判定し、erasing でぷよの削除を行います。

newPuyo で新しいぷよが作成できないと gameOver になり、batankyu となってばたんきゅーという文字を画面に描画します。

今後やりたいこと

mode の状態遷移がわかったことで、全体としてどんな処理を行っているのかが理解できました。今回久しぶりにぷよぷよをやったのですが、またハマりそうです。せっかくソースコードを理解したので、機能を追加していくのも楽しいかなと思っています。

Teamsのマイクミュートをワンクリックで切り替える方法

Microsoft TeamsでWeb会議をよくするのですが、マイクのミュートの切り替えが面倒に感じていました。会議中にTeamsとは別のウィンドウを開いていることが多く、Teamsのウィンドウをアクティブに戻してからミュートを切り替えるのが非常に手間でした。

今回は、Teamsのウィンドウがアクティブでなくても、ワンクリックでマイクのミュートを切り替える方法を紹介します。実施環境のOSはWindowsです。

設定手順

まずAutoHotKeyのv2.0をインストールします。以下のURLを開いてDowloadボタンからインストーラをダウンロードして、インストールします。

https://www.autohotkey.com/

ToggleMuteTeams.ahk という名前のファイルを任意の場所に作成し、以下のスクリプトを記載します。.ahkファイルはAutoHotKeyのスクリプトファイルです。

F1::toggle_mute_teams()
toggle_mute_teams() {
winList := WinGetlist("ahk_exe Teams.exe",,,)
for window in winList {
if (WinGetTitle(window) != "Microsoft Teams Notification" and WinGetTitle(window) != "") {
WinActivate(WinGetTitle(window))
Send("^+M")
sleep 20 ; sometimes it activates the new window before the sendkeys finishes, delay fixes it
break ; mute shortcut works in both windows, so break after first send
}
}
}

作成した ToggleMuteTeams.ahk をダブルクリックして起動します。これで、会議中に「F1」キーを押してTeamsのマイクミュートを切り替えられるようになりました。Teamsのウィンドウがアクティブでない状態で「F1」キーを押すと、Teamsのウィンドウがアクティブになってからマイクミュートが切り替わります。

PCを起動し直した場合は、このスクリプトを再度実行する必要があります。ToggleMuteTeams.ahk のショートカットを以下のフォルダに配置することで、PC起動時に実行されるようになります。

C:\Users\<ユーザー名>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

トラックボールのボタンへのキー割り当て

「F1」キーを押すことでマイクのミュートを切り替えられるようになりましたが、もっと簡単に、トラックボールのボタンで切り替えられるようにしました。

自分はLogicoolのトラックボールを使っていて、Logi Options+というPCアプリでボタンの動作をカスタマイズできます。トラックボールのミドルクリック(スクロールホイールを押す動作)に「F1」キーボタンを割り当てました。これにより、トラックボールから手を離さずにミドルクリックを押すことで、マイクのミュートが切り替えられるようになりました。

Logi Optionsの設定画面

ミュート切り替えボタンの変更

「F1」キーを押すには両手を使う必要があり、間違って押す可能性が低いため「F1」キーを設定しました。上記スクリプトの1行目を書き換えることで、別のキーを割り当てることもできます。AutoHotKeyのキーリストから設定できるキー名が確認できます。例えば、Insertキーに割り当てたい場合は、以下のように記載します。スクリプトを変更した場合は、変更を反映するために ToggleMuteTeams.ahk を再実行してください。

Insert::toggle_mute_teams()

補足

Teamsはチャットのウィンドウと会議のウィンドウが別に開きます。どちらのウィンドウがアクティブになっているのかAutoHotKeyは判定できないため、上記のスクリプトでは両方のウィンドウをアクティブにしてマイクミュート切り替えのボタン「Ctrl + Shift + M」を入力しています。現状では、チャットのウィンドウではこのボタンを押しても何も起こらないため問題ないですが、今後意図しない動作になる可能性があるため注意が必要です。

AutoHotKeyのスクリプトは以下のスクリプトを参考にしました。
https://github.com/randyklein/muteteams/blob/main/TeamsMute-ahkv2.ahk

自作のWebアプリをAndroidアプリ化できなかった

先日、風邪ログというWebアプリをリリースしました。自分の体調を記録して振り返り、自分がどんなときに体調が悪くなるかを理解できるアプリです。Webアプリですが、Google Playストアでもアプリを見つけてもらえるように、Androidアプリ化できるか試してみました。Androidアプリ化の方法はいくつかありますが、今回は一番さくっとできそうなWebViewを使った方法を試しました。

風邪ログのカレンダー画面
風邪ログの体調入力画面

WebViewで動かしてみる

WebViewを使ってWebアプリを動かす方法を、Android公式の記事を参考に試しました。

https://developer.android.com/guide/webapps/webview?hl=ja

まずはAndroid Studioで新規プロジェクトを作成し、Empty ActivityをTemplateとして選びます。この初期状態でアプリを実行すると、空の画面が表示されます。

まずはactivity_main.xmlにWebViewを記載します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 以下のブロックを記載 -->
    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

次に、インターネット上で動くWebアプリにアクセスできるよう、AndroidManifest.xmlに許可設定を記載します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.isolity.kazelog">

    <!-- 以下のブロックを記載 -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        ...

最後に、MainActivity.ktにWebViewの設定を記載します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 以降の行を記載
        val myWebView: WebView = findViewById(R.id.webview)
        // Webアプリ内のリンクをWebView内で開くようにする
        myWebView.webViewClient = WebViewClient()
        // JavaScriptの有効化
        myWebView.settings.javaScriptEnabled = true
        // Web Storageの有効化
        myWebView.settings.domStorageEnabled = true
        // Webアプリの読み込みのため、風邪ログのURLを指定
        myWebView.loadUrl("https://kazelog.shoarai.com")
    }
}

上記のコードでアプリを実行すると、風邪ログのトップページが表示されました。

WebView上でGoogleログインできない

ここでやっかいなのが、以下の処理です。

// Webアプリ内のリンクをWebView内で開くようにする
myWebView.webViewClient = WebViewClient()

風邪ログは、EmailによるログインやGoogleアカウントによるログインをサポートしています。ログインをボタンを押したときに、Googleのログイン画面に遷移することになるんですが、上記の設定を行わないとGoogle ChromeなどのAndroid規定のブラウザでログイン画面が開いてしまいます。上記の設定をするとこで、ログイン画面がWebView内で開くようになります。

さてここで本題なのですが、GoogleはWebView内でのログインをサポートしていません。たとえWebView内でGoogleにログインしようとしても、以下のエラー画面が表示されます。

認証エラー画面

この件に関する公式のドキュメントを探したところ、以下の記事が見つかりました。やはりWebViewからGoogleログインに必要なOAuthリクエストが許可されなくなるとのことです。

「ウェブビュー」と言われる埋め込みブラウザから Google への OAuth リクエストは許可されなくなります。

https://developers-jp.googleblog.com/2016/09/modernizing-oauth-interactions-in-native-apps.html

ということでWebアプリである風邪ログを、Androidアプリとしてリリースすることはできませんでした。Googleログインを行わないWebアプリであれば、リリースできるかもしれませんね。

WordPressをAMP化し、Gistの埋め込みに対応した

このサイトはWordPressを使って作成していますが、表示速度の向上やSEO対策のためにAMP化しました。

WordPressのAMP化

AMP化は、以下のWordPress公式プラグインを使って簡単に実現できました。

AMP

またGoogle Analyticsの設定は、「AMP → 設定 → アナリティクス」の「種類:」に「googleanalytics」と入力します。すると「JSON構成:」が自動入力されるので、「UA-」で始まるトラッキングIDを設定すればOKです。

しかし、AMP化によって一つ問題が発生しました。AMP化したことにより、Gistの埋め込みコードが表示されなくなってしまったのです。

GistのAMP対応

Gistの埋め込みコードを、AMP化したWordPressに表示する方法として、ショートコードを使う方法を紹介します。そのために、JetpackというこれまたWordPress公式プラグインを使います。

Jetpackの「設定 → 執筆」にある「ショートコードを使って作成し、人気サイトからメディアを埋め込む」にチェックを入れます。これにより、ショートコードの機能が使えるようになります。

AMP化前は、カスタムHTMLで以下のコードを記載すればGistを埋め込めました。

<script src="https://gist.github.com/shoarai/a98cf027914d43139f4dbe579f726b8b.js"></script>

AMP化後は、ショートコードで以下のテキストを記載すればGistを埋め込めます。

[gist]a98cf027914d43139f4dbe579f726b8b[/gist]

ちなみに、ショートコードはGist以外にも、Facebookの投稿やTwitterのツイートなども埋め込めます。利用可能なショートコードは以下で確認できます。
https://jetpack.com/support/shortcode-embeds/

KotlinでToastをカスタマイズする

先日、ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。このページではその中でもポップアップであるToastをカスタマイズして表示する処理について紹介します。

ポップアップアラームの設定画面
ポップアップアラームの通知画面

標準のToastを表示する

標準のToastは以下のコードで表示できます。

object ToastView {
    fun showToast(context: Context, message: String) {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
}

Toastをカスタマイズする

ポップアップアラームの通知画面

前述のような標準のToastだと目立たず、通知を見逃しやすいので、上の画像のようにToastのサイズやフォントサイズが大きい独自のレイアウトを作成します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:weightSum="1">

    <TextView
        android:id="@+id/message"
        android:layout_width="320dp"
        android:layout_height="80dp"
        android:background="@drawable/toast_shape"
        android:gravity="center"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textColor="@color/colorBase"
        android:textSize="40sp" />

</LinearLayout>

上記の独自レイアウトを表示します。

object ToastView {
    fun showToast(context: Context, message: String) {
        val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val view = inflate.inflate(R.layout.toast, null)
        val textView = view.findViewById(R.id.message) as TextView
        textView.text = text

        Toast(context).run {
            this.view = view
            duration = Toast.LENGTH_LONG
            setGravity(Gravity.BOTTOM, 0, 250)
            show()
        }
    }
}

KotlinでAndroidの時刻を検知する

先日、ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。このページではその中でも時刻の検知の処理について紹介します。

ポップアップアラームの設定画面
ポップアップアラームの通知画面

アラームを開始、停止する

Androidアプリで、ある時刻になったら処理を実行したい場合、AlarmManagerを使用します。厳密には、ある時刻になったことを通知するのではなく、ある時間を経過したことを通知できます。
今回はアラームアプリなので、ある程度正確な時間を通知できるsetExact()を呼びます。通知はBroadcastReceiverで受け取ります。
以下のクラスでは、アラームを開始して1度だけ通知します。別のアラームがすでに実行中なら、実行中のアラームは停止し、新しいアラームが開始されます。

object OnceAlarmManager {
    private const val REQUEST_CODE = 0
    private var alarmManager: AlarmManager? = null
    private var pendingIntent: PendingIntent? = null

    fun startAlarm(context: Context, calendar: Calendar) {
        alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val intent = Intent(context, AlarmBroadcastReceiver::class.java)
        pendingIntent = PendingIntent.getBroadcast(
                context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        alarmManager?.setExact(AlarmManager.RTC, calendar.timeInMillis, pendingIntent)
    }

    fun stopAlarm() {
        alarmManager?.cancel(pendingIntent)
    }
}

アラームアプリでは、1つのアラームで時刻通知したら次のアラームを開始する必要があるので、以下の通り、設定がONになっているアラームを開始します。

object WeeklyAlarmManager {
    fun startNextAlarm(context: Context) {
        var weeklyAlarms = WeeklyAlarmDataManager.weeklyAlarms
        if (WeeklyAlarmUtil.hasPowerOn(weeklyAlarms)) {
            val calendar = WeeklyAlarmUtil.getNextAlarmAsCalendar(weeklyAlarms)
            OnceAlarmManager.startAlarm(context, calendar)
        } else {
            OnceAlarmManager.stopAlarm()
        }
    }
}

アラーム通知を受け取る

前述の通り、BroadcastReceiverでアラーム通知を受け取ります。通知を受け取ったら、ポップアップを表示し、次のアラームを開始します。

class AlarmBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Show popup.
        WeeklyAlarmManager.startNextAlarm(context)
    }
}

BroadcastReceiverをアプリに登録します。

<application
    ...
    <receiver android:name=".AlarmBroadcastReceiver">
    </receiver>
</application>

端末の再起動時やタイムゾーンの変更時に、アラームを再設定する

端末が再起動されたことやタイムゾーンが変更されたことを、BroadcastReceiverで受け取れるよう、actionを設定します。

<application
    ...
    <receiver android:name=".AlarmBroadcastReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.TIMEZONE_CHANGED" />
            <action android:name="android.intent.action.TIME_SET" />
            <action android:name="android.intent.action.DATE_CHANGED" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.PACKAGE_REPLACED" />
            <data
                android:path="com.isolity.toastalarm"
                android:scheme="package" />
        </intent-filter>
    </receiver>
</application>

上記で設定したactionの通知時にも、BroadcastReceiverのonReceive()が実行されます。アラーム通知の場合のみ(ここではactionがnullのとき)、ポップアップメッセージを表示します。

class AlarmBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == null) {
            // Show popup.
        }
        WeeklyAlarmManager.startNextAlarm(context)
    }
}

改善点

上記コードの改善点として、例えば、アラーム通知の判定がaction==nullだとわかりにくいので、アクション名つけた方がいいですね。あとはDIとかテストの方法はこれから調べます。

※Huaweiのスマホでの注意事項

Huaweiのスマホでは、以下の設定をしないと、アプリをKillしたあとにAlarmManagerの処理が止まってしまい、通知を受け取れなくなってしまいます。
設定 > 詳細設定 > バッテリーマネージャー > 保護されたアプリ > 対象のアプリをONにする
アプリごとに設定が必要なので、アプリを初めてインストールしたときに設定しておきます。僕はHuaweiのP8liteというスマホを使っていて、Emulatorでは動くのに実機で動かず非常に苦労しました。。Android開発者は各端末の知識も必要で大変ですね;

KotlinでAndroidアラームアプリを作った

Androidのアラームアプリを作ったときにやったこと、調べたことを紹介します。

作ったアプリ

ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。
ストアに公開したので、ぜひ触ってみてください!

ポップアップアラームの設定画面
ポップアップアラームの通知画面

やったこと

Cordova/PhoneGapでAndroidアプリを作ったことはありますが、ネイティブで作るのは初めてなので、まずは言語仕様の理解から始めました。

  1. Kotlinの理解
  2. 開発環境の構築
  3. 実装
  4. アイコンの作成
  5. リリース

Kotlinの理解

開発言語として、JavaではなくKotlinを選びました。
型推論が欲しいこと、Null安全に興味があったこと、周囲の評判が良かったことが理由です。
※先日、KotlinがAndroidアプリ公式の開発言語に選ばれましたが、僕が選んだ後の話だったのでニュースを見たときは幸せを感じました笑
言語仕様は、その言語を書き始める前にざっと知っておいた方が効率がいいと思っているので、以下の本を読みました。

Kotlinスタートブック

その他、実装中にリスト操作について知りたいことが多かったので、以下の記事を参考にさせていただきました。

Qiita:Kotlin のコレクション使い方メモ

開発環境の構築

Android Studioのインストールは公式サイトより。
Android StudioへのKotlinの導入は、前述のKotlin本を参考にしました。取り急ぎこれで十分かと。
JavaのコードをコピペするとKotlinコードに自動的に変換されるのは、Kotlinの大きなメリットでもありますね。

実装

実装した主な機能は以下の通り。

Androidネイティブの実装は初めてということもあり、だいぶ詰まりました…
細かい部分も含めて、別ページで紹介しようと思います。

広告の実装

アプリにバナー広告を実装してみました。使ったのはGoogleのAdMobです。手順は公式サイトを参考のこと。Android Studioと連携できるのでほぼ自動で実装できました。

アイコンの作成

アイコンの作成には、Inkscapeという無料の画像編集ソフトを使いました。

リリース

リリースの際、APKに署名する必要がありますが、これもAndroid Studioからできます。
むしろ、アプリの説明文を記載するのがなかなか大変ですね。

かかった時間

ざっくりですが、開発にかかった実時間の内訳は以下のような感じ。
といっても期間としては3ヶ月ぐらいかかってます。途中、実装に詰まって手が止まりました;

  • Kotlinの理解 → 1日
  • 開発環境の構築 → 1日
  • 実装 → 2週間
  • アイコンの作成 → 半日
  • リリース → 1日

振り返り

Contextの扱いであったりResourceファイルの違いであったり、Android開発の知識が乏しく実装中に手が止まることが多かったです。実装する前にAndroidの基礎本を読んでおけばよかったかなと。
試しに広告を入れてみましたが、はじめから広告ありきで画面設計、実装しておけば、広告を表示する隙間をあとで開ける必要はなかったかなと。

IonicアプリのCordovaをアップデートする

久しぶりにGoogle Play Developer Consoleを見たら、アラートが表示されていました。
セキュリティアラート画面
アラートが出ているのはIonicで作ったアプリです。このアプリが使用しているCordovaのバージョンに脆弱性があるようで、期限内にアップロードしてね、という通知でした。

Ionicのアップデート

Ionicが依存するCordovaのバージョンは、Ionicのバージョンによって決まります。よってIonicをアップデートする必要があります。

$ npm i -g ionic

アプリのアップデート

Ionicをアップデートしても、アプリはアップデートされないため、一度アプリを消してから作り直す必要があります。まずはアプリを削除します。

$ ionic platform rm android

アプリをビルドし直します。

$ ionic build android

今後も脆弱性が見つかるかもしれないので、都度アップデートする必要がありますね。

C++によるベクトルと行列の計算クラス

大学の研究で運動機構のシミュレーションをしていた時に作った、ベクトルや行列の計算をするクラスを紹介します。GitHubにあるのでダウンロードしてみて下さい。
https://github.com/shoarai/arith

使い方

ベクトルの計算

ベクトルを含んだ計算を演算子のオーバーロードを使って実現することで、数式のようにスッキリと書けます。例では3次元ベクトルを計算していますが、z成分の値を0にすれば2次元ベクトルの計算も問題なくできます。

#include "arith/src/arith.h"
using namespace arith;
int main()
{
    // 初期化
    Vector vecA;              // x=0, y=0,  z=0
    Vector vecB(1, 10, 100);  // x=1, y=10, z=100
    // 要素の設定
    vecA.set(1, 2, 3);
    // 要素の取得
    double x = vecA.getx();
    double y = vecA.gety();
    double z = vecA.getz();
    Vector vecC;
    // ベクトル同士の加減算
    vecC  = vecA + vecB;
    vecC += vecA;
    vecC  = vecA - vecB;
    vecC -= vecA;
    int val = 10;
    // ベクトルとスカラの乗除算
    vecC  = vecA * val;
    vecC *= val;
    vecC  = vecA / val;
    vecC /= val;
    // 外積
    vecC = vecA * vecB;
    // 内積
    double inner = vecA % vecB;
    // ノルム
    double norm = vecA.norm();
    // 正規化
    vecC = vecA.normalize();
    return 0;
}

行列の計算

次は行列の計算!

#include "arith/src/arith.h"
using namespace arith;
int main
{
    // 初期化
    Matrix matA(3, 2);    // 3行2列行列
    Matrix matB(3, 2);    // 3行2列行列
    // 要素の設定
    matA(0, 1) = 10;
    matB(2, 1) = 20;
    // 要素の取得
    double a01 = matA(0, 1);
    double b21 = matB(2, 1);
    // 行列同士の加減算
    Matrix matC = matA + matB;
    matC += matA;
    matC  = matA - matB;
    matC -= matB;
    Matrix matD(2, 3);
    // 行列同士の乗算
    Matrix matE = matD * matA;
    int val = 10;
    // 行列とスカラの乗除算
    matA  = matB * val;
    matA *= val;
    matA  = matB / val;
    matA /= val;
    // 転置行列
    matA = matD.transpose();
    return 0;
}

正方行列の計算

次は行列の中でも、行と列の数が同じ正方行列の計算です。正方行列にのみ定義されている値を計算できます。

#include "arith/src/arith.h"
using namespace arith;
int main
{
    // 初期化
    SquareMatrix smatA(3);    // 3行3列の正方行列
    Matrix matA(2, 2);
    // 正方行列への型変換
    SquareMatrix smatB = matA;
    // 行列式
    double det = smatA.det();
    // 逆行列
    if (det != 0) {
        SquareMatrix smatC = smatA.invrs(det);
    }
    return 0;
}