We want to detect when the user looks at a thumbnail and highlight the image by changing its border color. If users move their gaze away from the thumbnail, it will unhighlight. When the user clicks on the Cardboard trigger, that image is loaded.
Fortunately, we implemented the isLooking
detection in the RenderBox
library at the end of Chapter 5, RenderBox Engine. If you remember, the technique determines whether the user is looking at the plane by checking whether the vector between the camera and the plane position is the same as the camera's view direction, within a threshold of tolerance.
We can use this in MainActivity
. We'll write a selectObject
helper method that checks whether any of the objects in the scene are selected and highlights them. First, let's declare some variables at the top of the MainActivity
class. The selectedThumbnail
object holds the currently selected thumbnail index. We define border colors for normal and selected states:
final float[] selectedColor = new float[]{0, 0.5f, 0.5f, 1}; final float[] invalidColor = new float[]{0.5f, 0, 0, 1}; final float[] normalColor = new float[]{0, 0, 0, 1}; Thumbnail selectedThumbnail = null;
Now the selectObject
method goes through each thumbnail, checks whether it's isLooking
, and highlights (or unhighlights) it accordingly:
void selectObject() { selectedThumbnail = null; for (Thumbnail thumb : thumbnails) { if (thumb.image == null) return; Plane plane = thumb.plane; BorderMaterial material = (BorderMaterial) plane.getMaterial(); if (plane.isLooking) { selectedThumbnail = thumb; material.borderColor = selectedColor; } else { material.borderColor = normalColor; } } }
RenderBox
provides hooks, including postDraw
where we'll check for selected objects. We want to use postDraw
because we need to wait until draw
is called on all of RenderObjects
before we know which one the user is looking at. In MainActivity
, add a call to the selectObject
method as follows:
@Override public void postDraw() { selectObject(); }
Run the project. As you gaze at a thumbnail image, it should get highlighted!
Well, now that we can pick an image from the thumbnail grid, we need a way to click on it and show that image. That'll happen in MainActivity
using the Cardboard SDK hook, onCardboardTrigger
.
With all the work we've done so far, it's not going to take much more to implement this:
@Override public void onCardboardTrigger() { if (selectedThumbnail != null) { showImage(selectedThumbnail.image); } }
Try and run it. Now highlight an image and pull the trigger. If you're lucky, it'll work…mine crashes.
What's going on? We're running into thread-safe issues. So far, we've been executing all of our code from the render thread, which is started by the GLSurfaceView
/CardboardView
class via the Cardboard SDK. This thread owns the access to the GPU and to the particular surface we're rendering on. The call to onCardboardTrigger
originates from a thread that is not the render thread. This means that we can't make any OpenGL calls from here. Luckily, GLSurfaceView
provides a nifty way to execute arbitrary code on the render thread through a method called queueEvent
. The queueEvent
method takes a single Runnable
argument, which is a Java class meant to create one-off procedures such as these (refer to http://developer.android.com/reference/android/opengl/GLSurfaceView.html#queueEvent(java.lang.Runnable).
Modify showImage
to wrap it inside a Runnable
argument, as follows:
void showImage(final Image image) { cardboardView.queueEvent(new Runnable() { @Override public void run() { UnlitTexMaterial bgMaterial = (UnlitTexMaterial) photosphere.getMaterial(); image.loadFullTexture(cardboardView); if (image.isPhotosphere) { Log.d(TAG, "!!! is photosphere"); bgMaterial.setTexture(image.textureHandle); screen.enabled = false; } else { bgMaterial.setTexture(bgTextureHandle); screen.enabled = true; image.show(cardboardView, screen); } } }); }
Note that any data passed to the anonymous class, such as our image, must be declared final
to be accessible from the new procedure.
Try to run the project again. It should work. You can gaze at a thumbnail, click on the trigger, and that image will be shown, either on the virtual screen or in the background photosphere.
No worries, we're keeping it clean. We want to provide some haptic feedback to the user when an image has been selected, using the phone's vibrator. And fortunately, in Android, that's straightforward.
First, make sure that your AndroidManifest.xml
file includes the following line of code:
<uses-permission android:name="android.permission.VIBRATE" />
At the top of the MainActivity
class, declare a vibrator
variable:
private Vibrator vibrator;
Then, in onCreate
, add the following code to initialize it:
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
Then, use it in onCardboardTrigger
, as follows:
vibrator.vibrate(25);
Run it again. Click on it and you'll feel it. Ahhh! But don't get carried away, it's not that kind of vibrator.