Skip to content

Files

Latest commit

1de56a1 · Dec 25, 2021

History

History
843 lines (601 loc) · 38.6 KB

File metadata and controls

843 lines (601 loc) · 38.6 KB

四、LauncherLobby

这个项目创建了一个纸板虚拟现实应用,可以用来启动安装在你设备上的其他纸板应用。我们称之为LauncherLobby。当您打开 LauncherLobby 时,您将看到多达 24 个水平排列的图标。当你把头转向右边或左边时,图标会滚动,就好像它们在一个圆柱体里面一样。你可以通过盯着它的图标并扣动卡纸板的扳机来打开一个应用。

对于这个项目,我们采用最少的方法来创建立体视图。该项目使用标准的安卓视图组布局模拟视差,并简单地将图像在每只眼睛中向左或向右移动,从而创建视差视觉效果。我们不使用三维图形。我们不直接使用 OpenGL,尽管大多数现代版本的 Android 都使用 OpenGL 渲染视图。事实上,我们几乎不使用纸板软件开发工具包;我们只使用它来绘制分屏叠加,并获得头部方向。然而,视图布局和图像移动逻辑是从谷歌的“寻宝”示例(用于绘制文本覆盖图)中派生出来的。

这种方法的优点是多方面的。它说明了即使没有高级图形、矩阵数学、渲染引擎和物理,也可以构建纸板应用。当然,这些通常是必需的,但在这种情况下,它们不是必需的。如果你有安卓开发经验,这里用到的类和模式可能特别熟悉。这个项目展示了如何纸板虚拟现实,至少,只需要一个纸板 SDK 头部转换和分屏布局,以产生立体应用。

实际上,我们选择这种方法是为了使用安卓的文本视图。在 3D 中渲染任意文本实际上非常复杂(尽管肯定是可能的),所以为了简单起见,我们将这个项目限制在 2D 视图和安卓布局。

为了构建该项目,我们将首先向您介绍将文本字符串和图标图像放在屏幕上并立体查看它们的一些基础知识。然后,我们将设计一个虚拟屏幕,就像一个未展开的圆柱体内部一样工作。水平转动你的头就像在这个虚拟屏幕上平移一样。屏幕将被分成多个窗口,每个窗口都包含一个纸板应用的图标和名称。凝视并点击其中一个插槽将启动相应的应用。如果您使用过纸板样本应用(在撰写本文时如此称呼),这个界面会很熟悉。

在本章中,我们将涵盖以下主题:

  • 创建新的纸板项目
  • 添加一个你好虚拟世界文本覆盖
  • 使用虚拟屏幕空间
  • 回应头部表情
  • 向视图添加图标
  • 列出已安装的纸板应用
  • 突出显示当前应用快捷方式
  • 使用触发器选择并启动应用

这个项目的源代码可以在 Packt Publishing 网站和位于https://github.com/cardbookvr/launcherlobby的 GitHub 上找到(每个主题作为一个单独的提交)。

创建新项目

如果您想要更多的细节和这些步骤的解释,请参考第 2 章创建新纸板项目一节中的框架纸板项目,然后继续:

  1. Android Studio 打开后,创建一个新项目。让我们将其命名为LauncherLobby,并以空活动为目标安卓 4.4 KitKat (API 19)

  2. 使用文件 | | 新模块,将纸板 SDK common.aarcore.aar库文件作为新模块添加到项目中...

  3. 使用文件 | 项目结构,将库模块设置为项目应用的依赖项。

  4. 按照第二章框架纸板项目中的说明编辑AndroidManifest.xml文件,注意保留该项目的package名称。

  5. 按照第 2 章框架纸板项目中的说明编辑build.gradle文件,根据 SDK 22 进行编译。

  6. 编辑activity_main.xml布局文件,如第 2 章框架纸板项目所述。

  7. 编辑MainActivity Java 类,使其扩展CardboardActivity并实现CardboardView.StereoRenderer。修改类申报行如下:

    public class MainActivity extends CardboardActivity implements CardboardView.StereoRenderer {
  8. 为界面添加存根方法覆盖(使用智能感知实现方法或按 Ctrl + I )。

  9. 最后,通过添加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);  
        }

添加 Hello 虚拟世界文本覆盖

首先,我们只是在屏幕上放一些文字,你可以用来给用户敬酒,或者一个带有信息内容的抬头显示器 ( 抬头显示器)。我们将逐步实施:

  1. 用一些文字创建一个简单的叠加视图。
  2. 把它放在屏幕中央。
  3. 添加视差以实现立体观看。

简单的文本叠加

首先我们来简单的添加一些叠加文字,不是立体的,只是屏幕上的文字。这将是我们对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实例,它有一个令人愉快的绿色和文本你好虚拟世界!

运行应用,你会注意到我们的文字在屏幕左上角,如下图截图所示:

A simple text overlay

使用子视图将文本居中

接下来,我们创建一个单独的视图组,并使用它来控制文本对象。具体来说,就是在视图中居中。

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%。

运行该应用,您会注意到我们的文本现在位于屏幕中央:

Center the text using a child view

为每只眼睛创建立体视图

现在,我们变得真实。事实上也就是。对于 VR,我们需要立体的左右眼视图。幸运的是,我们有这个方便的OverlayEye类,可以为每只眼睛重用。

你的眼睛之间有一个可测量的距离,这个距离被称为你的瞳距 ( IPD )。当您在硬纸板耳机中查看立体图像时,每只眼睛都有单独的视图,并偏移(水平)相应的距离。

假设我们的文本位于垂直于视图方向的平面上。也就是说,我们直视文本平面。给定一个与文本到眼睛的距离相对应的数值,我们可以将左眼和右眼的视图水平移动固定数量的像素,以创建视差效果。我们称之为depthOffset值。较大的深度偏移会使文本看起来更近;较小的深度偏移将导致文本看起来更远。深度偏移为零将表示没有视差,就好像文本非常远(大于 20 英尺)。

对于我们的应用,我们将选择 0.01 的深度偏移系数,或者在屏幕坐标中测量的 1%(屏幕大小的一小部分)。图标将出现在大约 2 米远(6 英尺)的地方,这对于虚拟现实来说是一个舒适的距离,尽管这个值是一个临时的近似值。使用屏幕尺寸的百分比而不是实际的像素数量,我们可以确保我们的应用能够适应任何屏幕/设备尺寸。

让我们现在就实现它。

首先,在OverlayView类的顶部为leftEyerightEye值声明变量:

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);
   }

注意中间的六行,我们在这里定义leftViewrightView,并为它们调用addViewsetDepthFactor调用将在视图中设置该值。

为深度、颜色和文本内容添加 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);

当你运行应用时,你的屏幕会是这样的:

Create stereoscopic views for each eye

文本现在是立体视图,尽管它“粘在你的脸上”,因为当你的头移动时它不会移动。它附着在遮阳板或平视显示器上。

从主活动控制覆盖视图

下一步是从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());

这将以像素为单位显示手机的物理宽度和高度。

使用虚拟屏幕

在虚拟现实中,你所看到的空间比给定时间屏幕上的空间要大。屏幕就像进入虚拟空间的视窗。在这个项目中,我们不计算三维视图和裁剪平面,我们将头部运动限制在左/右偏航旋转。

你可以把可见的空间想象成一个圆柱体的内表面,你的头在中心。当你旋转你的头时,一部分未散开的圆柱体显示在屏幕上。

Using a virtual screen

虚拟屏幕的高度以像素为单位与物理设备相同。

我们需要计算虚拟宽度。例如,一种方法是计算出头部旋转一个角度的像素数。那么,完整旋转的宽度将是每度像素 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);
    }

MainActivityonCreate方法中,添加以下调用:

        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方法获得的姿态为三个角度,称为欧拉角(发音为加油器,围绕俯仰、偏航和滚转三个轴:

  • 俯仰转动你的头,好像点头“是”
  • 偏航将头转向左/右(好像摇“否”)
  • 把头从耳朵转到肩膀上(“滚桶!”)

这些轴分别对应于 XYZ 坐标轴。我们将把这种体验限制在偏航,当你从一排菜单项中向左或向右看时。因此,我们将第二个欧拉角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方法中。

MainActivityonCreate方法中,修改addContent调用,将图标作为第二个参数传递:

        Drawable icon = getResources()
            .getDrawable(R.drawable.android_robot, null);
        overlayView.addContent("Hello Virtual World!", icon);

OverlayViewaddContent中,添加图标参数并将其传递给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;

修改OverlayEyeaddContent,以便也取一个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告诉视图保持图像的纵横比。

OverlayEyeonLayout方法中设置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 );
        }

运行应用。你的屏幕会是这样的。当你移动你的头时,图标和文本都会滚动。

Adding an icon to the view

列出已安装的纸板应用

如果你没有忘记,这个 LauncherLobby 应用的目的是在设备上显示一个 Cardboard 应用列表,让用户选择一个来启动它。

如果您喜欢我们到目前为止所构建的内容,您可能希望保存一份副本以供将来参考。我们接下来要做的更改将显著修改代码,以支持视图列表作为应用的快捷方式。

我们将用addShortcut替换addContent方法,用快捷方式列表替换imageViewtextView变量。每个快捷方式包括一个ImageView和一个TextView来显示快捷方式,以及一个ActivityInfo对象来启动应用。快捷方式图像和文本将出现在彼此之上,如前所示,并将水平排列成一行,相隔固定距离。

纸板应用的查询

首先,让我们获取设备上安装的纸板应用列表。在MainActivityonCreate方法的末尾,添加对新方法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 . htmlhttp://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方法。这将为布局构建一个TextViewImageView实例列表。

注意maxShortcutsshortcutWidth变量。maxShortcuts定义了我们想要在虚拟屏幕上容纳的最大快捷键数量,shortcutWidth将是屏幕上每个快捷键插槽的宽度。在calcVirtualWidth()中初始化shortcutWidth,在calcVirtualWidth末尾增加以下一行代码:

        shortcutWidth = virtualWidth / maxShortcuts;

在重叠视图中使用视图列表

OverlayEye的顶部,用列表替换textViewimageView变量:

    private class OverlayEye extends LinearLayout {
        private final List<TextView> textViews = new ArrayList<TextView>();
        private final List<ImageView> imageViews = new ArrayList<ImageView>();

现在我们准备在OverlayEye中编写addShortcut方法。这看起来很像我们正在取代的addContent方法。它创建了textViewimageView(如前所述),但随后将它们填充到一个列表中:

        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会保留图像纵横比。

删除OverlayViewOverlayEye类中过时的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++ ;
            }
        }

运行应用。你现在会看到你的纸板快捷方式整齐地排列在一个水平菜单中你可以通过转动你的头滚动。

Using view lists in OverlayEye

请注意,外面的一些 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。这一次,我们正在构建一个软件抽象层,它有助于封装许多较低层次的细节和内务处理。这个引擎也可以在本书的其他项目中重用。