Skip to content

Commit dd8bc8f

Browse files
Merge pull request #33 from bmwcarit/add-android-tutorial
Add tutorial how to program android apps
2 parents e7993fd + 17bb2fd commit dd8bc8f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1235
-1
lines changed

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*.ramres filter=lfs diff=lfs merge=lfs -text
1+
*.rlogic filter=lfs diff=lfs merge=lfs -text
22
*.ramses filter=lfs diff=lfs merge=lfs -text
33
*.png filter=lfs diff=lfs merge=lfs -text
44
* text=auto

basics/android_app/README.md

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
<!--
2+
SPDX-License-Identifier: MPL-2.0
3+
4+
This file is part of Ramses Composer
5+
(see https://github.com/bmwcarit/ramses-composer-docs).
6+
7+
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
8+
If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
-->
10+
# Writing an Android app
11+
12+
![](./doc/monkey_app.png)
13+
14+
This tutorial explains how to integrate a Ramses Composer asset into your Android application. We will use the asset from the
15+
[Monkey tutorial](../monkey/README.md) and integrate it into an android application.
16+
17+
We don't want to repeat the Android documentation here. If you are new to Android, please
18+
have a look at the [official docs]() which explain the various aspects of Android. In this
19+
tutorial, we assume you have basic understanding of Android and will focus only on Ramses
20+
specifics.
21+
22+
If you don't like reading, you can find the final source code of the app in the [app_src](./app_src)
23+
folder.
24+
25+
## Creating an empty Android app
26+
27+
Create a new project in Android Studio. Select "empty activity". In this tutorial, we
28+
use MinSDK=26, use Kotlin as project language.
29+
30+
## Adding Ramses as dependency
31+
32+
Next, we can add the Ramses AAR as a dependency in the app-level `build.gradle` file, like this:
33+
34+
```groovy
35+
// <root>/app/build.gradle
36+
dependencies
37+
{
38+
implementation "io.github.bmwcarit:ramses-aar:1.0.3"
39+
}
40+
```
41+
42+
We also have to disable compression for ramses assets in the app:
43+
44+
```groovy
45+
android {
46+
aaptOptions {
47+
noCompress "ramses", "rlogic"
48+
}
49+
}
50+
```
51+
52+
This is needed because Ramses already compresses assets which collides with
53+
the Android mechanism for asset compression.
54+
55+
## Creating a scene thread
56+
57+
Next, we create a thread class which loads and visualizes the Ramses assets. To do so, we can extend
58+
the RamsesThread with our custom logic, we will
59+
call it MonkeyThread in this example:
60+
61+
```kotlin
62+
63+
import com.bmwgroup.ramses.RamsesThread
64+
65+
class MonkeyThread(threadName: String, context: Context) :
66+
RamsesThread(threadName, context) {
67+
68+
// We will be adding the implementation of these methods a bit later!
69+
override fun onSceneLoaded() = Unit
70+
override fun onUpdate() = Unit
71+
override fun onSceneLoadFailed() = Unit
72+
override fun onLogicUpdated() = Unit
73+
override fun onDisplayResize(width: Int, height: Int) = Unit
74+
}
75+
```
76+
77+
Override all required methods and leave them empty for now, we will be returning back to this
78+
code shortly.
79+
80+
## Replace the TextView with a SurfaceView in the Activity layout
81+
82+
Replace the default TextView created by Android in our activity layout XML file with a
83+
SurfaceView which we will render our monkey into, like this:
84+
85+
```xml
86+
<SurfaceView
87+
android:id="@+id/surfaceView"
88+
android:layout_width="0dp"
89+
android:layout_height="0dp"
90+
app:layout_constraintBottom_toBottomOf="parent"
91+
app:layout_constraintEnd_toEndOf="parent"
92+
app:layout_constraintStart_toStartOf="parent"
93+
app:layout_constraintTop_toTopOf="parent" />
94+
```
95+
96+
Ramses supports rendering into a SurfaceView and a TextureView. We are going to be using the
97+
former in this example.
98+
99+
Next, we make our activity listen to surface events, so that we
100+
can load our asset files after a surface is created, and adapt its size when the
101+
surface size is changed. First, implement the SurfaceHolder.Callback interface:
102+
103+
```kotlin
104+
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
105+
// We will implement these later!
106+
override fun surfaceCreated(holder: SurfaceHolder) = Unit
107+
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) = Unit
108+
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
109+
}
110+
```
111+
112+
...and register the activity as a listener to surface events in the activity constructor:
113+
114+
```kotlin
115+
val surfaceView = findViewById<SurfaceView>(R.id.surfaceView)
116+
surfaceView.holder.addCallback(this)
117+
```
118+
119+
If we start the app now, we will only see a black surface - we are still not doing anything
120+
meaningful with the surface.
121+
122+
## Exporting a binary version of the asset
123+
124+
In order to render something into the SurfaceView, we need an asset exported in binary form from
125+
the RamsesComposer.
126+
There are multiple ways how to do this, the simplest one is to use the export menu (File -> Export).
127+
Open the project we created in the [Monkey tutorial](../monkey/README.md) and export the files
128+
into the assets/ folder of the app. If you used the standard paths, this is \<root\>/app/src/main/assets/.
129+
130+
## Loading the assets
131+
132+
Let's got back to the activity. We can now instantiate the MonkeyThread we created earlier and
133+
load the assets into it.
134+
135+
One way to do this is to use the 'lazy' initialization mechanism of Kotlin to instantiate the
136+
thread, and to
137+
initialize it in the onCreate() callback of our activity, like this:
138+
139+
140+
```kotlin
141+
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
142+
private val monkeyThread: MonkeyThread by lazy {
143+
MonkeyThread("MonkeyThread", applicationContext)
144+
}
145+
146+
override fun onCreate(savedInstanceState: Bundle?) {
147+
super.onCreate(savedInstanceState)
148+
setContentView(R.layout.activity_main)
149+
150+
// Loads the binary asset files we exported before using the asset manager
151+
monkeyThread.initRamsesThreadAndLoadScene(assets, "monkey.ramses", "monkey.rlogic")
152+
153+
val surfaceView = findViewById<SurfaceView>(R.id.surfaceView)
154+
surfaceView.holder.addCallback(this)
155+
}
156+
```
157+
158+
The app will still build now, but the screen will show black. Why is that so? Because we have loaded the
159+
scene in memory, but we are not showing it yet. To show it, we have to tell the RamsesThread that
160+
it should create a display and show the scene there. We have to do this once the SurfaceView's surface
161+
has been created. You guessed it! That belongs in the surfaceCreated() callback:
162+
163+
```kotlin
164+
override fun surfaceCreated(holder: SurfaceHolder) {
165+
// We are working with a thread - we have to catch interrupt exceptions
166+
try {
167+
// Creates a display and shows the scene we loaded in onCreate()
168+
monkeyThread.createDisplayAndShowScene(holder.surface, ClearColor(0F, 0F, 0F, 1F))
169+
170+
// Start rendering
171+
monkeyThread.addRunnableToThreadQueue {
172+
if (monkeyThread.isDisplayCreated && !monkeyThread.isRendering) {
173+
monkeyThread.startRendering()
174+
}
175+
}
176+
} catch (e: InterruptedException) {
177+
Log.e("MainActivity", "surfaceCreated failed: ", e)
178+
}
179+
}
180+
```
181+
182+
Multiple things going on here. Let's have a look one by one.
183+
184+
First, we need a try/catch block. Most of the calls to RamsesThread are performed
185+
asynchronously and therefore can be interrupted. In a real-world application, our business
186+
logic should handle such interruptions gracefully, e.g. by showing something else or
187+
displaying a 'loading screen' while reloading the content. In this example, we keep
188+
things simple and just print the exception stack trace.
189+
190+
Inside the try block, we first create a Ramses display and immediately show the scene we loaded
191+
earlier in the thread. Then, we tell the thread to start rendering.
192+
193+
Finally, in the catch block we do a trivial error handling - we log the error and proceed.
194+
Needless to say,
195+
this is not a true error handling, but we want to keep this example simple. A better way
196+
would be to tell the
197+
user of the app that there was a problem with the asset.
198+
199+
Notice how we check if a display is created and if the thread is not already rendering, before we
200+
ask it to start rendering. The lifecycle-related RamsesThread calls (start/stop rendering, creating displays etc.) are
201+
not graceful - i.e. they will throw exceptions if you are doing something which is not well defined,
202+
for example starting to render when the thread is already rendering.
203+
204+
## Improve the lifecycle
205+
206+
If you are an experienced Android developer, you noticed that we didn't implement the other
207+
two surface callbacks:
208+
209+
* surfaceChanged
210+
* surfaceDestroyed
211+
212+
The surfaceChanged overload should pass the size information down to your scene thread so
213+
that it can adjust itself
214+
to the new size (as we do in the [next section](#fix-the-viewport)):
215+
216+
```kotlin
217+
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
218+
monkeyThread.resizeDisplay(width, height)
219+
}
220+
```
221+
222+
The surfaceDestroyed should destroy the display we created:
223+
224+
```kotlin
225+
override fun surfaceDestroyed(holder: SurfaceHolder) {
226+
try {
227+
monkeyThread.destroyDisplay()
228+
} catch (e: InterruptedException) {
229+
Log.e("MainActivity", "surfaceDestroyed failed: ", e)
230+
}
231+
}
232+
```
233+
234+
If you start the app now, you should see at least one green monkey! But it doesn't look quite the same
235+
as the Ramses Composer preview... Let's fix this.
236+
237+
## Fix the viewport
238+
239+
Depending on your device/emulator, the monkeys are not centered on the screen, and probably
240+
look strange in portrait mode.
241+
This is because the monkey asset doesn't know the size of the display you are showing it on. It uses
242+
the camera/viewport settings which were statically set in the Ramses Composer! So you have to
243+
configure these properties in the app. It's time we implement some of the overloaded methods we
244+
left empty in our MonkeyThread!
245+
246+
First, we need to obtain and set the properties of the camera used to render the monkeys.
247+
You can find the camera object in the
248+
node tree of the Composer:
249+
250+
![](./doc/camera.png)
251+
252+
As you can see, the viewport is fixed at 1440x720 pixels. That's probably not the same as your surface
253+
size. We have to find these properties in the asset during runtime and set the values to be the same as
254+
the real pixel dimensions of the surface.
255+
256+
One way to find the camera object is by looking it up by name:
257+
258+
```kotlin
259+
val cameraRoot = getLogicNodeRootInput("PerspectiveCamera_CameraBinding")
260+
```
261+
262+
The name is a combination of the name in the RamsesComposer project ('PerspectiveCamera' in
263+
this case) and the binding
264+
used to control its properties (a 'CameraBinding' in this case). For more information on the
265+
available objects exported by
266+
the Ramses Composer, see [this page](../data_and_scopes/README.md).
267+
268+
You can also find a list of all exported objects and their names in the export menu of the
269+
Ramses Composer (File -> Export).
270+
271+
Next, we want to store the properties of the camera viewport in our MonkeyThread so that we
272+
can set them to their correct value later:
273+
274+
```kotlin
275+
private var viewportWidth: Property? = null
276+
private var viewportHeight: Property? = null
277+
private var aspectRatio: Property? = null
278+
279+
override fun onSceneLoaded() {
280+
val cameraRoot = getLogicNodeRootInput("PerspectiveCamera_CameraBinding")
281+
282+
viewportWidth = cameraRoot?.getChild("viewport")?.getChild("width")
283+
viewportHeight = cameraRoot?.getChild("viewport")?.getChild("height")
284+
aspectRatio = cameraRoot?.getChild("frustum")?.getChild("aspectRatio")
285+
}
286+
```
287+
288+
Each logic object in the scene has nested list of properties, which you can obtain either by
289+
iterating with getChildCount()/getChild(int) or by querying by their name, as we do in the snippet above.
290+
291+
But how do we know the names of these properties? In this example, the object we query is a
292+
CameraBinding - one of
293+
the standard objects provided by `Ramses Logic` to interact with a Ramses scene. As such, the object has
294+
a fixed list of properties, which are documented in the
295+
[CameraBinding documentation](https://ramses-logic.readthedocs.io/en/latest/classes/RamsesCameraBinding.html#classrlogic_1_1RamsesCameraBinding).
296+
You can find a list of all such binding objects in the
297+
[binding documentation](https://ramses-logic.readthedocs.io/en/latest/classes/RamsesBinding.html) -
298+
it lists all the subclass which bind Ramses objects.
299+
300+
Now, we have a handle of the camera properties. All that remains to be done is to set their
301+
values based on the
302+
surface size and orientation. We do this in the onDisplayResize() callback of the MonkeyThread:
303+
304+
```kotlin
305+
override fun onDisplayResize(width: Int, height: Int) {
306+
viewportWidth?.set(width)
307+
viewportHeight?.set(height)
308+
aspectRatio?.set(width.toFloat() / height)
309+
}
310+
```
311+
312+
Voila! The monkeys will now look undistorted and centered on the screen, regardless of
313+
its size and orientation.
314+
315+
## Doing more interesting things
316+
317+
What we did in the previous sections is configure the monkey asset to look correctly based on screen
318+
size. We can make the app a bit more interesting by using some of the features we implemented in the
319+
monkeys tutorial - for example controlling the lights. We can control any property of the scene either
320+
per frame, or when specific events occur (touch gestures, sensors etc.). For example, we can switch the
321+
light direction every two seconds like this:
322+
323+
```kotlin
324+
private var lightId: Property? = null
325+
private var frame = 0
326+
327+
328+
override fun onSceneLoaded() {
329+
// ...
330+
331+
lightId = getLogicNodeRootInput("LightControl")?.getChild("light_id")
332+
}
333+
334+
override fun onUpdate() {
335+
lightId?.set((frame / 60) % 3)
336+
frame += 1
337+
}
338+
```
339+
340+
We simply count the frames, and every 60th frame we toggle the light id by one (and restart
341+
from 0 when we reach 3).
342+
For a more complex example with touch gestures, please see
343+
[a more sophisticated example](#see-a-more-complex-app-example).
344+
345+
## See a more complex app example
346+
347+
This tutorial is designed to be small in code and relatively simple. For a more complex example,
348+
both in terms of
349+
graphics and in terms of control, please check out the
350+
[Digital car app](https://github.com/bmwcarit/ramses-sample-app).

basics/android_app/app_src/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/
5+
.DS_Store
6+
/build
7+
/captures
8+
.externalNativeBuild
9+
.cxx
10+
local.properties
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/build
2+

0 commit comments

Comments
 (0)