这个项目创建了一个纸板虚拟现实应用,可以用来启动安装在你设备上的其他纸板应用。我们称之为LauncherLobby。当您打开 LauncherLobby 时,您将看到多达 24 个水平排列的图标。当你把头转向右边或左边时,图标会滚动,就好像它们在一个圆柱体里面一样。你可以通过盯着它的图标并扣动卡纸板的扳机来打开一个应用。
对于这个项目,我们采用最少的方法来创建立体视图。该项目使用标准的安卓视图组布局模拟视差,并简单地将图像在每只眼睛中向左或向右移动,从而创建视差视觉效果。我们不使用三维图形。我们不直接使用 OpenGL,尽管大多数现代版本的 Android 都使用 OpenGL 渲染视图。事实上,我们几乎不使用纸板软件开发工具包;我们只使用它来绘制分屏叠加,并获得头部方向。然而,视图布局和图像移动逻辑是从谷歌的“寻宝”示例(用于绘制文本覆盖图)中派生出来的。
这种方法的优点是多方面的。它说明了即使没有高级图形、矩阵数学、渲染引擎和物理,也可以构建纸板应用。当然,这些通常是必需的,但在这种情况下,它们不是必需的。如果你有安卓开发经验,这里用到的类和模式可能特别熟悉。这个项目展示了如何纸板虚拟现实,至少,只需要一个纸板 SDK 头部转换和分屏布局,以产生立体应用。
实际上,我们选择这种方法是为了使用安卓的文本视图。在 3D 中渲染任意文本实际上非常复杂(尽管肯定是可能的),所以为了简单起见,我们将这个项目限制在 2D 视图和安卓布局。
为了构建该项目,我们将首先向您介绍将文本字符串和图标图像放在屏幕上并立体查看它们的一些基础知识。然后,我们将设计一个虚拟屏幕,就像一个未展开的圆柱体内部一样工作。水平转动你的头就像在这个虚拟屏幕上平移一样。屏幕将被分成多个窗口,每个窗口都包含一个纸板应用的图标和名称。凝视并点击其中一个插槽将启动相应的应用。如果您使用过纸板样本应用(在撰写本文时如此称呼),这个界面会很熟悉。
在本章中,我们将涵盖以下主题:
- 创建新的纸板项目
- 添加一个你好虚拟世界文本覆盖
- 使用虚拟屏幕空间
- 回应头部表情
- 向视图添加图标
- 列出已安装的纸板应用
- 突出显示当前应用快捷方式
- 使用触发器选择并启动应用
这个项目的源代码可以在 Packt Publishing 网站和位于https://github.com/cardbookvr/launcherlobby的 GitHub 上找到(每个主题作为一个单独的提交)。
如果您想要更多的细节和这些步骤的解释,请参考第 2 章创建新纸板项目一节中的框架纸板项目,然后继续:
-
Android Studio 打开后,创建一个新项目。让我们将其命名为
LauncherLobby
,并以空活动为目标安卓 4.4 KitKat (API 19) 。 -
使用文件 | 新 | 新模块,将纸板 SDK
common.aar
和core.aar
库文件作为新模块添加到项目中...。 -
使用文件 | 项目结构,将库模块设置为项目应用的依赖项。
-
按照第二章、框架纸板项目中的说明编辑
AndroidManifest.xml
文件,注意保留该项目的package
名称。 -
按照第 2 章、框架纸板项目中的说明编辑
build.gradle
文件,根据 SDK 22 进行编译。 -
编辑
activity_main.xml
布局文件,如第 2 章、框架纸板项目所述。 -
编辑
MainActivity
Java 类,使其扩展CardboardActivity
并实现CardboardView.StereoRenderer
。修改类申报行如下:public class MainActivity extends CardboardActivity implements CardboardView.StereoRenderer {
-
为界面添加存根方法覆盖(使用智能感知实现方法或按 Ctrl + I )。
-
最后,通过添加
CardboadView
实例来编辑onCreate()
,如下所示:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CardboardView cardboardView = (CardboardView) findViewById(R.id.cardboard_view); cardboardView.setRenderer(this); setCardboardView(cardboardView); }
首先,我们只是在屏幕上放一些文字,你可以用来给用户敬酒,或者一个带有信息内容的抬头显示器 ( 抬头显示器)。我们将逐步实施:
- 用一些文字创建一个简单的叠加视图。
- 把它放在屏幕中央。
- 添加视差以实现立体观看。
首先我们来简单的添加一些叠加文字,不是立体的,只是屏幕上的文字。这将是我们对OverlayView
类的初步实现。
打开activity_main.xml
文件,添加以下几行,在布局中添加一个OverlayView
:
<.OverlayView
android:id="@+id/overlay"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
请注意,我们仅用.OverlayView
引用OverlayView
类。如果你的视图类和你的MainActivity
类在同一个包中,你可以这样做。我们之前为.MainActivity
做了同样的事情。
接下来,我们编写 Java 类。右键点击app/java
文件夹(app/src/main/java/com.cardbookvr.launcherlobby/
,导航至新建 | Java 类。命名为OverlayView
。
定义类,使其扩展LinearLayout
,并添加一个构造方法,如下所示:
public class OverlayView extends LinearLayout{
public OverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
TextView textView = new TextView(context, attrs);
addView(textView);
textView.setTextColor(Color.rgb(150, 255, 180));
textView.setText("Hello Virtual World!");
setVisibility(View.VISIBLE);
}
}
OverlayView()
构造器方法创建一个新的TextView
实例,它有一个令人愉快的绿色和文本你好虚拟世界!。
运行应用,你会注意到我们的文字在屏幕左上角,如下图截图所示:
接下来,我们创建一个单独的视图组,并使用它来控制文本对象。具体来说,就是在视图中居中。
在OverlayView
构造函数中,用一个不同的ViewGroup
助手类的实例替换TextView
,我们将编写这个类叫做EyeView
。目前,它是单视场的,但是很快我们将使用这个类创建两个视图:一个用于每只眼睛:
public OverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutParams params = new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f);
params.setMargins(0, 0, 0, 0);
OverlayEye eye = new OverlayEye(context, attrs);
eye.setLayoutParams(params);
addView(eye);
eye.setColor(Color.rgb(150, 255, 180));
eye.addContent("Hello Virtual World!");
setVisibility(View.VISIBLE);
}
我们创建一个名为 eye 的OverlayEye
的新实例,设置它的颜色,并添加文本字符串。
使用ViewGroup
类时,需要指定LayoutParams
告诉家长如何布局视图,我们希望是全屏大小,没有边距(参考。LayoutParams.html)。
在同一个OverlayView.java
文件中,我们要添加名为OverlayEye
的私有类,如下所示:
private class OverlayEye extends ViewGroup {
private Context context;
private AttributeSet attrs;
private TextView textView;
private int textColor;
public OverlayEye(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
this.attrs = attrs;
}
public void setColor(int color) {
this.textColor = color;
}
public void addContent(String text) {
textView = new TextView(context, attrs);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(textColor);
textView.setText(text);
addView(textView);
}
}
我们已经将TextView
创建从OverlayEye
构造器中分离出来。这样做的原因很快就会变得清楚。
OverlayEye
构造器注册向组中添加新内容视图所需的上下文和属性。
然后,addContent
创建TextView
实例并将其添加到布局中。
现在我们为OverlayEye
定义onLayout
,它设置文本视图的边距,特别是上边距,作为强制文本垂直居中的机制:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = right - left;
final int height = bottom - top;
final float verticalTextPos = 0.52f;
float topMargin = height * verticalTextPos;
textView.layout(0, (int) topMargin, width, bottom);
}
为了使文本垂直居中,我们使用上边距从屏幕顶部向下推。文本将垂直放置在屏幕中心的正下方,如verticalTextPos
所指定的,百分比值 1.0 是屏幕的全高。我们选择了 0.52 的值,将文本的顶部向下推到屏幕中间下方额外的 2%。
运行该应用,您会注意到我们的文本现在位于屏幕中央:
现在,我们变得真实。事实上也就是。对于 VR,我们需要立体的左右眼视图。幸运的是,我们有这个方便的OverlayEye
类,可以为每只眼睛重用。
你的眼睛之间有一个可测量的距离,这个距离被称为你的瞳距 ( IPD )。当您在硬纸板耳机中查看立体图像时,每只眼睛都有单独的视图,并偏移(水平)相应的距离。
假设我们的文本位于垂直于视图方向的平面上。也就是说,我们直视文本平面。给定一个与文本到眼睛的距离相对应的数值,我们可以将左眼和右眼的视图水平移动固定数量的像素,以创建视差效果。我们称之为depthOffset
值。较大的深度偏移会使文本看起来更近;较小的深度偏移将导致文本看起来更远。深度偏移为零将表示没有视差,就好像文本非常远(大于 20 英尺)。
对于我们的应用,我们将选择 0.01 的深度偏移系数,或者在屏幕坐标中测量的 1%(屏幕大小的一小部分)。图标将出现在大约 2 米远(6 英尺)的地方,这对于虚拟现实来说是一个舒适的距离,尽管这个值是一个临时的近似值。使用屏幕尺寸的百分比而不是实际的像素数量,我们可以确保我们的应用能够适应任何屏幕/设备尺寸。
让我们现在就实现它。
首先,在OverlayView
类的顶部为leftEye
和rightEye
值声明变量:
public class OverlayView extends LinearLayout{
private final OverlayEye leftEye;
private final OverlayEye rightEye;
在OverlayView
构造函数方法中初始化它们:
public CardboardOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutParams params = new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f);
params.setMargins(0, 0, 0, 0);
leftEye = new OverlayEye(context, attrs);
leftEye.setLayoutParams(params);
addView(leftEye);
rightEye = new OverlayEye(context, attrs);
rightEye.setLayoutParams(params);
addView(rightEye);
setDepthFactor(0.01f);
setColor(Color.rgb(150, 255, 180));
addContent("Hello Virtual World!");
setVisibility(View.VISIBLE);
}
注意中间的六行,我们在这里定义leftView
和rightView
,并为它们调用addView
。setDepthFactor
调用将在视图中设置该值。
为深度、颜色和文本内容添加 setter 方法:
public void setDepthFactor(float factor) {
leftEye.setDepthFactor(factor);
rightEye.setDepthFactor(-factor);
}
public void setColor(int color) {
leftEye.setColor(color);
rightEye.setColor(color);
}
public void addContent(String text) {
leftEye.addContent(text);
rightEye.addContent(text);
}
重要提示:请注意,对于rightEye
值,我们使用负值的偏移值。为了创建视差效果,需要将其移动到左眼视图的相反方向。我们仍然可以通过只移动一只眼睛来实现视差,但是所有的内容看起来都会稍微偏离中心。
OverlayEye
类需要深度因子设置器,我们将其转换为像素depthOffset
。另外,为物理视图宽度声明一个变量(以像素为单位):
private int depthOffset;
private int viewWidth;
在onLayout
中,以像素为单位设置计算后的视图宽度:
viewWidth = width;
定义 setter 方法,该方法将深度因子转换为像素偏移:
public void setDepthFactor(float factor) {
this.depthOffset = (int)(factor * viewWidth);
}
现在,当我们在addContent
中创建textView
时,我们可以通过depthOffset
像素值来移动它:
textView.setX(depthOffset);
addView(textView);
当你运行应用时,你的屏幕会是这样的:
文本现在是立体视图,尽管它“粘在你的脸上”,因为当你的头移动时它不会移动。它附着在遮阳板或平视显示器上。
下一步是从MainActivity
类中移除一些硬编码属性并控制它们。
在MainActivity.java
中,在类的顶部添加一个overlayView
变量:
public class MainActivity extends CardboardActivity implements CardboardView.StereoRenderer {
private OverlayView overlayView;
在onCreate
中初始化其值。我们将使用addContent()
方法显示文本:
...
setCardboardView(cardboardView);
overlayView = (OverlayView) findViewById(R.id.overlay);
overlayView.addContent("Hello Virtual World");
不要忘记从OverlayView
方法中移除对addContent
的调用:
setDepthOffset(0.01f);
setColor(Color.rgb(150, 255, 180));
addContent("Hello Virtual World!");
setVisibility(View.VISIBLE);
}
再次运行该应用。它看起来应该和前面显示的一样。
您可以使用这样的代码来创建一个 3D 吐司,例如文本通知消息。或者,它可以用来构建一个 HUD 面板,以共享游戏中的状态或报告当前的设备属性。例如,要显示当前屏幕参数,您可以将其放入MainActivity
:
ScreenParams sp = cardboardView.getHeadMountedDisplay().getScreenParams();
overlayView.setText(sp.toString());
这将以像素为单位显示手机的物理宽度和高度。
在虚拟现实中,你所看到的空间比给定时间屏幕上的空间要大。屏幕就像进入虚拟空间的视窗。在这个项目中,我们不计算三维视图和裁剪平面,我们将头部运动限制在左/右偏航旋转。
你可以把可见的空间想象成一个圆柱体的内表面,你的头在中心。当你旋转你的头时,一部分未散开的圆柱体显示在屏幕上。
虚拟屏幕的高度以像素为单位与物理设备相同。
我们需要计算虚拟宽度。例如,一种方法是计算出头部旋转一个角度的像素数。那么,完整旋转的宽度将是每度像素 360* 。
我们可以很容易地找到显示器的物理宽度(以像素为单位)。事实上,我们已经在onLayout
中找到了它作为viewWidth
变量。或者,可以从纸板软件开发工具包调用中检索:
ScreenParams sp = cardboardView.getHeadMountedDisplay().getScreenParams();
Log.d(TAG, "screen width: " + sp.getWidth());
从 SDK 中,我们还可以得到 Cardboard 耳机的视场 ( FOV )角度(以度为单位)。该值因设备而异,是纸板设备配置参数的一部分:
FieldOfView fov = cardboardView.getHeadMountedDisplay().getCardboardDeviceParams().getLeftEyeMaxFov();
Log.d(TAG, "FOV: " + fov.getLeft());
鉴于此,我们可以计算出每度的像素数和虚拟屏幕的总像素宽度。例如,在我的 Nexus 4 上,设备宽度(横向模式)为 1,280,使用 Homido 查看器,FOV 为 40.0 度。因此,分屏视图为 640 像素,每度 16.0 像素,虚拟屏幕宽度为 5760 像素。
在此过程中,我们还可以计算并记住pixelsPerRadian
值,这将有助于根据当前用户的HeadTransform
(以弧度给出)确定头部偏移。
我们来补充一下。在OverlayView
类的顶部,添加以下变量:
private int virtualWidth;
private float pixelsPerRadian;
然后,添加以下方法:
public void calcVirtualWidth(CardboardView cardboard) {
int screenWidth = cardboard.getHeadMountedDisplay().getScreenParams().getWidth() / 2;
float fov = cardboard.getCardboardDeviceParams().getLeftEyeMaxFov().getLeft();
float pixelsPerDegree = screenWidth / fov;
pixelsPerRadian = (float) (pixelsPerDegree * 180.0 / Math.PI);
virtualWidth = (int) (pixelsPerDegree * 360.0);
}
在MainActivity
的onCreate
方法中,添加以下调用:
overlayView.calcVirtualWidth(cardboardView);
请注意,从设备参数报告的 FOV 值是耳机制造商定义的粗略近似值,在某些设备中,可能会被高估和填充。实际的 FOV 可以从传递给onDrawEye()
的眼睛对象中检索到,因为这表示应该渲染的实际平截头体。一旦项目开始工作,您可能会考虑对自己的代码进行这种更改。
现在,我们可以使用这些值来响应用户的头部外观旋转。
让我们用头移动文字,这样看起来就不会粘在你脸上了!当你向左或向右看时,我们会将文本向相反的方向移动,所以它在空间中看起来是静止的。
为此,我们将从MainActivity
开始。在onNewFrame
方法中,我们将确定水平头部旋转角度,并将其传递给overlayView
对象。
在MainActivity
中,定义onNewFrame
:
public void onNewFrame(HeadTransform headTransform) {
final float[] angles = new float[3];
headTransform.getEulerAngles(angles, 0);
runOnUiThread(new Runnable() {
@Override
public void run() {
overlayView.setHeadYaw(angles[1]);
}
});
}
onNewFrame
方法接收当前的HeadTransform
实例作为参数,它是一个提供当前头部姿态的对象。
有多种方法可以从数学上表示头部姿态,例如前向 XYZ 方向向量或角度组合。getEulerAngles
方法获得的姿态为三个角度,称为欧拉角(发音为加油器,围绕俯仰、偏航和滚转三个轴:
- 俯仰转动你的头,好像点头“是”
- 偏航将头转向左/右(好像摇“否”)
- 滚把头从耳朵转到肩膀上(“滚桶!”)
这些轴分别对应于 X 、 Y 和 Z 坐标轴。我们将把这种体验限制在偏航,当你从一排菜单项中向左或向右看时。因此,我们将第二个欧拉角angles[1]
发送到overlayView
类。
注意runOnUiThread
的使用,确保overlayView
更新在 UI 线程上运行。否则会造成各种异常,破坏 UI(可以参考。
所以,回到OverlayView
中,给headOffset
添加一个变量和一个设置它的方法,setHeadYaw
:
private int headOffset;
public void setHeadYaw(float angle) {
headOffset = (int)( angle * pixelsPerRadian );
leftEye.setHeadOffset(headOffset);
rightEye.setHeadOffset(headOffset);
}
这里的想法是将头部旋转转换为屏幕上文本对象的位置偏移。当你的头转向左边时,把物体移到右边。当你的头转向右边时,把物体移到左边。因此,当你转动你的头时,物体在屏幕上滚动。
我们从纸板 SDK 获得的偏航角(围绕垂直 Y 轴的旋转)以弧度为单位。我们计算与头部方向相反的偏移视图的像素数。因此,我们取角度并乘以pixelsPerRadian
。我们为什么不否定这个角度呢?结果只是顺时针旋转在 Y 轴上被记录为负旋转。去想想。
最后,在OverlayEye
中,定义setHeadOffset
方法来改变视图对象的 X 位置。确保你也包括了depthOffset
变量:
public void setHeadOffset(int headOffset) {
textView.setX( headOffset + depthOffset );
}
运行应用。当你移动你的头时,文本应该向相反的方向滚动。
接下来,我们将向视图添加图标图像。
现在,让我们只使用一个通用图标,如android_robot.png
。这本书的副本可以在网上找到,本章的文件中也有一份副本。将android_robot.png
文件粘贴到项目的app/res/drawable/
文件夹中。别担心,我们稍后会使用实际的应用图标。
我们希望同时显示文本和图标,因此我们可以添加代码,以便将图像视图添加到addContent
方法中。
在MainActivity
的onCreate
方法中,修改addContent
调用,将图标作为第二个参数传递:
Drawable icon = getResources()
.getDrawable(R.drawable.android_robot, null);
overlayView.addContent("Hello Virtual World!", icon);
在OverlayView
的addContent
中,添加图标参数并将其传递给OverlayEye
视图:
public void addContent(String text, Drawable icon) {
leftEye.addContent(text, icon);
rightEye.addContent(text, icon);
}
现在为OverlayEye
班。在OverlayEye
的顶部,向ImageView
实例添加一个变量:
private class OverlayEye extends ViewGroup {
private TextView textView;
private ImageView imageView;
修改OverlayEye
的addContent
,以便也取一个Drawable
图标并为其创建ImageView
实例。修改后的方法现在如下所示:
public void addContent(String text, Drawable icon) {
textView = new TextView(context, attrs);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(textColor);
textView.setText(text);
addView(textView);
imageView = new ImageView(context, attrs);
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setAdjustViewBounds(true);
// preserve aspect ratio
imageView.setImageDrawable(icon);
addView(imageView);
}
使用imageView.setScaleType.CENTER_INSIDE
告诉视图从其中心缩放图像。将setAdjustViewBounds
设置为true
告诉视图保持图像的纵横比。
在OverlayEye
的onLayout
方法中设置ImageView
的布局参数。在onLayout
方法的底部添加以下代码:
final float imageSize = 0.1f;
final float verticalImageOffset = -0.07f;
float imageMargin = (1.0f - imageSize) / 2.0f;
topMargin = (height * (imageMargin + verticalImageOffset));
float botMargin = topMargin + (height * imageSize);
imageView.layout(0, (int) topMargin, width, (int) botMargin);
当图像被绘制时,它将适合上边距和下边距,自动缩放。换句话说,给定所需的图像尺寸(如屏幕高度的 10%,或 0.1f),图像边距因子为 (1 - size)/2 ,乘以屏幕的像素高度,得到以像素为单位的边距。我们还添加了一个小的垂直偏移量(负的,向上移动),用于图标和它下面的文本之间的间距。
最后,将imageView
偏移量添加到setHeadOffset
方法中:
public void setHeadOffset(int headOffset) {
textView.setX( headOffset + depthOffset );
imageView.setX( headOffset + depthOffset );
}
运行应用。你的屏幕会是这样的。当你移动你的头时,图标和文本都会滚动。
如果你没有忘记,这个 LauncherLobby 应用的目的是在设备上显示一个 Cardboard 应用列表,让用户选择一个来启动它。
如果您喜欢我们到目前为止所构建的内容,您可能希望保存一份副本以供将来参考。我们接下来要做的更改将显著修改代码,以支持视图列表作为应用的快捷方式。
我们将用addShortcut
替换addContent
方法,用快捷方式列表替换imageView
和textView
变量。每个快捷方式包括一个ImageView
和一个TextView
来显示快捷方式,以及一个ActivityInfo
对象来启动应用。快捷方式图像和文本将出现在彼此之上,如前所示,并将水平排列成一行,相隔固定距离。
首先,让我们获取设备上安装的纸板应用列表。在MainActivity
的onCreate
方法的末尾,添加对新方法getAppList
的调用:
getAppList();
然后,在MainActivity
中定义该方法,如下:
private void getAppList() {
final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory("com.google.intent.category.CARDBOARD");
mainIntent.addFlags(PackageManager.GET_INTENT_FILTERS);
final List<ResolveInfo> pkgAppsList = getPackageManager().queryIntentActivities( mainIntent, PackageManager.GET_INTENT_FILTERS);
for (ResolveInfo info : pkgAppsList) {
Log.d("getAppList", info.loadLabel(getPackageManager()).toString());
}
}
运行它,在 Android Studio 查看logcat
窗口。代码获取当前设备(pkgAppsList
)上的纸板应用列表,并将它们的标签(name
)打印到调试控制台。
纸板应用是通过拥有CARDBOARD
意图类别来识别的,所以我们以此进行过滤。调用addFlags
并在queryIntentActivities
中指定标志是很重要的,因为否则我们将不会得到意图过滤器的列表,并且应用的注释将匹配CARDBOARD
类别。另外,注意我们使用的是Activity
类的getPackageManager()
函数。如果您需要将这个方法放在另一个类中,它将需要对活动的引用。我们将在本书后面再次使用意图。关于包管理器和 Intents 的更多信息,请参考http://developer . Android . com/reference/Android/content/pm/package manager . html和http://developer . Android . com/reference/Android/content/intent . html。
接下来,我们将定义一个Shortcut
类,在一个方便的对象中保存我们需要的每个 Cardboard 应用的细节。
创建一个名为Shortcut
的新 Java 类。定义如下:
public class Shortcut {
private static final String TAG = "Shortcut";
public String name;
public Drawable icon;
ActivityInfo info;
public Shortcut(ResolveInfo info, PackageManager packageManager){
name = info.loadLabel(packageManager).toString();
icon = info.loadIcon(packageManager);
this.info = info.activityInfo;
}
}
在MainActivity
中,修改getAppList()
从pkgAppsList
构建快捷方式并添加到overlayView
:
...
int count = 0;
for (ResolveInfo info : pkgAppsList) {
overlayView.addShortcut( new Shortcut(info, getPackageManager()));
if (++ count == 24)
break;
}
我们需要限制视图圆柱体内的快捷方式的数量。在这种情况下,我选择 24 作为一个合理的数字。
现在,我们修改OverlayView
以支持将被渲染的快捷方式列表。首先,声明一个列表变量shortcuts
,来保存它们:
public class OverlayView extends LinearLayout {
private List<Shortcut> shortcuts = new ArrayList<Shortcut>();
private final int maxShortcuts = 24;
private int shortcutWidth;
addShortcut
方法如下:
public void addShortcut(Shortcut shortcut){
shortcuts.add(shortcut);
leftEye.addShortcut(shortcut);
rightEye.addShortcut(shortcut);
}
如您所见,这在OverlayEye
类中调用了addShortcut
方法。这将为布局构建一个TextView
和ImageView
实例列表。
注意maxShortcuts
和shortcutWidth
变量。maxShortcuts
定义了我们想要在虚拟屏幕上容纳的最大快捷键数量,shortcutWidth
将是屏幕上每个快捷键插槽的宽度。在calcVirtualWidth()
中初始化shortcutWidth
,在calcVirtualWidth
末尾增加以下一行代码:
shortcutWidth = virtualWidth / maxShortcuts;
在OverlayEye
的顶部,用列表替换textView
和imageView
变量:
private class OverlayEye extends LinearLayout {
private final List<TextView> textViews = new ArrayList<TextView>();
private final List<ImageView> imageViews = new ArrayList<ImageView>();
现在我们准备在OverlayEye
中编写addShortcut
方法。这看起来很像我们正在取代的addContent
方法。它创建了textView
和imageView
(如前所述),但随后将它们填充到一个列表中:
public void addShortcut(Shortcut shortcut) {
TextView textView = new TextView(context, attrs);
textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12.0f);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(textColor);
textView.setText(shortcut.name);
addView(textView);
textViews.add(textView);
ImageView imageView = new ImageView(context, attrs);
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setAdjustViewBounds(true);
imageView.setImageDrawable(shortcut.icon);
addView(imageView);
imageViews.add(imageView);
}
将setAdjustViewBounds
设置为true
会保留图像纵横比。
删除OverlayView
和OverlayEye
类中过时的addContent
方法定义。
在onLayout
中,我们现在遍历textViews
的列表,如下所示:
for(TextView textView : textViews) {
textView.layout(0, (int) topMargin, width, bottom);
}
我们还遍历imageViews
列表,如下所示:
for(ImageView imageView : imageViews) {
imageView.layout(0, (int) topMargin, width, (int) botMargin);
}
最后,我们还需要迭代setHeadOffset
中的列表:
public void setHeadOffset(int headOffset) {
int slot = 0;
for(TextView textView : textViews) {
textView.setX(headOffset + depthOffset + (shortcutWidth * slot));
slot++ ;
}
slot = 0;
for(ImageView imageView : imageViews) {
imageView.setX(headOffset + depthOffset + (shortcutWidth * slot));
slot++ ;
}
}
运行应用。你现在会看到你的纸板快捷方式整齐地排列在一个水平菜单中你可以通过转动你的头滚动。
请注意,外面的一些 Java 程序员可能会指出,每个OverlayEye
类中的快捷方式列表和视图列表都是多余的。的确如此,但事实证明,将每只眼睛的绘制功能重构到Shortcut
类中是相当复杂的。我们发现这种方式是最简单易懂的。
当用户凝视一个快捷方式时,应该能够表示该快捷方式是可选的。在下一节中,我们将把它连接起来,以突出显示选定的项目,并实际启动相应的应用。
这里的技巧是确定哪个插槽在用户前面。为了突出显示它,我们将使文本颜色变亮。
让我们编写一个助手方法,根据headOffset
变量(根据头部偏航角度计算)来确定当前凝视的位置。将getSlot
方法添加到OverlayView
类:
public int getSlot() {
int slotOffset = shortcutWidth/2 - headOffset;
slotOffset /= shortcutWidth;
if(slotOffset < 0)
slotOffset = 0;
if(slotOffset >= shortcuts.size())
slotOffset = shortcuts.size() - 1;
return slotOffset;
}
shortcutWidth
值的一半被添加到headOffset
值,因此我们检测到凝视捷径的中心。然后,我们添加headOffset
的负值,因为它最初是作为位置偏移计算的,与视图方向相反。headOffset
的负值实际上对应大于零的槽号。
getSlot
应该返回一个介于 0 和我们的虚拟布局中的槽数之间的数字;在这种情况下,它的 24。由于可以向右看并设置一个正的headOffset
变量,getSlot
可以返回负数,所以我们检查边界条件。
现在,我们可以突出显示当前选择的插槽。我们将通过更改文本标签颜色来实现。修改setHeadOffset
如下:
public void setHeadOffset(int headOffset) {
int currentSlot = getSlot();
int slot = 0;
for(TextView textView : textViews) {
textView.setX(headOffset + depthOffset + (shortcutWidth * slot));
if (slot==currentSlot) {
textView.setTextColor(Color.WHITE);
} else {
textView.setTextColor(textColor);
}
slot++ ;
}
slot = 0;
for(ImageView imageView : imageViews) {
imageView.setX(headOffset + depthOffset + (shortcutWidth * slot));
slot++ ;
}
}
运行该应用,你眼前的项目将会高亮显示。当然,可能还有其他有趣的方法来突出显示所选的应用,但目前这已经足够好了。
最后一块是检测用户正在注视哪个快捷方式,并通过启动应用来响应触发(点击)。
当我们从这个应用中推出一个新应用时,我们需要引用MainActivity
对象。一种方法是使它成为单例对象。我们现在就开始吧。请注意,将活动定义为单件可能会遇到麻烦。安卓可以启动单个Activity
类的多个实例,但即使是跨应用,静态变量也是共享的。
在MainActivity
类的顶部,添加一个instance
变量:
public static MainActivity instance;
在onCreate
中初始化:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
instance = this;
现在在MainActivity
中,给纸板触发器添加一个处理器:
@Override
public void onCardboardTrigger(){
overlayView.onTrigger();
}
然后,在OverlayView
中,增加以下方法:
public void onTrigger() {
shortcuts.get( getSlot() ).launch();
}
我们使用getSlot
来索引我们的快捷方式列表。因为我们检查了getSlot
本身的边界条件,所以我们不需要担心ArrayIndexOutOfBounds
异常。
最后,在Shortcut
上增加一个launch()
方法:
public void launch() {
ComponentName name = new ComponentName(info.applicationInfo.packageName,
info.name);
Intent i = new Intent(Intent.ACTION_MAIN);
i.addCategory(Intent.CATEGORY_LAUNCHER);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
i.setComponent(name);
if(MainActivity.instance != null) {
MainActivity.instance.startActivity(i);
} else {
Log.e(TAG, "Cannot find activity singleton");
}
}
我们使用存储在Shortcut
类中的对象来创建一个新的Intent
实例,然后用它作为参数调用MainActivity.instance.startActivity
来启动应用。
请注意,一旦你启动了一个新的应用,就没有系统范围的方法可以从虚拟现实中回到启动状态。用户必须将手机从纸板浏览器中取出,然后点击后退按钮。然而,软件开发工具包确实支持CardboardView.setOnCardboardBackButtonListener
,如果你想显示后退或退出按钮,可以将其添加到你的纸板应用中。
给你。劳恩切洛比已经准备好摇滚了。
关于如何改进和提高该项目的一些想法包括以下内容:
- 支持超过 24 个快捷方式,也许可以添加多行或无限滚动机制
- 重用图像和文本视图对象;你一次只能看到几个
- 目前,真正长的应用标签会重叠,调整视图代码使文本换行,或者引入省略号(...)当标签太长时
- 添加圆柱形背景图像(天空框)
- 突出显示当前快捷方式的替代方法,可能是发光,或者通过调整其视差偏移将其移近
- 添加声音和/或振动以增强体验并强化选择反馈
在本章中,我们构建了 LauncherLobby 应用,该应用可用于在您的设备上启动其他 Cardboard 应用。我们没有使用三维图形和 OpenGL,而是使用安卓图形用户界面和虚拟圆柱形屏幕来实现这一点。
实现的第一部分主要是指导性的:如何添加一个TextView
覆盖,将其放在视图组的中心,然后用左右眼视差图立体显示。然后,我们根据当前的物理设备大小和当前的 Cardboard 设备视场参数,确定了虚拟屏幕(未展开的圆柱体)的大小。当用户左右移动他的头时(偏航旋转),对象在虚拟屏幕上滚动。最后,我们在安卓设备上查询已安装的纸板应用,在水平菜单中显示它们的图标和标题,并允许您通过凝视它并点击触发器来选择一个启动。
在下一章,我们回到三维图形和 OpenGL。这一次,我们正在构建一个软件抽象层,它有助于封装许多较低层次的细节和内务处理。这个引擎也可以在本书的其他项目中重用。