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
参考情報
- Scene2d.ui · libgdx/libgdx Wiki · GitHub
- Scene2d UI 公式文書。
解説
Button
Table クラスのサブクラス。
設定項目 | 型 | 説明 |
---|---|---|
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 クラスのサブクラス。
設定項目 | 型 | 説明 |
---|---|---|
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 クラスのサブクラス。
設定項目 | 型 | 説明 |
---|---|---|
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 クラスのサブクラス。
設定項目 | 型 | 説明 |
---|---|---|
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 · libgdx/libgdx Wiki · GitHub
- Scene2d 公式文書。
- Viewports · libgdx/libgdx Wiki · GitHub
- Viewport 公式文書。
- Scene2d.ui · libgdx/libgdx Wiki · GitHub
- Scene2d UI 公式文書。
- libGDXの基礎5 Scene2Dを使う - Qiita
- 概要。
解説
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
- ScalingViewport
- Stage サイズを指定し、Screen に合わせて Scale (拡大/縮小) して表示する。Stage サイズは指定したサイズ。
- Scale 方法は以下のとおり。
- 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 までの全ての親 Widget で invalidate
メソッドが呼ばれる。
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
参考情報
- The life cycle · libgdx/libgdx Wiki · GitHub
- 公式文書。ライフサイクルの章。
- Continuous & non continuous rendering · libgdx/libgdx Wiki · GitHub
- 公式文書。continuous & not continuous レンダリングの章。
解説
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 Studio で iOS アプリを作成できます。 しかし、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
参考情報
- Intel® Developer Zone
- Intel Multi-OS Engine で libGDX を使う方法
- Android StudioでlibGDX入門 - Qiita
- Android Studio で libGDX を使う方法
解説
手順は下記のとおり。
- libGDX の Setup App (gdx-setup.jar) を libgdx からダウンロード
- Setup App を実行してプロジェクトを作成
- Android Studio にプロジェクトをインポート
- プロジェクトの設定を変更
- Android/iOS アプリ (Java版) を実行
- Kotlin に変換
- Android/iOS アプリ (Kotlin版) を実行
プロジェクトの作成
プロジェクトの作成では、Setup App (gdx-setup.jar) を起動して Destination と Android SDK のパス、Sub projects を指定する。
Destination は何処でも構わない。
今回は Android Studio にインポートするため、AndroidStudioProjects の配下に置くことにした。
Android SDK は適当なプロジェクトを開いた上で File > Project Structure... > SDK Location
と辿ればパスを知ることができる。
Sub projects は Android と Ios-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 クラスでエラーが発生する。
internal
を private 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 でそれぞれ実行する。
Android と iOS で縦向き、横向きが異なる。
AndroidManifest.xml にて screenOrientation="sensor"
とすれば Android も縦向きとなる。
Kotlin で iOS アプリを作る
概要
Kotlin で iOS アプリを作成します。 開発環境には Android Studio を使用し、Intel Multi-OS Engine を使って 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 24.0.1
- compileSdkVersion 24
参考情報
- Intel® Developer Zone
- Intel Multi-OS Engine の公式文書
- GitHub - multi-os-engine/moe-samples-kotlin: Multi-OS Engine: Kotlin Samples
- Kotlin サンプル
解説
前提
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 モジュールから使用できる。
今後の予定
Kotlin の書籍
Android の実行環境
概要
Android のアプリを動かしている仕組みを整理します。 全体像を概観することを目的としています。 細かな部分は推測になっている所があります。 (間違いがあれば教えてください。)
参考情報
- Anatomy & Physiology of an Android
- 2008 Google I/O のスライド
- KMC Staff Blog:AndroidのBinderによるプロセス間のメソッド呼び出し(メモ)
- Binder によるプロセス間通信
- KMC Staff Blog:AndroidでのJavaプログラムの起動やZygoteまわりのメモ
- Zygote
- アプリケーションの基礎 | Android Developers
- アクセス権
- Android Runtime - Wikipedia
- Dalvik と ART のアーキテクチャ比較
- DalvikVMに替わるランタイム ART – Happy My Life
- ART
- Android 4.4に入ったARTのソースを見た感想 - 組み込みの人。
- ART を解析
解説
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 VM、Android 5.0 以降は ART が Android Runtime に該当する。
Android Runtime は Zygote から複製された後、アプリの共有ライブラリをロードする。 ロードされるファイルは Dalvik VM と ART で異なる。
緑四角はネイティブコード、青四角は非ネイティブコード、緑文字は環境依存、青文字は環境非依存を意味する。
odex ファイルは機種に合わせて最適化した dex ファイル。 odex ファイルの内容はネイティブコードではないが、機種に依存した内容となる。 ネイティブコードへは実行時に JIT (Just In Time) コンパイルされる。