20191026のAndroidに関する記事は3件です。

ARCoreアプリ開発環境構築&Pixel4で動作

はじめに

先日待望のPixel4が届きました。
以前使ってた端末はAndroid 8.0なのにARCoreが使えず、AR開発のツールが制限されていました。
これでようやくARCoreを使って色々開発できそうです。
まずは環境構築を一から行ったので、いつも通り備忘録として残しておきます。

使用環境

  • 開発用PC
    • Windows 10 ver.1903
    • Android Studio 3.5.1
    • JRE: 1.8.0_202-release-1483-b03 amd64
    • JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
  • 検証端末

環境構築

  • Android StudioのHPの「DOWNLOAD ANDROID STUDIO」をクリックしてダウンロード
  • 保存したインストーラを起動して、デフォルトの設定でAndroid Studioをインストール
  • GoogleのARCore githubよりARCoreのサンプルプロジェクトをダウンロード。今回はsceneform-android-sdk-1.12.0.zip を利用
  • ダウンロードしたファイルを任意のディレクトリで解凍。今回は「C:\Users\myaccount\AndroidStudioProjects」に解凍。
  • Android Studioを立ち上げ、「Open an Existing Android Studio Project」で任意のプロジェクトを選択。今回は「Argumented Image」を選択
    select-project.jpg

  • GradleのBuildingなど様々な処理が完了した後、メニューの[Tools]→[SDK Manager]を選択

  • 左メニューの[Appearance & Behavior]→[System Settings]→[Android SDK]を選択

  • [SDK Platforms]タブのAndroid 10.0~Android 8.0をすべて選択
    sdk-platforms.jpg

  • [SDK Tools]タブの[Android SDK Tools]と[Android SDK Platform-Tools]がインストールされていなければ選択(おそらくデフォルトでインストール済み)
    sdk-tools.jpg

  • [OK]をクリックした後、[Accept]をクリック、インストールが開始される

  • SDKなどがインストールされた後、メニューの[Tools]→[SDK Manager]を選択

  • 左メニューの[Plugins]を選択

  • [Marketplace]タブを選択し、検索窓で[sceneform]で検索

  • [Google Sceneform Tools (Beta)]が表示されるのでインストール(画像はインストール後のもの)
    コメント 2019-10-26 215249.PNG.jpg

  • Android Studio IDEを再起動

サンプルアプリ起動

先ほど入手したARCoreのサンプルプロジェクトを実機にインストールする。

Pixel4の操作

  • Pixel4にてGoogle Playを起動
  • Google Play 開発者サービス(AR)がインストールされていることを確認
  • 開発者向けオプションを有効化([設定]→[デバイス情報]へ進み、[ビルド番号]を7回タップ)
  • [設定]→[システム]→[開発者向けオプション]に進み、デバッグ項目内の[USBデバッグ]を有効化
  • Pixel4をPCに接続

開発用PCの操作

  • Android Studioの画面を表示
  • apkのインストール先を[Google Pixel4]に設定 menu-device.jpg
  • apkインストール先の選択欄の隣にある緑三角マークをクリックし、apkをインストール

起動確認(Pixel4)

  • Pixel4にて権限の要求が表示されるので許可、アプリが実行される

さいごに

これで環境構築は完了です。
環境構築の途中で導入したGoogle Sceneform Tools (Beta)は、Google Polyなどで入手した3Dモデルを導入する際に利用します(この記事のような事態を避けるため)。
また、Argumented Imageを使ってアプリ開発を進めていくので、開発が進んだら別記事で掲載します。

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jetpack ComposeでHello WorldのLayoutNodeが追加されるまで

モチベーション

Jetpack ComposeのHello Worldは以下のような形になっています。
image.png

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

この@Composeで何が起こるのか。。? 実際には馴染みのAndroidのViewがあるはず。。?GreetingTextはUnitしかこれ返してないけど、どうやって追加してるの。。? :thinking:

0.1.0-dev02で確認していきます

デバッグのミスなどで間違っている可能性などはかなりあるので、何かあればご指摘ください :pray:

コードを見ていく

LayoutNodeとは?

実際には馴染みのAndroidのViewがあるはず。。?

setContentの中は以下のようになっており、ComposeはAndroidComposeViewというViewに描画するようです。

fun Activity.setContent(
    content: @Composable() () -> Unit
): CompositionContext? {
    val composeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? AndroidComposeView
        ?: AndroidComposeView(this).also { setContentView(it) }  // **← ここでnewする!!**

AndroidComposeViewは以下のようにrootのレイアウトを持っています。

class AndroidComposeView constructor(context: Context) :
    ViewGroup(context), Owner, SemanticsTreeProvider, DensityScope {
    override var density: Density = Density(context)
        private set

    val root = LayoutNode().also { // **← rootのLayoutNode!!**
        it.measureBlocks = RootMeasureBlocks
    }

このrootのNodeにTextが追加されるようです。
ちなみにこのLayoutNodeクラスはComponentNodeのサブクラスになっており、ComponentNodeは以下のように子codeを持ちます。

sealed class ComponentNode : Emittable {
    internal val children = mutableListOf<ComponentNode>()  // **← childrenを保持する!!**

TextのLayoutNodeを追加しているのは誰か?

このchildrenの追加のタイミングにデバッガーを仕掛けてみます。
image.png
このようにTextが追加されているのがわかります。このTextKt$Text$3$2$1$clidren...、生成されている感があるので、気になります
このinstanceの値の出どころを探りましょう!

どうやらこのup()メソッドから渡されてくるようです。そして、_currentの値が渡されてくるようなので、down()の呼び元を調べます。
image.png

image.png

down()の呼び元としてemitNode()があるのですが、、emitNode()を呼び出しているところがスタックトレースでは追えない部分が出てきます。

コンパイルされたコードの中から来るLayoutNode

コードを見ていくと実際にはViewComposer内の以下のような実装が適応されているようです。

    inline fun <T : View> emit(
        key: Any,
        /*crossinline*/
        ctor: (context: Context) -> T,
        update: ViewUpdater<T>.() -> Unit
    ) = with(composer) {
        startNode(key)
        // **ここにemitNodeがある**
        val node = if (inserting) ctor(context).also { emitNode(it) } 
        else useNode() as T
        ViewUpdater<T>(this, node).update()
        endNode()
    }
...
    @Suppress("PLUGIN_WARNING")
    inline fun call(
        key: Any,
        /*crossinline*/
        invalid: ViewValidator.() -> Boolean,
        block: @Composable() () -> Unit
    ) = with(composer) {
        startGroup(key)
        if (ViewValidator(composer).invalid() || inserting) {
            startGroup(invocation)
            block()
            endGroup()
        } else {
            skipCurrentGroup()
        }
        endGroup()
    }

そこでコンパイラのコードをちょっと見ると以下のようなコメントを発見できます。
https://android.googlesource.com/platform/frameworks/support/+/a5396b43ef905756b18805fe0ae31a99e96e6df6/compose/compose-compiler-hosted/src/main/java/androidx/compose/plugins/kotlin/compiler/lower/ComposableCallTransformer.kt#239

つまりFoo(text="foo")composer.call()に変換しています

        /*
        Foo(text="foo")
        // transforms into
        val attr_text = "foo"
        composer.call(
            key = 123,
            invalid = { changed(attr_text) },
            block = { Foo(attr_text) }
        )
         */

つまり最初のMainActivityのコードは @Composeをなくして以下のように書くことができます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

//@Composable
//fun Greeting(name: String) {
//    Text(text = "Hello $name!")
//}

fun Greeting(name: String) {
    composer.call(
        key = 0x9a8fdf7d,
        invalid = { changed(name) },
        block = { Text(text = "Hello $name!") }
    )
}

GreetingTextはUnitしかこれ返してないけど、どうやって追加してるの。。?

これと同じことがText()の中でも行われ、そこでcomposable.emit()に変換され、emitNode()を呼んでいるようです。

デバッグ方法のメモ

公開されているコードなのですが、かなりinline化されていてデバッガーで追いにくいです。またAndroid StudioについているKotlinのJavaへのデコンパイラではラムダを埋め込んでくれないので実質追えません。
brew cask install jadなどでjadをインストールして
./gradlew compileDebugKotlin
などをした後に

cd app/build/tmp/kotlin-classes/debug/[パッケージ]
jad *.class

することでラムダを埋め込んだコードを出してくれました。

まとめ

多分破壊的変更が何度も入っていくだろうとは思うので一瞬でこの知識は使えなくなるとは思いますが、ソースコードを追えるようになってきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jetpack ComposeでHello WorldのLayoutNodeが追加されるまでを読んでみるメモ

普通に以下のセッションがめちゃくちゃわかりやすいのでそれを見ることをおすすめします
Understanding Compose (Android Dev Summit '19)
https://www.youtube.com/watch?v=Q9MtlmmN4Q0

Understanding composeメモ
https://qiita.com/takahirom/items/e8fb7933fa44a546915f

モチベーション

Jetpack ComposeのHello Worldは以下のような形になっています。
image.png

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

この@Composeで何が起こるのか。。? 実際には馴染みのAndroidのViewがあるはず。。?GreetingTextはUnitしかこれ返してないけど、どうやって追加してるの。。? :thinking:

0.1.0-dev02で確認していきます

デバッグのミスなどで間違っている可能性などはかなりあるので、何かあればご指摘ください :pray:

コードを見ていく

LayoutNodeとは?

実際には馴染みのAndroidのViewがあるはず。。?

setContentの中は以下のようになっており、ComposeはAndroidComposeViewというViewに描画するようです。

fun Activity.setContent(
    content: @Composable() () -> Unit
): CompositionContext? {
    val composeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? AndroidComposeView
        ?: AndroidComposeView(this).also { setContentView(it) }  // **← ここでnewする!!**

AndroidComposeViewは以下のようにrootのレイアウトを持っています。

class AndroidComposeView constructor(context: Context) :
    ViewGroup(context), Owner, SemanticsTreeProvider, DensityScope {
    override var density: Density = Density(context)
        private set

    val root = LayoutNode().also { // **← rootのLayoutNode!!**
        it.measureBlocks = RootMeasureBlocks
    }

このrootのNodeにTextが追加されるようです。
ちなみにこのLayoutNodeクラスはComponentNodeのサブクラスになっており、ComponentNodeは以下のように子codeを持ちます。

sealed class ComponentNode : Emittable {
    internal val children = mutableListOf<ComponentNode>()  // **← childrenを保持する!!**

TextのLayoutNodeを追加しているのは誰か?

このchildrenの追加のタイミングにデバッガーを仕掛けてみます。
image.png
このようにTextが追加されているのがわかります。このTextKt$Text$3$2$1$clidren...、生成されている感があるので、気になります
このinstanceの値の出どころを探りましょう!

どうやらこのup()メソッドから渡されてくるようです。そして、_currentの値が渡されてくるようなので、down()の呼び元を調べます。
image.png

image.png

down()の呼び元としてemitNode()があるのですが、、emitNode()を呼び出しているところがスタックトレースでは追えない部分が出てきます。

コンパイルされたコードの中から来るLayoutNode

コードを見ていくと実際にはViewComposer内の以下のような実装が適応されているようです。

    inline fun <T : View> emit(
        key: Any,
        /*crossinline*/
        ctor: (context: Context) -> T,
        update: ViewUpdater<T>.() -> Unit
    ) = with(composer) {
        startNode(key)
        // **ここにemitNodeがある**
        val node = if (inserting) ctor(context).also { emitNode(it) } 
        else useNode() as T
        ViewUpdater<T>(this, node).update()
        endNode()
    }
...
    @Suppress("PLUGIN_WARNING")
    inline fun call(
        key: Any,
        /*crossinline*/
        invalid: ViewValidator.() -> Boolean,
        block: @Composable() () -> Unit
    ) = with(composer) {
        startGroup(key)
        if (ViewValidator(composer).invalid() || inserting) {
            startGroup(invocation)
            block()
            endGroup()
        } else {
            skipCurrentGroup()
        }
        endGroup()
    }

そこでコンパイラのコードをちょっと見ると以下のようなコメントを発見できます。
https://android.googlesource.com/platform/frameworks/support/+/a5396b43ef905756b18805fe0ae31a99e96e6df6/compose/compose-compiler-hosted/src/main/java/androidx/compose/plugins/kotlin/compiler/lower/ComposableCallTransformer.kt#239

つまりFoo(text="foo")composer.call()に変換しています

        /*
        Foo(text="foo")
        // transforms into
        val attr_text = "foo"
        composer.call(
            key = 123,
            invalid = { changed(attr_text) },
            block = { Foo(attr_text) }
        )
         */

つまり最初のMainActivityのコードは @Composeをなくして以下のように書くことができます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

//@Composable
//fun Greeting(name: String) {
//    Text(text = "Hello $name!")
//}

fun Greeting(name: String) {
    composer.call(
        key = 0x9a8fdf7d,
        invalid = { changed(name) },
        block = { Text(text = "Hello $name!") }
    )
}

GreetingTextはUnitしかこれ返してないけど、どうやって追加してるの。。?

これと同じことがText()の中でも行われ、そこでcomposable.emit()に変換され、emitNode()を呼んでいるようです。

デバッグ方法のメモ

公開されているコードなのですが、かなりinline化されていてデバッガーで追いにくいです。またAndroid StudioについているKotlinのJavaへのデコンパイラではラムダを埋め込んでくれないので実質追えません。
brew cask install jadなどでjadをインストールして
./gradlew compileDebugKotlin
などをした後に

cd app/build/tmp/kotlin-classes/debug/[パッケージ]
jad *.class

することでラムダを埋め込んだコードを出してくれました。

まとめ

多分変更が何度も入っていくだろうとは思うので一瞬でこの知識は使えなくなるとは思いますが、ソースコードを追えるようになってきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む