libGDX 2D UI Library - Widget (Button)

概要

libGDX の 2D UI Library のうち、Button 系 Widget についてまとめます。

目次

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

Button

Table クラスのサブクラス。

Button (libgdx API)

設定項目 説明
child Actor 子の Actor。
style Button.ButtonStyle 背景の Drawable (up, down, checked, over, checkedOver, disabled)。child のオフセット (unpressedOffset[X|Y], pressedOffset[X|Y], checkedOffset[X|Y])。
Skin

状態遷移(一部)

サイズ

min pref max
width/height prefと同じ child のサイズ 0

実行例

up checked (offsetX = 100, offsetY = 100)

サンプルコード

    private val actor by lazy {
        val imageButton = ImageButton(
                SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                    color = Color.RED
                }),
                SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                    color = Color.BLUE
                }),
                SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                    color = Color.GREEN
                })
        )
        Button(imageButton,
                Button.ButtonStyle().apply {
                    up = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.YELLOW
                    })
                    down = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.MAGENTA
                    })
                    checked = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.CYAN
                    })
                    checkedOffsetX = 100f
                    checkedOffsetY = 100f
                }
        ).apply {
            width = this@MyGdxGame.stage.width
            height = this@MyGdxGame.stage.height
        }
    }

    override fun create() {
        ...
        stage.addActor(actor)
    }

ImageButton

Button クラスのサブクラス。

ImageButton (libgdx API)

設定項目 説明
style ImageButton.ImageButtonStyle imageUp, imageDown, imageChecked, imageOver, imageCheckedOver, imageDisabled。加えて、Button.ButtonStyle の項目。
Skin

サイズ

min pref max
width/height prefと同じ image のサイズ 0

実行例

up checked

サンプルコード

    private val actor by lazy {
        ImageButton(
                ImageButton.ImageButtonStyle().apply {
                    up = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.YELLOW
                    })
                    imageUp = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.RED
                    })
                    down = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.MAGENTA
                    })
                    imageDown = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.BLUE
                    })
                    checked = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.CYAN
                    })
                    imageChecked = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.GREEN
                    })
                    disabled = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.GRAY
                    })
                    imageDisabled = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.WHITE
                    })
                }
        ).apply {
            width = this@MyGdxGame.stage.width
            height = this@MyGdxGame.stage.height
            isDisabled = false
        }
    }

    override fun create() {
        ...
        stage.addActor(actor)
    }

TextButton

Button クラスのサブクラス。

TextButton (libgdx API)

設定項目 説明
text String ボタンテキスト。改行文字 ('\n') で改行される。
style TextButton.TextButtonStyle font, fontColor, downFontColor, checkedFontColor, overFontColor, checkedOverFontColor, disabledFontColor。加えて、Button.ButtonStyle の項目
Skin
label.* Label の設定項目。

サイズ

min pref max
width/height prefと同じ text が収まるサイズを算出 0

実行例

up checked

サンプルコード

    private val actor by lazy {
        TextButton("OK\n(Enter)",
                TextButton.TextButtonStyle().apply {
                    font = BitmapFont()
                    fontColor = Color.RED
                    downFontColor = Color.BLUE
                    checkedFontColor = Color.GREEN
                    disabledFontColor = Color.GRAY
                    up = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.YELLOW
                    })
                    down = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.MAGENTA
                    })
                    checked = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.CYAN
                    })
                    disabled = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.WHITE
                    })
                }
        ).apply {
            width = this@MyGdxGame.stage.width
            height = this@MyGdxGame.stage.height
            label.setFontScale(5f, 10f)
            isDisabled = false
        }
    }

    override fun create() {
        ...
        stage.addActor(actor)
    }

CheckBox

TextButton クラスのサブクラス。

CheckBox (libgdx API)

設定項目 説明
text String ボタンテキスト。改行文字 ('\n') で改行される。
style CheckBox.CheckBoxStyle Drawable (checkboxOff, checkboxOn, checkboxOffDisabled, checkboxOnDisabled, checkboxOver)。 加えて、TextButton.TextButtonStyle の項目。
Skin
label.* Label の設定項目。

サイズ

min pref max
width/height prefと同じ text と checkbox (Drawable) が収まるサイズを算出 0

実行例

up checked

サンプルコード

    private val actor by lazy {
        CheckBox("On",
                CheckBox.CheckBoxStyle().apply {
                    font = BitmapFont()
                    checkboxOff = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.RED
                    })
                    checkboxOn = SpriteDrawable(Sprite(Texture("badlogic.jpg")).apply {
                        color = Color.GREEN
                    })
                }
        ).apply {
            width = this@MyGdxGame.stage.width
            height = this@MyGdxGame.stage.height
            label.setFontScale(5f, 10f)
        }
    }

    override fun create() {
        ...
        stage.addActor(actor)
    }

libGDX Scene2d の基礎

概要

libGDX で 2D UI Widget (Button, Label, CheckBox, etc.) を使用するための基礎となる Stage, Viewport, Layout についてまとめます。 (ゲームライブラリとしてではなく、マルチプラットフォームGUI ライブラリとしての使用を模索中です。)

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

Scene2d は基本的な 2D scene graph の機能を提供する。 Scene2d により以下が提供される。

  • アクター (actor)
    • 2D scene graph のノード。位置 (position)、サイズ (rectangular size)、原点 (origin)、スケール (scale)、回転 (rotation)、色 (color) を持つ。
  • グループ (group)
    • アクターのグループ。
  • 描画 (drawing)
    • 回転 (rotation)、スケール (scale) を考慮した描画を行う。
  • 当たり判定 (hit detection)
    • 回転 (rotation)、スケール (scale) を考慮した当たり判定を行う。
  • イベント (event)
    • 各アクターに入力イベントを分配する。
  • アクション (action)
    • 各アクターを時間に応じて変化させる。

Scene2d を基礎とした UI Widget が用意されている。 UI Widget 関連のクラスを図に示す。

Stage

Stage は 複数の Actor を子に持ち、Actor の root 要素になる。 Stage は InputProcessor であり、入力イベントを各 Actor に分配する。 Viewport により Stage を Screen に投影する方法を指定する。

基本的な使い方は以下のとおり。

class MyGdxGame : ApplicationAdapter() {

    private val stage by lazy {
        Stage()
    }

    override fun create() {
        Gdx.input.inputProcessor = stage
        Gdx.graphics.isContinuousRendering = false
        stage.addActor(Image(Texture("badlogic.jpg")))
    }

    override fun resize(width: Int, height: Int) {
        stage.viewport.update(width, height, true)
    }

    override fun render() {
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
        stage.act(Gdx.graphics.deltaTime)
        stage.draw()
    }

    override fun dispose() {
        stage.dispose()
    }
}

stage プロパティは var lateinit stage でも良いが、stage プロパティを変更不可としたいため val stage by lazy を使用している。 by lazy を使用することにより、最初の get 呼び出しでインスタンスが生成されるようになる。

Viewport

Viewport により Stage を Screen に投影する方法を指定する。 Viewport のクラス階層を以下に示す。

各 Viewport における、Screen サイズ (単位はピクセル) と Stage サイズ (単位はユニット) の関係を以下に示す。 Stage を構成する単位 (Stage の論理的な画素) を 1 ユニットとする。

  • ScreenViewport
    • Stage サイズ = Screen サイズ × 1 ピクセル当たりのユニット (unitsPerPixel)
    • デフォルトでは、1 ユニット = 1 ピクセル (unitsPerPixcel = 1)。
  • ScalingViewport
    • Stage サイズを指定し、Screen に合わせて Scale (拡大/縮小) して表示する。Stage サイズは指定したサイズ。
    • Scale 方法は以下のとおり。
      • none : 1 ユニット = 1 ピクセルとする。
      • fit : 縦横一方を Stage = Screen となるように、一方を Stage < Screen となるように、Stage のアスペクト比を変えずに拡大/縮小する。
      • fill : 縦横一方を Stage = Screen となるように、一方を Stage > Screen となるように、Stage のアスペクト比を変えずに拡大/縮小する。
      • stretch : 縦横共に Stage = Screen となるように、Stage のアスペクト比を変えて拡大/縮小する。
  • FitViewport
    • ScalingViewport の fit と同じ。
  • FillViewport
    • ScalingViewport の fill と同じ。
  • StretchViewport
    • ScalingViewport の stretch と同じ。
  • ExtendViewport
    • Stage サイズを指定し、Screen に合わせて拡大/縮小して表示する。拡大/縮小に合わせて Stage サイズを変更する。
    • FitViewport と同様に拡大/縮小した後に、Stage < Screen となっている辺の Stage サイズを変更する。
      • 最大サイズが指定されていない場合は、Stage = Screen となるように Stage サイズを変更する。
      • 最大サイズが指定されている場合は、指定した最大サイズに Stage サイズを変更する。

言葉では分かりづらいため図示すると以下のようになる。 赤は Screen サイズ (単位はピクセル)。 黒は Stage サイズ (単位はユニット)。

ScreenViewport (unitsPerPixcel = 1) ScreenViewport (unitsPerPixcel = 2)

ScalingViewport (none, 640, 480) ScalingViewport (fit, 640, 480)
ScalingViewport (fill, 640, 480) ScalingViewport (stretch, 640, 480)

ExtendViewport (640, 480) ExtendViewport (640, 480, 800, 480) ExtendViewport (640, 480, 800, 640)

表示例を得るために以下のソースコードを使用した。

class MyGdxGame : ApplicationAdapter() {

    companion object {
        private val TAG = MyGdxGame::class.java.name
    }

    private val stage by lazy {
        Stage(ScreenViewport())
        /*
        Stage(ScreenViewport().apply {
            unitsPerPixel = 2f
        })
        */
        //Stage(ScalingViewport(Scaling.none, 640f, 480f))
        //Stage(ScalingViewport(Scaling.fit, 640f, 480f))
        //Stage(FitViewport(640f, 480f))
        //Stage(ScalingViewport(Scaling.fill, 640f, 480f))
        //Stage(FillViewport(640f, 480f))
        //Stage(ScalingViewport(Scaling.stretch, 640f, 480f))
        //Stage(StretchViewport(640f, 480f))
        //Stage(ExtendViewport(640f, 480f))
        //Stage(ExtendViewport(640f, 480f, 800f, 480f))
        //Stage(ExtendViewport(640f, 480f, 800f, 640f))
    }

    override fun create() {
        Gdx.app.logLevel = Application.LOG_DEBUG
        Gdx.app.debug(TAG, "create")
        Gdx.graphics.isContinuousRendering = false
        stage.addActor(object : Actor() {
            override fun draw(batch: Batch, parentAlpha: Float) {
                batch.end()
                ShapeRenderer().run {
                    projectionMatrix = batch.projectionMatrix
                    begin(ShapeRenderer.ShapeType.Filled)
                    color = Color.RED
                    rect(0f, 0f, stage.width, stage.height)
                    Gdx.app.debug(TAG, "${stage.width} ${stage.height}")
                    end()
                }
                batch.begin()
            }
        })
        stage.addActor(Image(Texture("badlogic.jpg")))
    }

    override fun resize(width: Int, height: Int) {
        Gdx.app.debug(TAG, "resize $width $height")
        stage.viewport.update(width, height, true)
    }

    override fun render() {
        Gdx.app.debug(TAG, "render")
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
        stage.draw()
    }

    override fun dispose() {
        Gdx.app.debug(TAG, "dispose")
        stage.dispose()
    }
}

レイアウト

UI Widget は自身のサイズと位置を決定しない。 親の UI Widget が子の UI Widget のサイズと位置を決定する。 UI Widget は親がサイズと位置を決定するためのヒントとなる最小サイズ、推奨サイズ、最大サイズを提供する。

UI Widget が描画 (draw) されるとき、最初に validate メソッドが呼ばれる。 UI Widget のレイアウトが invalid のとき、描画の前に layout メソッドが呼ばれる。 描画においては、layout によりキャッシュされたレイアウト情報 (サイズと位置) を用いる。 invalidate, invalidateHierarchy メソッドを呼ぶことで、レイアウトを invalid にできる。

UI Widget の状態が変化してキャッシュされたレイアウト情報を再計算する場合には invalidate メソッドを呼ぶ。 但し、最小サイズ、推奨サイズ、最大サイズに影響がない場合に限る。 サイズが変化せず、親 Widget が影響を受けない場合に使用する。

UI Widget の状態が変化して最小サイズ、推奨サイズ、最大サイズに影響が及ぶ場合には invalidateHierarcy メソッドを呼ぶ。 親 Widget のレイアウトに影響が及ぶため、root までの全ての親 Widgetinvalidate メソッドが呼ばれる。

libGDX アプリケーションのライフサイクル

概要

libGDX アプリケーションのライフサイクルの説明です。 言語は Kotlin、Android/iOS アプリで動作確認をしています。

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

libGDX のアプリケーションを作成するには ApplicationListener インターフェイスを実装したクラスを作成する。 ApplicationAdapter クラスは ApplicationListener インターフェイスを実装し、各メソッドのデフォルト実装を提供する。

class MyGdxGame : ApplicationAdapter() {

    companion object {
        private val TAG = MyGdxGame::class.java.name
    }

    override fun create() {
        Gdx.app.logLevel = Application.LOG_DEBUG
        Gdx.app.debug(TAG, "create")
        //Gdx.graphics.isContinuousRendering = false
    }

    override fun pause() {
        Gdx.app.debug(TAG, "pause")
    }

    override fun resume() {
        Gdx.app.debug(TAG, "resume")
    }

    override fun resize(width: Int, height: Int) {
        Gdx.app.debug(TAG, "resize")
    }

    override fun render() {
        Gdx.app.debug(TAG, "render")
    }

    override fun dispose() {
        Gdx.app.debug(TAG, "dispose")
    }
}

ライフサイクルの図は The life cycle · libgdx/libgdx Wiki · GitHub を参照。 各操作に応じて呼ばれるメソッドは以下のとおり。

操作 Android iOS
アプリ起動 create, resize create, resize
ホーム pause pause
タスクリスト pause pause
pause状態でアプリ起動 resume, resize resume
タスクリストでアプリ起動 resume, resize resume
タスクリストでアプリ終了 dispose
戻る(アプリ終了) pause, dispose 操作不可
回転 resize resize, resize

(Android はタスクリストでアプリを終了しても dispose が呼ばれないのは仕様なのか?バグなのか? 呼ばれているがログに出ていないだけなのか?)

render メソッドは連続して呼ばれる。 使用しているハードウェアに応じて 1 秒間に 30-50-80 回の頻度で呼ばれる。 連続して呼び出される状態ではバッテリー消費が増える。 Gdx.graphics.isContinuousRendering = false により連続呼び出しを無効にできる。 無効にした場合は、以下のタイミングでのみ render メソッドが呼ばれる。

  • 入力イベントが発生した
  • Gdx.graphics.requestRendering メソッドが呼ばれた
  • Gdx.app.postRunnable メソッドが呼ばれた

UI Action (フェードイン、フェードアウトなど) では render メソッドが連続して呼ばれる。 デフォルトでは有効になっている。 stage.actionsRequestRendering = false により無効にできる。

(Java の場合、isContinuousRendering, actionsRequestRendering は set メソッド呼び出しの形式で書く。)

変更履歴

2016-11-05

以下のようにしていましたが、kotlin-refrect を追加すると APK サイズが大きくなるとの意見を見かけたので kotlin-reflect を使わないようにしました。 kotlin-reflect を追加すると APK サイズが 0.7MB 増えます。

class MyGdxGame : ApplicationAdapter() {

    companion object {
        private val TAG = MyGdxGame::class.qualifiedName
    }

    ...
}

上記のコードを実行するためには build.gradle に kotlin-reflect への依存を追加する必要がある。 MyGdxGame::class.qualifiedName が kotlin-reflect を必要とする。

dependencies {
    ...
    compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

libGDX ロギング

概要

libGDX のロギング機能の使い方です。Android/iOS アプリで動作確認をしています。

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

libGDX のログレベルは 3 段階。

  • ERROR
  • INFO
  • DEBUG

各ログレベルに対応したログ出力を行うメソッドが存在する。

  • ERROR : Gdx.app.error
  • INFO : Gdx.app.log
  • DEBUG: Gdx.app.debug

logLevel プロパティにより出力するログレベルを変更することができる。 実行環境によってデフォルトのログレベルが異なっている。 (logLevel を設定せずに実行すると Android/iOS で出力結果が異なる。)

使用例。

import com.badlogic.gdx.Application
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx

class MyGdxGame : ApplicationAdapter() {

    override fun create() {
        Gdx.app.logLevel = Application.LOG_DEBUG
        Gdx.app.debug("MyTag", "Debug level message")
        Gdx.app.log("MyTag", "Info level message")
        Gdx.app.error("MyTag", "Error level message")

        Gdx.app.logLevel = Application.LOG_ERROR
        Gdx.app.debug("MyTag", "Debug level message") // 出力されない
        Gdx.app.log("MyTag", "Info level message") // 出力されない
        Gdx.app.error("MyTag", "Error level message")
    }
}

ios-moe の出力結果。

MyTag 3 Debug level message
MyTag 4 Info level message
MyTag 6 Error level message
MyTag 6 Error level message

android の出力結果。logcat に出力される。

D/MyTag: Debug level message
I/MyTag: Info level message
E/MyTag: Error level message
E/MyTag: Error level message

Kotlin で iOS アプリを作る (libGDX 編)

概要

Intel Multi-OS Engine を使うことにより Android StudioiOS アプリを作成できます。 しかし、UI 部分は各 OS によってライブラリが異なるため、共通化できる部分は限られます。 そこで、ゲーム用フレームワーク libGDX を使って描画の共通化を計ります。 本記事では、プロジェクトの作成とサンプルアプリの起動までを行います。

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

手順は下記のとおり。

  1. libGDX の Setup App (gdx-setup.jar) を libgdx からダウンロード
  2. Setup App を実行してプロジェクトを作成
  3. Android Studio にプロジェクトをインポート
  4. プロジェクトの設定を変更
  5. Android/iOS アプリ (Java版) を実行
  6. Kotlin に変換
  7. Android/iOS アプリ (Kotlin版) を実行

プロジェクトの作成

プロジェクトの作成では、Setup App (gdx-setup.jar) を起動して DestinationAndroid SDK のパス、Sub projects を指定する。 Destination は何処でも構わない。 今回は Android Studio にインポートするため、AndroidStudioProjects の配下に置くことにした。 Android SDK は適当なプロジェクトを開いた上で File > Project Structure... > SDK Location と辿ればパスを知ることができる。 Sub projects は AndroidIos-moe を選択する。

Generate を選択するといくつかのダイアログが表示される。

You have a more recent version of android build tools than recomended. Do you want to use your more recent version?

Android Build Tools のバージョンが推奨するバージョンよりも新しいが、新しいバージョンを使用するか? とのことなので「はい」を選択した。

You have a more recent Android API than the recomended. Do you want to use your more recent version?

Android API のバージョンが推奨するバージョンよりも新しいが、新しいバージョンを使用するか? とのことなので「はい」を選択した。

ダウンロードが行われるなど、暫く待つことになる。 遅いのは最初の1回だけ。

プロジェクトのインポート

作成されたプロジェクトの build.gradle を指定してインポート。

プロジェクトの設定変更

本記事執筆時点では、インポートすると ios-moe/java フォルダ配下の IOSMoeLauncher でエラーが発生する。 (libGDX version 1.9.4 で発生するが、1.9.5 になれば修正されると思われる。) エラーの理由は Pointer クラスのパッケージが異なることによる。 libGDX では com.intel.moe.natj.general.Pointer だが、MOE では org.moe.natj.general.Pointer に変更されている。 プロジェクトの build.gradle で gdxVersion を 1.9.4 から 1.9.5-SNAPSHOT に変更すればエラーはなくなる。

アプリの実行

Android Emulator / SimulatorApp でそれぞれ実行する。

Kotlin に変換

build.gradle に下記を追加する。

build.gradle (Project)

buildscript {
    ext.kotlin_version = '1.0.4'
    ... 
    dependencies {
        ...
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'

...

build.gradle (Module: android)

apply plugin: 'kotlin-android'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

...

build.gradle (Module: core)

apply plugin: 'kotlin'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

...

build.gradle (Module: ios-moe)

apply plugin: 'kotlin'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

...

android, core, ios-moe の各モジュールの java ディレクトリを選択して Code > Convert Java File to Kotlin File を実行する。

core モジュールの MyGdxGame クラスでエラーが発生する。 internalprivate lateinit に変更する。 internal lateinit でも構わないがアクセス修飾子はできる限り狭い範囲のみアクセスできるようにする。

class MyGdxGame : ApplicationAdapter() {
    private lateinit var batch: SpriteBatch
    private lateinit var img: Texture

    ...
}

ios-moe モジュールの IOSMoeLauncher クラスでは警告が発生する。 constructor の protected は削除する。 Kotlin では クラスが final 扱いとなるためである。 IOSMoeLauncher::class.java!!.getName()IOSMoeLauncher::class.java.name でよい。 get メソッドは Kotlin ではプロパティを使う。 !! は null があり得る場合に使用するが、null となることはないので不要。

再びアプリの実行

Android Emulator / SimulatorApp でそれぞれ実行する。 AndroidiOS で縦向き、横向きが異なる。 AndroidManifest.xml にて screenOrientation="sensor" とすれば Android も縦向きとなる。

Kotlin で iOS アプリを作る

概要

Kotlin で iOS アプリを作成します。 開発環境には Android Studio を使用し、Intel Multi-OS Engine を使って iOS アプリに変換して実行します。

確認環境

参考情報

解説

前提

Xcode, Android Studio, Kotlin Plugin はインストール済みとする。

Multi-OS Engine のインストール

Android Studio に Multi-OS Engine (MOE) Plugin をインストールする。

プロジェクトの作成

プロジェクトの作成手順は通常の Android プロジェクトと同じ。

Multi-OS Engine モジュールの作成

iOS アプリは Multi-OS Engine モジュールとして作成する。

モジュールを作成したら、実行可能な状態になっているので Run 'ios' を選択して実行する。 実機での実行は Developer Team ID が必須な様子(How to configure the Development Team id with MOE 1.2.0 - Support - Multi-OS Engine)。 Simulator では実行された。

Kotlin に変換

Multi-OS Engine モジュールのソースコードJava で書かれている。 Java を Kotlin に変換する。

まずは、build.gradle に Kotlin の設定を加える。 以下を行うとプロジェクトと app (Android アプリ) モジュールに Kotlin の設定が追加される。

ios (Multi-OS Engine モジュール) の build.gradle にはエディタで以下を追記する。

apply plugin: 'kotlin'

dependencies {
    ...
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

次に、Javaソースコードを Kotlin に変換する。

app (Android アプリ) モジュールは java フォルダを選択し Code > Convert Java File to Kotlin File を選択すれば変換される。

ios (Multi-OS Engine モジュール) でも java フォルダを選択し Code > Convert Java File to Kotlin File を選択すれば変換されるが、一部編集が必要。

変換前の Java ファイルと変換後の Kotlin ファイルを記載する。

Main.java

@RegisterOnStartup
public class Main extends NSObject implements UIApplicationDelegate {

    public static void main(String[] args) {
        UIKit.UIApplicationMain(0, null, null, Main.class.getName());
    }

    @Selector("alloc")
    public static native Main alloc();

    protected Main(Pointer peer) {
        super(peer);
    }

    private UIWindow window;

    @Override
    public boolean applicationDidFinishLaunchingWithOptions(UIApplication application, NSDictionary launchOptions) {
        return true;
    }

    @Override
    public void setWindow(UIWindow value) {
        window = value;
    }

    @Override
    public UIWindow window() {
        return window;
    }
}

Main.kt

@RegisterOnStartup
class Main protected constructor(peer: Pointer) : NSObject(peer), UIApplicationDelegate {

    companion object {

        @JvmStatic fun main(args: Array<String>) {
            UIKit.UIApplicationMain(0, null, null, Main::class.java.name)
        }

        @Selector("alloc")
        @JvmStatic external fun alloc(): Main
    }

    private var window: UIWindow? = null

    override fun applicationDidFinishLaunchingWithOptions(application: UIApplication?, launchOptions: NSDictionary<*, *>?): Boolean {
        return true
    }

    override fun setWindow(value: UIWindow?) {
        window = value
    }

    override fun window(): UIWindow? {
        return window
    }
}

alloc 関数では override を削除し @JvmStatic external を追加する。 window 関数の戻り値の型が UIWindow になっているため UIWindow? に変更する。

AppViewController.java

@org.moe.natj.general.ann.Runtime(ObjCRuntime.class)
@ObjCClassName("AppViewController")
@RegisterOnStartup
public class AppViewController extends UIViewController {

    @Owned
    @Selector("alloc")
    public static native AppViewController alloc();

    @Selector("init")
    public native AppViewController init();

    protected AppViewController(Pointer peer) {
        super(peer);
    }

    public UILabel statusText = null;
    public UIButton helloButton = null;

    @Override
    public void viewDidLoad() {
        statusText = getLabel();
        helloButton = getHelloButton();
    }

    @Selector("statusText")
    @Property
    public native UILabel getLabel();

    @Selector("helloButton")
    @Property
    public native UIButton getHelloButton();

    @Selector("BtnPressedCancel_helloButton:")
    public void BtnPressedCancel_button(NSObject sender){
        statusText.setText("Hello Intel Multi-OS Engine!");
    }
}

AppViewController.kt

@org.moe.natj.general.ann.Runtime(ObjCRuntime::class)
@ObjCClassName("AppViewController")
@RegisterOnStartup
class AppViewController protected constructor(peer: Pointer) : UIViewController(peer) {

    @Selector("init")
    external override fun init(): AppViewController

    val statusText: UILabel
        get() = getStatusTextSel()

    val helloButton: UIButton
        get() = getHelloButtonSel()

    override fun viewDidLoad() {}

    @Selector("statusText")
    @Property
    external fun getStatusTextSel(): UILabel

    @Selector("helloButton")
    @Property
    external fun getHelloButtonSel(): UIButton

    @Selector("BtnPressedCancel_helloButton:")
    fun BtnPressedCancel_button(sender: NSObject) {
        statusText.setText("Hello Intel Multi-OS Engine!")
    }

    companion object {

        @Owned
        @Selector("alloc")
        @JvmStatic external fun alloc(): AppViewController
    }
}

alloc 関数は override を削除して @JvmStatic external とする。 実装の無い Selector は external を付ける。 関数名を getStatusText とするとプロパティの statusText と衝突してしまうため getStatusTextSel としている。 自動変換では getLabel 関数が label プロパティに変換されてしまうので修正する。 statusText, helloButton プロパティは var から val に変更し、get をオーバーライドしている。 Java と同様に viewDidLoad 関数が呼ばれたタイミングでフィールドにオブジェクトを保持したい場合は lateinit var を使えば良い。

以上で変換は完了となるため、再度実行して動作することを確認する。

共通モジュールの作成

app (Android モジュール) と ios (Multi-OS Engine モジュール) で共有するモジュール (Java モジュール) を作成する。 app と同様に Kotlin に変換できる。

app と ios モジュールの依存関係に common モジュールを追加する。

以上により共通モジュールのクラスを app と ios モジュールから使用できる。

今後の予定

  • iOS アプリの UI 編集方法の検証
  • 通化できる範囲の検証

Kotlin の書籍

Android の実行環境

概要

Android のアプリを動かしている仕組みを整理します。 全体像を概観することを目的としています。 細かな部分は推測になっている所があります。 (間違いがあれば教えてください。)

参考情報

解説

Android の全体構成

全ての土台は Android 用に拡張した Linux Kernel。 Linux Kernel が持つメモリ管理、プロセス管理、権限によるセキュリティモデル、ドライバモデル、共有ライブラリの仕組みを使う。 ディスプレイ、カメラ、USB などのデバイスドライバ、共有メモリドライバ(Shared Memory Driver)、電源管理(Power Management)、プロセス間通信(Inter-Process Communication; IPC)のための Binder Driver を含む。 Binder Driver については KMC Staff Blog:AndroidのBinderによるプロセス間のメソッド呼び出し(メモ) が参考になる。 プロセス間通信は、startActivity, sendBroadcast などにより他のアプリのコンポーネントと通信する際に使われる。 Android におけるプロセス構成については後述。

Linux Kernel に Libraries 層が積み重なり、Linux Kernel の機能を使って様々な機能を実現する。 Libc (C 言語用の基本機能)、WebKit (HTML レンダリングエンジン)、SQLite (リレーショナルデータベース)、OpenGL|ES (組み込み向け 3 次元グラフィックス)、Surface Manager (描画)、Audio Manager (音源再生)などの機能を含む。 Libc は GNU Libc ではなく Bionic Libc。 Bionic Libc は組み込み向けの Libc で、GNU Libc とは互換性がない。

Libraries の一部に Android Runtime が含まれる。 以前は Dalvik 仮想マシン (Dalvik VM) を使用していたが、Android 5.0 からは ART (Android RunTime) に変更されている。 本記事中では、Dalvik VM と ART を総称して Android Runtime としている。 Android Runtime については後述。

Libraries 層に Application Framework 層が積み重なり、Java API を提供する。 Libraries はネイティブコードで提供されており、Application Framework 層が Java コードから各機能を使用できるようにする。

これら様々な層(機能)を土台として Applications は動作している。

Android のプロセス構成

プロセスはプログラムの実行単位。 基本的に、メモリ領域はプロセス単位で確保され、プロセスが異なるメモリ領域にはアクセスできない。 Android の主要なプロセスと各プロセスを構成するモジュールは下図のとおり。

Anatomy & Physiology of an Android の内容を基に、推測により一部編集を加えている。 Service Manager は個別のプロセスではなく Runtime Process 内で動作すると推測。 Surface Flinger, Audio Flinger は赤色で示しているページがあり Driver の一種と推測し、Libraries 層としては Surface Manager, Audio Manager とする方が適切と判断。 黄色の Runtime は元資料では Dalvik VM だが ART も同様であると推測し、黄色の Rutime として総称。 Home, Contacts は Application の一例であるため、Application で総称。 外側の角四角はプロセス、内側の丸四角はモジュールと推測。

プロセスの起動順序

ブートローダーは Linux Kernel をロードし、Linux Kernel は Init プロセスを起動する。 Init プロセスはバックグラウンドサービスを提供する様々な Daemon プロセスを起動する。 例えば、USB Daemon, Android Debug Bridge Daemon, Debugger Daemon など。

続いて、Init プロセスは Zygote プロセスを起動する。 Zygote プロセスは起動時にアプリケーション実行に必要なライブラリ一式をロード(メモリ領域に展開)する。 Zygote プロセスは Android Runtime のインスタンスを保持し、アプリの起動要求に応じて自身を複製して新しい Android Runtime インスタンスを生成する。 これにより、Android Runtime インスタンスの生成を高速化する。 また、copy-on-write の仕組みにより、メモリへの書き込みが発生する場合のみメモリ領域の複製を行うため footprint の最小化 (メモリ使用量の削減) につながる。 Zygote の仕組みが無い場合にはアプリの起動要求の度にライブラリ一式がロードされるため、ロード時間が必要となり、ロードされたライブラリ一式がメモリ領域を消費する。Zygote については KMC Staff Blog:AndroidでのJavaプログラムの起動やZygoteまわりのメモ が参考になる。

Zygote プロセスの次は Runtime プロセスを起動する。 Runtime プロセスは Service Manager を初期化する。 Service Manager はサービスの登録と検索を担う。

Runtime プロセスは Zygote プロセスに起動要求を送信し、Android Runtime インスタンス(新規プロセス)を生成する。 生成されたプロセスで各種 System Service を起動する。 Surface Manager, Audio Manager などのネイティブシステムサービス、 Activity Manager, Window Manager, Location Manager などの Android システムサービスが System Service に含まれる。 (各種 System Serivce が同一プロセスで動作するのか、別プロセスで動作するのかは読み取れなかったが、1 つの角四角に含まれるので同一プロセスと推測。)

以上で、システムの起動が完了。

アプリの起動は、Zygote プロセスに起動要求を送信し Android Runtime インスタンスを生成することにより行われる。 つまり、アプリ毎にプロセスが生成される。

アクセス権

Android では Linux のマルチユーザーシステムを用いてアクセス権を管理する。

Android システムはアプリ毎に Linux ユーザー ID を割り振る。 アプリのファイルとプロセスは割り振られたユーザーを所有者とする。 ファイルのアクセス権は Android システムにより、プロセスとファイルの所有者が異なる場合にはアクセスできないように設定される。

プロセスはアプリ毎に生成されており、異なる所有者が設定されている。 これにより、他のアプリのファイルやメモリ領域にはアクセスできないようになり、安全性が高まる。

Android Runtime

Android Runtime は Android アプリの実行環境を意味する。 Android 4.x までは Dalvik VMAndroid 5.0 以降は ART が Android Runtime に該当する。

Android Runtime は Zygote から複製された後、アプリの共有ライブラリをロードする。 ロードされるファイルは Dalvik VM と ART で異なる。

緑四角はネイティブコード、青四角は非ネイティブコード、緑文字は環境依存、青文字は環境非依存を意味する。

odex ファイルは機種に合わせて最適化した dex ファイル。 odex ファイルの内容はネイティブコードではないが、機種に依存した内容となる。 ネイティブコードへは実行時に JIT (Just In Time) コンパイルされる。