AR触ってみたい。

widgets(UIの部品 例:TextView、ImageView)がARで表示できるらしい

https://developers.google.com/ar/develop/java/sceneform/create-renderables#create_from_android_widgets

Screenshot_1577032540.png
Screenshot_1577032540.png

ここ。sceneformってなんぞやって話だけどこれできたらくっそ面白そうだと思ったので、
今回はTextViewをAR上に表示させるところまでやろうと思います。

エミュレーターでARCoreアプリを動かすために

いやPixelとかGalaxyとか使いますからって方は飛ばしていいぞ。
てかARCore対応端末無いのにARアプリ作ろうとしてる人、どこからやる気が出てるんだ。

Google Play 開発者サービス(AR)を入れます

https://github.com/google-ar/arcore-android-sdk/releases
ここからAPKをDLして、エミュレーターにドラッグアンドドロップしてインストールしてください。

今回は記事作成時最新Ver(Google_Play_Services_for_AR_1.14.1_x86_for_emulator.apk)を入れます。

成功するとアプリ一覧画面に表示されると思います。

image.png
image.png

環境

なまえ あたい
端末 エミュレーター / Google Pixel 3 XL
Android 10
minSdkVersion 24(ARCoreのせい)

実装

MainActivityができてる段階まで来てください。

build.gradle(appフォルダの中)書き足す

参考:https://developers.google.com/ar/develop/java/sceneform#configure-project

こうなっていると思いますが、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "io.github.takusan23.aredittext"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

ここから書き足していきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "io.github.takusan23.aredittext"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
//Java 8が必要みたい。
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {

//ArFragmentなど
implementation "com.google.ar.sceneform.ux:sceneform-ux:1.14.0"

implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

AndroidManifest書き足す

参考:https://developers.google.com/ar/develop/java/sceneform#manifest
まず上の方にこんなかんじに、

1
2
3
4
5
6
7
8
9
10
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="なんとか~">

  <!-- Both "AR Optional" and "AR Required" apps require CAMERA permission. -->
  <uses-permission android:name="android.permission.CAMERA" />
<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is
visible only in the Google Play Store on devices that support ARCore.
For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />
の中にも書き足します
1
2
3
4
5
6
7
8

```xml
<!-- Indicates that app requires ARCore ("AR Required"). Causes the Google
Play Store to download and install Google Play Services for AR along
with the app. For an "AR Optional" app, specify "optional" instead of
"required".
-->
<meta-data android:name="com.google.ar.core" android:value="required" />

全部つけるとこうなります

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.github.takusan23.aredittext">

<!-- Both "AR Optional" and "AR Required" apps require CAMERA permission. -->
<uses-permission android:name="android.permission.CAMERA" />

<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is
visible only in the Google Play Store on devices that support ARCore.
For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<!-- Indicates that app requires ARCore ("AR Required"). Causes the Google
Play Store to download and install Google Play Services for AR along
with the app. For an "AR Optional" app, specify "optional" instead of
"required".
-->
<meta-data android:name="com.google.ar.core" android:value="required" />


<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

MainActivityにFragmentを置く。

参考:https://developers.google.com/ar/develop/java/sceneform#scene-view
ConstraintLayoutは使い方がよくわからないのでLinearLayoutに置き換えて、Fragmentをドラッグします。

image.png
image.png

できたらArFragmentを選択してOKです!

layout_heightの値をmatch_parentにして最大まで広げるようにしましょう。
それとidもfragmentだとわかりにくくなるので、ar_fragmentとかにしときましょう。

xmlだとこうなっていると思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >

<fragment
android:id="@+id/ar_fragment"
android:name="com.google.ar.sceneform.ux.ArFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

Sceneformが利用可能かチェックする

https://developers.google.com/ar/develop/java/sceneform
には書いてありませんが、ARCoreのサンプルには書いてあったので、Kotlinに変換して使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun checkIsSupportedDeviceOrFinish(activity: Activity): Boolean {
val MIN_OPENGL_VERSION = 3.0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Toast.makeText(activity, "SceneformにはAndroid N以降が必要です。", Toast.LENGTH_LONG).show()
activity.finish()
return false
}
val openGlVersionString = (activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).deviceConfigurationInfo.glEsVersion
if (java.lang.Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) {
Toast.makeText(activity, "SceneformにはOpen GL 3.0以降が必要です。", Toast.LENGTH_LONG).show()
activity.finish()
return false
}
return true
}

これをsetContentView()の前で使います。

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//条件満たしてなければActivity終了させる
if(!checkIsSupportedDeviceOrFinish(this)){
return
}
setContentView(R.layout.activity_main)
}

ArFragment取得

MainActivityで置いたArFragmentを取得します。

1
2
3
4
5
6
7
8
9
10
11
lateinit var arFragment: ArFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//条件満たしてなければActivity終了させる
if(!checkIsSupportedDeviceOrFinish(this)){
return
}
setContentView(R.layout.activity_main)
//ArFragment取得
arFragment = supportFragmentManager.findFragmentById(R.id.ar_fragment) as ArFragment
}

起動してみる

ここまでで特に問題がなければ、
カメラの権限許可がきて、許可すると映ると思います。

image.png
image.png
image.png
image.png

映れば成功です。ここまで間違えずついてこれてます。

いよいよViewを現実に表示させる・・・!

参考:https://developers.google.com/ar/develop/java/sceneform/create-renderables#create_from_android_widgets

まずlayoutフォルダにar_layout.xmlという名前で作成します。

image.png
image.png

次に現実で表示させたいUI部品を並べます。
今回はTextViewを置きます。(タイトル詐欺回避)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffffff"
android:orientation="vertical"
tools:context=".MainActivity">


<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10sp"
android:gravity="center"
android:text="てきすとだよー" />
</LinearLayout>

ARで扱えるように

参考:https://developers.google.com/ar/develop/java/sceneform/create-renderables#create_from_android_widgets

1
2
//レイアウトをARに・・・
lateinit var viewRenderable: ViewRenderable

読み込みます。

1
2
3
4
5
6
7
8
9
10
11
//レイアウトを読み込む
ViewRenderable.builder()
.setView(this, R.layout.ar_layout)
.build()
.thenAccept { renderable -> viewRenderable = renderable } //読み込み成功
.exceptionally {
//読み込み失敗
it.printStackTrace()
Toast.makeText(this, "読み込みに失敗しました。", Toast.LENGTH_LONG).show()
null
}

最後に押したらレイアウトをARで表示させるところを

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arFragment.setOnTapArPlaneListener { hitResult, plane, motionEvent ->
if (::viewRenderable.isInitialized) {
//初期化済みのとき、利用可能
// Create the Anchor.
val anchor = hitResult.createAnchor()
val anchorNode = AnchorNode(anchor)
anchorNode.setParent(arFragment.arSceneView.scene)
// Create the transformable andy and add it to the anchor.
val node = TransformableNode(arFragment.transformationSystem)
node.setParent(anchorNode)
node.renderable = viewRenderable
node.select()
}
}

これで動くはずです!!!実行してみましょう!!!

Screenshot_1577028389.png
Screenshot_1577028389.png

てきすとだよー

押したら消す

1
2
3
node.setOnTapListener { hitTestResult, motionEvent -> 
node.isEnabled = false
}

isEnabledにtrueで消えますがこれでいいのかは不明。誰か頼んだ

ARで表示したTextViewのテキストを変更したい

MainActivityにEditTextとボタンを置きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >

<fragment
android:id="@+id/ar_fragment"
android:name="com.google.ar.sceneform.ux.ArFragment"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<EditText
android:id="@+id/ar_change_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:inputType="textPersonName"
android:text="てきすとだよー" />

<Button
android:id="@+id/ar_change_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="変更" />
</LinearLayout>

</LinearLayout>

そしたらボタンを押したらテキストを変更する処理を書きます。

1
2
3
4
5
6
7
8
9
10
11
//テキスト変更
ar_change_button.setOnClickListener {
//テキスト取得
val text = ar_change_textview.text.toString()
//ARで表示するレイアウト取得
val linearLayout = viewRenderable.view as LinearLayout
//TextView取得
val textView = linearLayout.findViewById<TextView>(R.id.textView)
//変更
textView.text = text
}

動くはずです。

Screenshot_1577029034.png
Screenshot_1577029034.png

なんかEditTextの部分が黒くなるけどなんで?

写真にして保存する

おまけです。
疲れたのでコードだけ。レイアウトにボタンを追加してidを「ar_take_a_picture」にしてください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//撮影ボタン押したとき
ar_take_a_picture.setOnClickListener {
//PixelCopy APIを利用する。のでOreo以降じゃないと利用できません。
val bitmap = Bitmap.createBitmap(
arFragment.view?.width ?: 100,
arFragment.view?.height ?: 100,
Bitmap.Config.ARGB_8888
)
val intArray = IntArray(2)
arFragment.view?.getLocationInWindow(intArray)
try {
PixelCopy.request(
arFragment.arSceneView as SurfaceView, //SurfaceViewを継承してるらしい。windowだと真っ暗なので注意!
Rect(
intArray[0],
intArray[1],
intArray[0] + (arFragment.view?.width ?: 0),
intArray[1] + (arFragment.view?.height ?: 0)
),
bitmap,
{ copyResult: Int ->
if (copyResult == PixelCopy.SUCCESS) {
//成功時
//ここのフォルダは自由に使っていい場所(サンドボックス)
val mediaFolder = externalMediaDirs.first()
//写真ファイル作成
val file = File("${mediaFolder.path}/${System.currentTimeMillis()}.jpg")
//Bitmap保存
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show()
}
},
Handler()
)
} catch (e: IllegalArgumentException) {
e.printStackTrace()
Toast.makeText(this@MainActivity, "失敗しました。", Toast.LENGTH_LONG).show()
}
}

?:の値がくっそ雑だけどゆるして
Android 10で動作確認済です。対象範囲別ストレージ対策。
写真データは
/sdcard/Android/media/パッケージID
に入っています。

おわりに

GitHubに公開しておきます。
https://github.com/takusan23/AREditText

ついでに参考にしたプロジェクトも。
https://github.com/google-ar/sceneform-android-sdk/tree/master/samples/hellosceneform

PixelCopy参考にしました。
https://friegen.xyz/getdrawingcache-deprecated/

おつです。888888