GoだけでAndroidアプリを作る その3 〜タッチイベントとライフサイクル〜

連載目次

はじめに

@tenntennです。

前回の記事では,spriteパッケージといくつかのイベントについて扱いました。前回の記事を読んでいただいた方は,Android上で画像の描画と移動,回転,拡大/縮小を行うコードをGoだけで書けるようになったのではないかと思います。

なお,前回の記事を執筆後にevent/configevent/sizeに変更されました。確かにサイズに関する情報しかconfig.Eventには含まれていなかったので,納得できる名前変更です。

また,GoチームによってGo Mobileの情報が徐々に整理されてきました。GoのWikiに導入の記事が追加され,リポジトリにもgomobile bindを使ったSDKアプリケーションのサンプルが追加されました。

さて,今回は前回扱わなかった,event/touchevent/lifecycleについて扱おうと思います。この記事を読めば,Go Mobileでタッチイベントを使った簡単なゲームなら実装できるようになるでしょう。

この記事は,2015年8月16日時点の情報を基に執筆しております。これからもしばらくは,event/configのような破壊的な変更が加わる可能性は十分ありますので,ご注意ください。なお,この記事を執筆当時の最新のコミットはbb4db21edcf86f8e35d2401be92605acd79c8d10です。そのため,記事で参照するGo Mobileのリポジトリのリンクは,このコミットを基準にしております。また,この記事にあるソースコードの一部はGo Mobileのリポジトリから引用したものです。

タッチイベント

第1回目の記事で動かしてみたexample/basicを覚えていますでしょうか?このサンプルでは,画面をタッチするとタッチした位置に,緑の三角形が描画されていたと思います。

example/basicをAndroidで動かしてみた

Go Mobileでは,画面をタッチするとtouch.Eventが送られてきます。このサンプルでは,送られてきたtouch.Eventをハンドルしてタッチした位置を取得し,その位置に三角形を描画しています。

前回の記事で説明した通り,イベントをハンドルするには,app.AppEventsメソッドで取得されるチャネルからイベントを受信します。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            var sz size.Event
            switch e := app.Filter(e).(type) {
                case size.Event:
                    sz = e
                case touch.Event:
                    fmt.Println(e.X, e.Y)
            }
        }
    })
}

タッチイベントを表すtouch.Eventは以下のように定義されています。

// Event is a touch event.
type Event struct {
    // X and Y are the touch location, in pixels.
    X, Y float32

    // Sequence is the sequence number. The same number is shared by all events
    // in a sequence. A sequence begins with a single TypeBegin, is followed by
    // zero or more TypeMoves, and ends with a single TypeEnd. A Sequence
    // distinguishes concurrent sequences but its value is subsequently reused.
    Sequence Sequence

    // Type is the touch type.
    Type Type
}

XYはご想像の通り、タッチした座標です。これらの値はピクセル単位なので,spriteパッケージで使われるgeom.Pt単位にするには,size.Event.PixelsPerPtを基に計算してやる必要があります。

Sequenceは,一連のタッチイベントに付けられるシーケンス番号です。指を同時に画面にタッチすると、タッチイベントが同時に複数発生します。そのため,シーケンス番号は、それらを区別するために利用します。

Typeには,タッチイベントの種類を表す値が入っています。タッチイベントの種類は,TypeBeginTypeMoveTypeEndの3種類が存在し,以下のように定義されています。

const (
    // TypeBegin is a user first touching the device.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_DOWN.
    // On iOS, this is a call to touchesBegan.
    TypeBegin Type = iota

    // TypeMove is a user dragging across the device.
    //
    // A TypeMove is delivered between a TypeBegin and TypeEnd.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_MOVE.
    // On iOS, this is a call to touchesMoved.
    TypeMove

    // TypeEnd is a user no longer touching the device.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_UP.
    // On iOS, this is a call to touchesEnded.
    TypeEnd
)

1本の指を画面に置いてから,離すまでの間に複数のタッチイベントが発生します。それらの一連のタッチイベントは同じシーケンス番号が振られ,指がどの状態にあるかはTypeで取得します。画面に指を置いた瞬間にTypeTypeBeginのタッチイベントが発生し,指を動かす度にTypeTypeMoveのイベントが発生します。画面から指を離すと,TypeTypeEndのイベントが発生し,一連のシーケンスのタッチイベントが終了します。

なお,シーケンス番号は常に一意である訳ではなく,そのアプリの起動中に何度も同じシーケンス番号のイベントが送られてきます。現在の実装では,画面に指を置いた順にシーケンス番号を振られ,指を離すとそのシーケンス番号は開放され、再度別の指を置くと同じシーケンス番号が使いまわされます。つまり,新しくシーケンス番号を振る場合は,現在どの指にも振られていない0以上のもっとも小さい数を振るように実装されています。

タッチイベントは基礎的な情報しか提供していませんが,これらをうまく使うことでドラッグやフリックなどを実装することができます。
Webフロントエンドのタッチイベントのライブラリなどを参考にして、高レベルなタッチイベントを行うライブラリを実装しても面白いでしょう。

ライフサイクル

Go Mobileにおいて,アプリのライフサイクルはlifecycle.Eventで表わされます。Go Mobileでは,複数のプラットフォームを一元的に扱うためOnStartOnStopなどのAndroidのActivityのライフサイクルとは一致していません。

lifecycle.Eventは以下のように定義されています。

// Event is a lifecycle change from an old stage to a new stage.
type Event struct {
    From, To Stage
}

ライフサイクル中のある状態をStageという型で表現し,lifecycle.Eventは,とあるStageから別のStageに移ることを表しています。Stageには,StageDeadStageAliveStageVisibleStageFocusedの4つがあり,以下のように定義されています。

const (
    // StageDead is the zero stage. No lifecycle change crosses this stage,
    // but:
    //  - A positive change from this stage is the very first lifecycle change.
    //  - A negative change to this stage is the very last lifecycle change.
    StageDead Stage = iota

    // StageAlive means that the app is alive.
    //  - A positive cross means that the app has been created.
    //  - A negative cross means that the app is being destroyed.
    // Each cross, either from or to StageDead, will occur only once.
    // On Android, these correspond to onCreate and onDestroy.
    StageAlive

    // StageVisible means that the app window is visible.
    //  - A positive cross means that the app window has become visible.
    //  - A negative cross means that the app window has become invisible.
    // On Android, these correspond to onStart and onStop.
    // On Desktop, an app window can become invisible if e.g. it is minimized,
    // unmapped, or not on a visible workspace.
    StageVisible

    // StageFocused means that the app window has the focus.
    //  - A positive cross means that the app window has gained the focus.
    //  - A negative cross means that the app window has lost the focus.
    // On Android, these correspond to onResume and onFreeze.
    StageFocused
)

ステージはStageDeadから始まり,アプリをサスペンドしたり,レジュームしたりすると変わります。各ステージがAndroidのライフサイクルのどの部分にあたるのかは、上記のStageの定義にコメントとして書いてあります。

表にまとめると以下のようになります。なお,「順方向」や「逆方向」は,そのステージ「へ」遷移した(順方向)か,そのステージ「から」遷移した(逆方向)かを表しています。詳細は後述のCrossesメソッドの説明で行います。

ステージ名 意味 対応するAndroidのライフサイクル
StageDead 初期状態 なし
StageAlive アプリの起動している 正方向:onCreate,逆方向:onDestroy
StageVisible ウィンドウの表示されている 正方向:onStart,逆方向:onStop
StageFocused ウィンドウが選択されている 正方向:onResume,逆方向:onFreeze

執筆当時はMacとAndroidでステージの遷移の仕方が違っていました。
たとえば,Androidではステージは以下の表のように変化します。

操作 ステージの遷移
アプリを起動する StageDead->StageFocused
ホームボタンを押す StageFocused->StageAlive
アプリに戻る StageAlive->StageFocused

一方,Macの場合は以下のように遷移します。

操作 ステージの遷移
アプリを起動する StageDead->StageAlive->StageVisible->StageFocused
別のウィンドウを選択 StageFocused->StageVisible
アプリのウィンドウを選択 StageVisible->StageFocused
アプリのウィンドウを閉じる StageFocused->StageAlive->StageVisible
アプリを終了(ウィンドウを閉じてる場合) StageVisible->StageDead
アプリを終了(ウィンドウを開いている場合) StageFocused->StageDead->StageAlive

このようにAndroidの場合はStageVisibleになることはなく,Go Mobileのソースコードを見てもイベントを発生させてる箇所は見つけられませんでした。

以下の図のように,ステージは基本的にStageDead->StageAlive->StageVisible->StageFocusedの順(定数の小さい順)に遷移することを前提としています。
lifecycle.Event.Crossesメソッドを使うと,引数で渡したステージを順方向(定数の小さい順)に通り過ぎてるか(CrossOn),逆方向(定数の大きい順)に通り過ぎているか(CrossOff)が取得できます。
たとえば,図の左側のようにイベントがlifecycle.Event{From: StageAlive, To: StageFocused}の場合,Crosses(StageVisible)CrossOnを返します。

ライフサイクル

少し分かりづらいですね。Crossesメソッドの実装を見る方が分かりやすいかもしれません。なお,Stage型は単なるuint32のエイリアス型であるため,大小比較ができることに注意しましょう。

// Crosses returns whether the transition from From to To crosses the stage s:
//  - It returns CrossOn if it does, and the lifecycle change is positive.
//  - It returns CrossOff if it does, and the lifecycle change is negative.
//  - Otherwise, it returns CrossNone.
// See the documentation for Stage for more discussion of positive and negative
// crosses.
func (e Event) Crosses(s Stage) Cross {
    switch {
    case e.From < s && e.To >= s:
        return CrossOn
    case e.From >= s && e.To < s:
        return CrossOff
    }
    return CrossNone
}

Crossesメソッドを使うことで,プラットフォームによって,とあるステージに遷移しない場合でも,そのステージを「通過」したかどうかが分かります。

たとえば,以下のようにCrossesをメソッドを呼び出した場合,たとえStageVisibleに遷移してなくても,あたかも遷移したかどうかを判断しているように処理ができます。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            switch e := app.Filter(e).(type) {
            case lifecycle.Event:
                switch e.Crosses(lifecycle.StageVisible) {
                case lifecycle.CrossOn:
                    // 画面が表示された場合の処理
                case lifecycle.CrossOff:
                    // 画面が表示されなくなった場合の処理
                }
            }
        }
    })
}

Crossesメソッドは一見無駄なように見えますが,Go Mobileでは,複数のプラットフォームの実装を用意する必要があり,Crossesメソッドは,その違いを吸収することができる,よく考えられたメソッドです。

今回は,タッチイベントとライフサイクルについて説明しました。タッチイベントと前回説明したspriteパッケージを使えば簡単なゲームを作ることができるでしょう。また,ライフサイクルイベントを使えば,アプリがサスペンドされたことを検出して,ゲームをポーズさせたりすることができ,より完成度のゲームを作ることができるようになると思います。

次回は,音の再生と各種センサーを扱う方法について説明する予定です。

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

おすすめ

合わせて読みたい

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。