Livekarten

Live-Karten werden im aktuellen Abschnitt der Zeitachse angezeigt und enthalten Informationen, die zum aktuellen Zeitpunkt relevant sind.

Live-Karten eignen sich hervorragend für Nutzer, die aktiv an einer Aufgabe beteiligt sind, aber in Glass regelmäßig nach zusätzlichen Informationen suchen möchten. Zum Beispiel, um alle paar Minuten die Zeit beim Laufen abzurufen oder einen Musikplayer zu steuern, wenn ein Song übersprungen oder pausiert wird.

Wenn Sie zum ersten Mal für Glass entwickeln, lesen Sie zuerst den Leitfaden für laufende Aufgaben. In diesem Dokument wird beschrieben, wie Sie unter Berücksichtigung unserer Best Practices für das Design eine vollständige Glassware mit einer Live-Karte erstellen.

Funktionsweise

Mit Live-Karten können Infokarten im aktuellen Abschnitt der Zeitachse angezeigt werden, solange sie relevant sind. Im Gegensatz zu statischen Karten bleiben Live-Karten nicht in der Zeitachse erhalten und Nutzer entfernen sie explizit, nachdem sie sie nicht mehr verwendet haben.

Nutzer starten Livekarten in der Regel per Sprachbefehl über das Hauptmenü, wodurch ein Hintergrunddienst gestartet wird, der die Karte rendert. Sie können dann auf die Karte tippen, um Menüpunkte aufzurufen, die Aktionen für die Karte ausführen können, z. B. um sie von der Zeitachse zu schließen.

Verwendung

Live-Karten sind für laufende Aufgaben konzipiert, aus denen Nutzer häufig wechseln können, z. B. Anzeigen, die den laufenden Status einer Aktion anzeigen, eine animierte Karte während der Navigation oder ein Musikplayer.

Ein weiterer Vorteil von Livekarten besteht darin, dass sie sich gut für Benutzeroberflächen eignen, die eine Echtzeitinteraktion mit Nutzern und Updates in Echtzeit erfordern.

Bei der Verwendung von Live-Karten hat die Zeitachse weiterhin die Kontrolle über die Nutzererfahrung. Wenn Sie also auf einer Live-Karte vor- oder zurückwischen, wird durch die Zeitachse navigiert, statt auf der Live-Karte selbst zu navigieren. Außerdem schaltet sich der Bildschirm abhängig vom Verhalten des Systems ein und aus (nach 5 Sekunden ohne Nutzerinteraktion oder während einer Erinnerung nach oben).

Livekarten haben jedoch Zugriff auf viele der Funktionen, die auch im immersiven Modus verfügbar sind, z. B. auf Sensor- oder GPS-Daten. So kannst du ansprechende Inhalte erstellen und Nutzern gleichzeitig die Möglichkeit geben, in der Zeitachse andere Aufgaben zu erledigen, z. B. Nachrichten abzurufen.

Architektur

Für Live-Karten ist ein länger andauernder Kontext erforderlich, damit sie während der gesamten Dauer sichtbar sind. Daher solltest du sie in einem Hintergrunddienst verwalten.

Sie können dann eine Live-Karte veröffentlichen und rendern, sobald der Dienst gestartet wird oder auf andere vom Dienst überwachte Ereignisse reagieren. Sie können Live-Karten mit niedriger Häufigkeit (einmal alle paar Sekunden) oder hoher Häufigkeit (maximal so oft, wie das System aktualisiert werden kann) rendern.

Wenn die Live-Karte nicht mehr relevant ist, zerstören Sie den Dienst, um das Rendering zu beenden.

Low-Frequency-Rendering

Das Rendering mit niedriger Häufigkeit ist auf eine kleine Gruppe von Android-Aufrufen beschränkt und kann nur alle paar Sekunden aktualisiert werden.

Es ist eine einfache Möglichkeit, Livekarten mit einfachen Inhalten zu erstellen, die kein ständiges Rendern oder häufige Updates erfordern.

Hochfrequenz-Rendering

Mit dem Hochfrequenzrendering kannst du mehr Optionen nutzen, die im Android-Grafik-Framework verfügbar sind.

Das System zeigt Ihnen die tatsächliche unterstützende Oberfläche der Live-Karte, auf die Sie mithilfe von 2D-Ansichten und -Layouts oder sogar komplexen 3D-Grafiken mit OpenGL direkt zeichnen.

 

Niedrige Live-Karten erstellen

Für das Rendering mit niedriger Häufigkeit ist eine UI erforderlich, die von einem RemoteViews-Objekt bereitgestellt wird und die folgenden Android-Layouts und -Ansichten unterstützt:

In folgenden Fällen sollten Sie das Rendering mit niedriger Häufigkeit verwenden:

  • Sie benötigen nur die oben aufgeführten standardmäßigen Android View APIs.
  • Sie müssen nur relativ selten Updates durchführen, d. h., es liegen wenige Sekunden zwischen den einzelnen Aktualisierungen vor.

Wichtig:

  • Für Live-Karten muss immer eine PendingIntent mit setAction() deklariert sein, damit die Karte im Zeitplan veröffentlicht werden kann.
  • Wenn Sie eine Karte nach der Veröffentlichung ändern möchten, rufen Sie vor der erneuten Veröffentlichung auf der Karte mit dem aktualisierten RemoteViews-Objekt setViews() auf.

So erstellst du Live-Karten mit geringer Häufigkeit:

  1. Erstellen Sie das Layout oder die Ansicht, das bzw. die Sie rendern möchten. Das folgende Beispiel zeigt ein Layout für ein imaginäres Basketballspiel:

     <TextView
         android:id="@+id/home_team_name_text_view"
         android:layout_width="249px"
         android:layout_height="wrap_content"
         android:layout_alignParentRight="true"
         android:gravity="center"
         android:textSize="40px" />
    
     <TextView
         android:id="@+id/away_team_name_text_view"
         android:layout_width="249px"
         android:layout_height="wrap_content"
         android:layout_alignParentLeft="true"
         android:gravity="center"
         android:textSize="40px" />
    
     <TextView
         android:id="@+id/away_score_text_view"
         android:layout_width="249px"
         android:layout_height="wrap_content"
         android:layout_alignLeft="@+id/away_team_name_text_view"
         android:layout_below="@+id/away_team_name_text_view"
         android:gravity="center"
         android:textSize="70px" />
    
     <TextView
         android:id="@+id/home_score_text_view"
         android:layout_width="249px"
         android:layout_height="wrap_content"
         android:layout_alignLeft="@+id/home_team_name_text_view"
         android:layout_below="@+id/home_team_name_text_view"
         android:gravity="center"
         android:textSize="70px" />
    
     <TextView
         android:id="@+id/footer_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
         android:layout_alignParentLeft="true"
         android:layout_marginBottom="33px"
         android:textSize="26px" />
    

  2. Erstellen Sie einen Dienst, der die Live-Karte verwaltet und Ihr Layout oder Ihre Ansicht rendert. Dieser Beispieldienst aktualisiert das Ergebnis eines imaginären Basketballspiels alle 30 Sekunden.

    import java.util.Random;
    
    import com.google.android.glass.timeline.LiveCard;
    import com.google.android.glass.timeline.LiveCard.PublishMode;
    
    import android.app.PendingIntent;
    import android.app.Service;
    import android.content.Intent;
    import android.os.Handler;
    import android.os.IBinder;
    import android.widget.RemoteViews;
    
    public class LiveCardService extends Service {
    
        private static final String LIVE_CARD_TAG = "LiveCardDemo";
    
        private LiveCard mLiveCard;
        private RemoteViews mLiveCardView;
    
        private int homeScore, awayScore;
        private Random mPointsGenerator;
    
        private final Handler mHandler = new Handler();
        private final UpdateLiveCardRunnable mUpdateLiveCardRunnable =
            new UpdateLiveCardRunnable();
        private static final long DELAY_MILLIS = 30000;
    
        @Override
        public void onCreate() {
            super.onCreate();
            mPointsGenerator = new Random();
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if (mLiveCard == null) {
    
                // Get an instance of a live card
                mLiveCard = new LiveCard(this, LIVE_CARD_TAG);
    
                // Inflate a layout into a remote view
                mLiveCardView = new RemoteViews(getPackageName(),
                        R.layout.main_layout);
    
                // Set up initial RemoteViews values
                homeScore = 0;
                awayScore = 0;
                mLiveCardView.setTextViewText(R.id.home_team_name_text_view,
                        getString(R.string.home_team));
                mLiveCardView.setTextViewText(R.id.away_team_name_text_view,
                        getString(R.string.away_team));
                mLiveCardView.setTextViewText(R.id.footer_text,
                        getString(R.string.game_quarter));
    
                // Set up the live card's action with a pending intent
                // to show a menu when tapped
                Intent menuIntent = new Intent(this, MenuActivity.class);
                menuIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                        Intent.FLAG_ACTIVITY_CLEAR_TASK);
                mLiveCard.setAction(PendingIntent.getActivity(
                        this, 0, menuIntent, 0));
    
                // Publish the live card
                mLiveCard.publish(PublishMode.REVEAL);
    
                // Queue the update text runnable
                mHandler.post(mUpdateLiveCardRunnable);
            }
            return START_STICKY;
        }
    
        @Override
        public void onDestroy() {
            if (mLiveCard != null && mLiveCard.isPublished()) {
                //Stop the handler from queuing more Runnable jobs
                mUpdateLiveCardRunnable.setStop(true);
    
                mLiveCard.unpublish();
                mLiveCard = null;
            }
            super.onDestroy();
        }
    
        /**
         * Runnable that updates live card contents
         */
        private class UpdateLiveCardRunnable implements Runnable{
    
            private boolean mIsStopped = false;
    
            /*
             * Updates the card with a fake score every 30 seconds as a demonstration.
             * You also probably want to display something useful in your live card.
             *
             * If you are executing a long running task to get data to update a
             * live card(e.g, making a web call), do this in another thread or
             * AsyncTask.
             */
            public void run(){
                if(!isStopped()){
                    // Generate fake points.
                    homeScore += mPointsGenerator.nextInt(3);
                    awayScore += mPointsGenerator.nextInt(3);
    
                    // Update the remote view with the new scores.
                    mLiveCardView.setTextViewText(R.id.home_score_text_view,
                            String.valueOf(homeScore));
                    mLiveCardView.setTextViewText(R.id.away_score_text_view,
                            String.valueOf(awayScore));
    
                    // Always call setViews() to update the live card's RemoteViews.
                    mLiveCard.setViews(mLiveCardView);
    
                    // Queue another score update in 30 seconds.
                    mHandler.postDelayed(mUpdateLiveCardRunnable, DELAY_MILLIS);
                }
            }
    
            public boolean isStopped() {
                return mIsStopped;
            }
    
            public void setStop(boolean isStopped) {
                this.mIsStopped = isStopped;
            }
        }
    
        @Override
        public IBinder onBind(Intent intent) {
          /*
           * If you need to set up interprocess communication
           * (activity to a service, for instance), return a binder object
           * so that the client can receive and modify data in this service.
           *
           * A typical use is to give a menu activity access to a binder object
           * if it is trying to change a setting that is managed by the live card
           * service. The menu activity in this sample does not require any
           * of these capabilities, so this just returns null.
           */
           return null;
        }
    }
    

Häufig verwendete Live-Karten erstellen

Mit dem Hochfrequenz-Rendering können Sie direkt auf der Hintergrundfläche der Live-Karte zeichnen.

In folgenden Fällen sollten Sie Hochfrequenz-Rendering verwenden:

  • Du musst die Live-Karte häufig (mehrmals pro Sekunde) aktualisieren.
  • Was Sie rendern können, ist flexibel. Mit dem Hochfrequenz-Rendering können Sie Android-Ansichten und -Layouts für komplexe OpenGL-Grafiken verwenden.

Wichtig:

  • Du solltest immer einen Hintergrunddienst erstellen, der auf der Oberfläche der Live-Karte gerendert wird.
  • Bei Live-Karten muss immer ein PendingIntent mit setAction() deklariert sein.
  • Verwenden Sie GLRenderer, wenn Sie OpenGL rendern, und DirectRenderingCallback in allen anderen Fällen.

DirectRenderingCallback verwenden

So erstellen Sie Live-Karten mit Android-Standardansichten und Zeichenlogik:

  1. Erstelle eine Klasse, die DirectRenderingCallback implementiert. Wenn du die Callbacks in diesen Schnittstellen implementierst, kannst du Aktionen während wichtiger Ereignisse im Oberflächenlebenszyklus der Livekarte ausführen.

    Im folgenden Beispiel wird ein Hintergrundthread erstellt, der regelmäßig gerendert wird. Sie können die Karte aber als Reaktion auf externe Ereignisse (z. B. Sensor- oder Standortaktualisierungen) aktualisieren.

    public class LiveCardRenderer implements DirectRenderingCallback {
    
        // About 30 FPS.
        private static final long FRAME_TIME_MILLIS = 33;
    
        private SurfaceHolder mHolder;
        private boolean mPaused;
        private RenderThread mRenderThread;
    
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format,
                int width, int height) {
            // Update your views accordingly.
        }
    
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            mPaused = false;
            mHolder = holder;
            updateRendering();
        }
    
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            mHolder = null;
            updateRendering();
        }
    
        @Override
        public void renderingPaused(SurfaceHolder holder, boolean paused) {
            mPaused = paused;
            updateRendering();
        }
    
        /**
         * Start or stop rendering according to the timeline state.
         */
        private void updateRendering() {
            boolean shouldRender = (mHolder != null) && !mPaused;
            boolean rendering = mRenderThread != null;
    
            if (shouldRender != rendering) {
                if (shouldRender) {
                    mRenderThread = new RenderThread();
                    mRenderThread.start();
                } else {
                    mRenderThread.quit();
                    mRenderThread = null;
                }
            }
        }
    
        /**
         * Draws the view in the SurfaceHolder's canvas.
         */
        private void draw() {
            Canvas canvas;
            try {
                canvas = mHolder.lockCanvas();
            } catch (Exception e) {
                return;
            }
            if (canvas != null) {
                // Draw on the canvas.
                mHolder.unlockCanvasAndPost(canvas);
            }
        }
    
        /**
         * Redraws in the background.
         */
        private class RenderThread extends Thread {
            private boolean mShouldRun;
    
            /**
             * Initializes the background rendering thread.
             */
            public RenderThread() {
                mShouldRun = true;
            }
    
            /**
             * Returns true if the rendering thread should continue to run.
             *
             * @return true if the rendering thread should continue to run
             */
            private synchronized boolean shouldRun() {
                return mShouldRun;
            }
    
            /**
             * Requests that the rendering thread exit at the next
             opportunity.
             */
            public synchronized void quit() {
                mShouldRun = false;
            }
    
            @Override
            public void run() {
                while (shouldRun()) {
                    draw();
                    SystemClock.sleep(FRAME_TIME_MILLIS);
                }
            }
        }
    }
    
  2. Lege eine Instanz von DirectRenderingCallback als Callback des LiveCard-SurfaceHolder fest. Dadurch weiß die Livekarte, welche Logik zum Rendern selbst verwendet werden soll.

    // Tag used to identify the LiveCard in debugging logs.
    private static final String LIVE_CARD_TAG = "my_card";
    
    // Cached instance of the LiveCard created by the publishCard() method.
    private LiveCard mLiveCard;
    
    private void publishCard(Context context) {
        if (mLiveCard == null) {
            mLiveCard = new LiveCard(this, LIVE_CARD_TAG);
    
            // Enable direct rendering.
            mLiveCard.setDirectRenderingEnabled(true);
            mLiveCard.getSurfaceHolder().addCallback(
                    new LiveCardRenderer());
    
            Intent intent = new Intent(context, MenuActivity.class);
            mLiveCard.setAction(PendingIntent.getActivity(context, 0,
                    intent, 0));
            mLiveCard.publish(LiveCard.PublishMode.SILENT);
        } else {
            // Card is already published.
            return;
        }
    }
    
    private void unpublishCard(Context context) {
        if (mLiveCard != null) {
            mLiveCard.unpublish();
            mLiveCard = null;
        }
    }
    

OpenGL verwenden

  1. Erstellen Sie eine Klasse, die GlRenderer implementiert. Durch die Implementierung der Callbacks in dieser Schnittstelle kannst du Aktionen während wichtiger Ereignisse im Lebenszyklus der Live-Karte ausführen. In diesem Beispiel wird ein farbiger, rotierender Würfel gezeichnet.

    import com.google.android.glass.timeline.GlRenderer;
    
    import android.opengl.GLES20;
    import android.opengl.Matrix;
    import android.os.SystemClock;
    
    import java.util.concurrent.TimeUnit;
    import javax.microedition.khronos.egl.EGLConfig;
    
    /**
     * Renders a 3D OpenGL Cube on a {@link LiveCard}.
     */
    public class CubeRenderer implements GlRenderer {
    
        /** Rotation increment per frame. */
        private static final float CUBE_ROTATION_INCREMENT = 0.6f;
    
        /** The refresh rate, in frames per second. */
        private static final int REFRESH_RATE_FPS = 60;
    
        /** The duration, in milliseconds, of one frame. */
        private static final float FRAME_TIME_MILLIS = TimeUnit.SECONDS.toMillis(1) / REFRESH_RATE_FPS;
    
        private final float[] mMVPMatrix;
        private final float[] mProjectionMatrix;
        private final float[] mViewMatrix;
        private final float[] mRotationMatrix;
        private final float[] mFinalMVPMatrix;
    
        private Cube mCube;
        private float mCubeRotation;
        private long mLastUpdateMillis;
    
        public CubeRenderer() {
            mMVPMatrix = new float[16];
            mProjectionMatrix = new float[16];
            mViewMatrix = new float[16];
            mRotationMatrix = new float[16];
            mFinalMVPMatrix = new float[16];
    
            // Set the fixed camera position (View matrix).
            Matrix.setLookAtM(mViewMatrix, 0, 0.0f, 0.0f, -4.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
        }
    
        @Override
        public void onSurfaceCreated(EGLConfig config) {
            // Set the background frame color
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            GLES20.glClearDepthf(1.0f);
            GLES20.glEnable(GLES20.GL_DEPTH_TEST);
            GLES20.glDepthFunc(GLES20.GL_LEQUAL);
            mCube = new Cube();
        }
    
        @Override
        public void onSurfaceChanged(int width, int height) {
            float ratio = (float) width / height;
    
            GLES20.glViewport(0, 0, width, height);
            // This projection matrix is applied to object coordinates in the onDrawFrame() method.
            Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1.0f, 1.0f, 3.0f, 7.0f);
            // modelView = projection x view
            Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
        }
    
        @Override
        public void onDrawFrame() {
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
    
            // Apply the rotation.
            Matrix.setRotateM(mRotationMatrix, 0, mCubeRotation, 1.0f, 1.0f, 1.0f);
            // Combine the rotation matrix with the projection and camera view
            Matrix.multiplyMM(mFinalMVPMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0);
    
            // Draw cube.
            mCube.draw(mFinalMVPMatrix);
            updateCubeRotation();
        }
    
        /** Updates the cube rotation. */
        private void updateCubeRotation() {
            if (mLastUpdateMillis != 0) {
                float factor = (SystemClock.elapsedRealtime() - mLastUpdateMillis) / FRAME_TIME_MILLIS;
                mCubeRotation += CUBE_ROTATION_INCREMENT * factor;
            }
            mLastUpdateMillis = SystemClock.elapsedRealtime();
        }
    }
    
  2. Erstelle einen Dienst, der die Live-Karte verwaltet und die CubeRenderer-Klasse als Renderer der Live-Karte festlegt.

    import com.google.android.glass.timeline.LiveCard;
    import com.google.android.glass.timeline.LiveCard.PublishMode;
    
    import android.app.PendingIntent;
    import android.app.Service;
    import android.content.Intent;
    import android.os.IBinder;
    
    /**
     * Creates a {@link LiveCard} rendering a rotating 3D cube with OpenGL.
     */
    public class OpenGlService extends Service {
    
        private static final String LIVE_CARD_TAG = "opengl";
    
        private LiveCard mLiveCard;
    
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if (mLiveCard == null) {
                mLiveCard = new LiveCard(this, LIVE_CARD_TAG);
                mLiveCard.setRenderer(new CubeRenderer());
                mLiveCard.setAction(
                        PendingIntent.getActivity(this, 0, new Intent(this, MenuActivity.class), 0));
                mLiveCard.publish(PublishMode.REVEAL);
            } else {
                mLiveCard.navigate();
            }
    
            return START_STICKY;
        }
    
        @Override
        public void onDestroy() {
            if (mLiveCard != null && mLiveCard.isPublished()) {
                mLiveCard.unpublish();
                mLiveCard = null;
            }
            super.onDestroy();
        }
    }
    
import android.opengl.GLES20;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

/**
 * Renders a 3D Cube using OpenGL ES 2.0.
 *
 * For more information on how to use OpenGL ES 2.0 on Android, see the
 * <a href="//developer.android.com/training/graphics/opengl/index.html">
 * Displaying Graphics with OpenGL ES</a> developer guide.
 */
public class Cube {

    /** Cube vertices */
    private static final float VERTICES[] = {
        -0.5f, -0.5f, -0.5f,
        0.5f, -0.5f, -0.5f,
        0.5f, 0.5f, -0.5f,
        -0.5f, 0.5f, -0.5f,
        -0.5f, -0.5f, 0.5f,
        0.5f, -0.5f, 0.5f,
        0.5f, 0.5f, 0.5f,
        -0.5f, 0.5f, 0.5f
    };

    /** Vertex colors. */
    private static final float COLORS[] = {
        0.0f, 1.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 1.0f, 1.0f, 1.0f,
        0.0f, 1.0f, 1.0f, 1.0f,
    };


    /** Order to draw vertices as triangles. */
    private static final byte INDICES[] = {
        0, 1, 3, 3, 1, 2, // Front face.
        0, 1, 4, 4, 5, 1, // Bottom face.
        1, 2, 5, 5, 6, 2, // Right face.
        2, 3, 6, 6, 7, 3, // Top face.
        3, 7, 4, 4, 3, 0, // Left face.
        4, 5, 7, 7, 6, 5, // Rear face.
    };

    /** Number of coordinates per vertex in {@link VERTICES}. */
    private static final int COORDS_PER_VERTEX = 3;

    /** Number of values per colors in {@link COLORS}. */
    private static final int VALUES_PER_COLOR = 4;

    /** Vertex size in bytes. */
    private final int VERTEX_STRIDE = COORDS_PER_VERTEX * 4;

    /** Color size in bytes. */
    private final int COLOR_STRIDE = VALUES_PER_COLOR * 4;

    /** Shader code for the vertex. */
    private static final String VERTEX_SHADER_CODE =
            "uniform mat4 uMVPMatrix;" +
            "attribute vec4 vPosition;" +
            "attribute vec4 vColor;" +
            "varying vec4 _vColor;" +
            "void main() {" +
            "  _vColor = vColor;" +
            "  gl_Position = uMVPMatrix * vPosition;" +
            "}";

    /** Shader code for the fragment. */
    private static final String FRAGMENT_SHADER_CODE =
            "precision mediump float;" +
            "varying vec4 _vColor;" +
            "void main() {" +
            "  gl_FragColor = _vColor;" +
            "}";


    private final FloatBuffer mVertexBuffer;
    private final FloatBuffer mColorBuffer;
    private final ByteBuffer mIndexBuffer;
    private final int mProgram;
    private final int mPositionHandle;
    private final int mColorHandle;
    private final int mMVPMatrixHandle;

    public Cube() {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(VERTICES.length * 4);

        byteBuffer.order(ByteOrder.nativeOrder());
        mVertexBuffer = byteBuffer.asFloatBuffer();
        mVertexBuffer.put(VERTICES);
        mVertexBuffer.position(0);

        byteBuffer = ByteBuffer.allocateDirect(COLORS.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
        mColorBuffer = byteBuffer.asFloatBuffer();
        mColorBuffer.put(COLORS);
        mColorBuffer.position(0);

        mIndexBuffer = ByteBuffer.allocateDirect(INDICES.length);
        mIndexBuffer.put(INDICES);
        mIndexBuffer.position(0);

        mProgram = GLES20.glCreateProgram();
        GLES20.glAttachShader(mProgram, loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE));
        GLES20.glAttachShader(
                mProgram, loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE));
        GLES20.glLinkProgram(mProgram);

        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        mColorHandle = GLES20.glGetAttribLocation(mProgram, "vColor");
        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
    }

    /**
     * Encapsulates the OpenGL ES instructions for drawing this shape.
     *
     * @param mvpMatrix The Model View Project matrix in which to draw this shape
     */
    public void draw(float[] mvpMatrix) {
        // Add program to OpenGL environment.
        GLES20.glUseProgram(mProgram);

        // Prepare the cube coordinate data.
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        GLES20.glVertexAttribPointer(
                mPositionHandle, 3, GLES20.GL_FLOAT, false, VERTEX_STRIDE, mVertexBuffer);

        // Prepare the cube color data.
        GLES20.glEnableVertexAttribArray(mColorHandle);
        GLES20.glVertexAttribPointer(
                mColorHandle, 4, GLES20.GL_FLOAT, false, COLOR_STRIDE, mColorBuffer);

        // Apply the projection and view transformation.
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

        // Draw the cube.
        GLES20.glDrawElements(
                GLES20.GL_TRIANGLES, INDICES.length, GLES20.GL_UNSIGNED_BYTE, mIndexBuffer);

        // Disable vertex arrays.
        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glDisableVertexAttribArray(mColorHandle);
    }

    /** Loads the provided shader in the program. */
    private static int loadShader(int type, String shaderCode){
        int shader = GLES20.glCreateShader(type);

        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }
}

Fokus auf Live-Karten legen

Wenn du eine Live-Karte mit LiveCard.publish() veröffentlichst, übergibst du einen Parameter, um zu steuern, ob sie sofort im Fokus ist.

Wenn direkt nach der Veröffentlichung zur Karte gewechselt werden soll, verwende LiveCard.PublishMode.REVEAL. Wenn du die Karte ohne Meldung veröffentlichen und Nutzer selbst dazu veranlassen möchtest, sie aufzurufen, verwende LiveCard.PublishMode.SILENT.

Außerdem kannst du mit der Methode LiveCard.navigate() zur Karte springen, nachdem sie veröffentlicht wurde. Wenn Nutzer beispielsweise versuchen, Ihre Live-Karte über das Hauptmenü für die Spracheingabe zu starten, und diese bereits gestartet ist, können Sie mit dieser Methode zur Live-Karte springen.

Menü erstellen und anzeigen

Für Live-Karten kann kein eigenes Menüsystem angezeigt werden. Sie müssen also eine Aktivität erstellen, um ein Menü für die Live-Karte anzuzeigen.

Die Menüaktivität kann dann Elemente enthalten, mit denen die Live-Karte beendet, ein Eintauchen oder andere Aktionen gestartet werden können. Sie können auch Aktivitäten zu Systemeinstellungen hinzufügen, z. B. die Lautstärkeregelung, als Menüpunkt. Weitere Informationen finden Sie unter Starteinstellungen.

Menüressourcen erstellen

Die Erstellung von Menüressourcen erfolgt auf die gleiche Weise wie auf der Android-Plattform, beachten Sie jedoch die folgenden Richtlinien für Glass:

  • Stellen Sie für jeden Menüpunkt ein 50 × 50 Pixel großes Menüsymbol bereit. Das Menüsymbol muss weiß auf einem transparenten Hintergrund sein. Unter Glass-Menüelementsymbole finden Sie ein Beispiel. Sie können sie auch zur eigenen Verwendung herunterladen.
  • Verwende einen Kurznamen, der die Aktion beschreibt und die Groß-/Kleinschreibung des Titels verwendet. Ein imperatives Verb funktioniert gut, z. B. Teilen oder Allen antworten.
  • Glass zeigt keine Live-Karten ohne Menüpunkt an. Du solltest zumindest einen Menüpunkt Stopp angeben, damit Nutzer die Live-Karte aus der Zeitachse entfernen können.
  • Das CheckBox-Widget wird nicht unterstützt.

    <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item
            android:id="@+id/menu_item_1"
            android:title="@string/Menu_Item_1"       <!-- must have "Stop" menu item -->
            android:icon="@drawable/menu_item_1_icon" />   <!-- white on transparent icon -->
    </menu>
    

Aktivität für Menü-Callbacks erstellen

Sie müssen eine Menüaktivität definieren, die auf Ihrer Livekarte ausgelöst wird, wenn Nutzer darauf tippen.

Überschreiben Sie die folgenden Activity-Callback-Methoden, damit Menüs in Ihrer Menüaktivität richtig erstellt, angezeigt und geschlossen werden können:

  1. onCreateOptionsMenu() erhöht die XML-Menüressource.
  2. onAttachedToWindow() zeigt das Menü an, wenn die Aktivität im Fokus ist.
  3. Mit onPrepareOptionsMenu() können Sie Menüpunkte bei Bedarf ein- oder ausblenden. Sie können z. B. je nach Nutzerverhalten verschiedene Menüpunkte anzeigen. Sie können beispielsweise basierend auf bestimmten Kontextdaten verschiedene Menüelemente anzeigen.
  4. onOptionsItemSelected() übernimmt die Nutzerauswahl.
  5. onOptionsMenuClosed(), um die Aktivität abzuschließen, damit sie nicht mehr auf der Live-Karte angezeigt wird.

Du musst die Aktivität hier abschließen, damit sie richtig abgeschlossen wird, wenn das Menü durch eine Auswahl oder durch Wischen nach unten geschlossen wird.

/**
 * Activity showing the options menu.
 */
public class MenuActivity extends Activity {

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        openOptionsMenu();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.stopwatch, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection.
        switch (item.getItemId()) {
            case R.id.stop:
                stopService(new Intent(this, StopwatchService.class));
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onOptionsMenuClosed(Menu menu) {
        // Nothing else to do, closing the activity.
        finish();
    }
}

Menüaktivität transparent gestalten

Um dem Glass-Stil zu entsprechen, machen Sie die Menüaktivität transparent, damit die Live-Karte weiterhin unter dem Menü angezeigt wird:

  1. Erstellen Sie eine res/values/styles.xml-Datei und deklarieren Sie einen Stil, durch den der Hintergrund der Aktivität transparent wird:

    <resources>
        <style name="MenuTheme" parent="@android:style/Theme.DeviceDefault">
            <item name="android:windowBackground">@android:color/transparent</item>
            <item name="android:colorBackgroundCacheHint">@null</item>
            <item name="android:windowIsTranslucent">true</item>
            <item name="android:windowAnimationStyle">@null</item>
        </style>
    </resources>
    
  2. Weisen Sie in der Datei AndroidManifest.xml das Design der Menüaktivität zu:

    <?xml version="1.0" encoding="utf-8"?>
        <manifest ... >
          ...
            <application ... >
                ...
                <activity
                    android:name=".MenuActivity"
                    android:theme="@style/MenuTheme"
                    ...>
                </activity>
            </application>
    
        </manifest>
    

Menü aufrufen

Gib mit setAction() eine PendingIntent für die Aktion der Karte an. Der ausstehende Intent wird verwendet, um die Menüaktivität zu starten, wenn Nutzer auf die Karte tippen:

Intent menuIntent = new Intent(this, MenuActivity.class);
mLiveCard.setAction(PendingIntent.getActivity(this, 0, menuIntent, 0));
mLiveCard.publish(LiveCard.PublishMode.REVEAL); // or SILENT

Kontextbezogene Sprachbefehle unterstützen

  1. Geben Sie an, dass MenuActivity kontextbezogene Sprachbefehle unterstützt:

    // Initialize your LiveCard as usual.
    mLiveCard.setVoiceActionEnabled(true);
    mLiveCard.publish(LiveCard.PublishMode.REVEAL); // or SILENT
    
  2. Ändere deinen MenuActivity so, dass er den Aufruf über den Sprachablauf unterstützt:

    /**
     * Activity showing the options menu.
     */
    public class MenuActivity extends Activity {
    
        private boolean mFromLiveCardVoice;
        private boolean mIsFinishing;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mFromLiveCardVoice =
                    getIntent().getBooleanExtra(LiveCard.EXTRA_FROM_LIVECARD_VOICE, false);
            if (mFromLiveCardVoice) {
                // When activated by voice from a live card, enable voice commands. The menu
                // will automatically "jump" ahead to the items (skipping the guard phrase
                // that was already said at the live card).
                getWindow().requestFeature(WindowUtils.FEATURE_VOICE_COMMANDS);
            }
        }
    
        @Override
        public void onAttachedToWindow() {
            super.onAttachedToWindow();
            if (!mFromLiveCardVoice) {
                openOptionsMenu();
            }
        }
    
        @Override
        public boolean onCreatePanelMenu(int featureId, Menu menu) {
            if (isMyMenu(featureId)) {
                getMenuInflater().inflate(R.menu.stopwatch, menu);
                return true;
            }
            return super.onCreatePanelMenu(featureId, menu);
        }
    
        @Override
        public boolean onPreparePanel(int featureId, View view, Menu menu) {
            if (isMyMenu(featureId)) {
                // Don't reopen menu once we are finishing. This is necessary
                // since voice menus reopen themselves while in focus.
                return !mIsFinishing;
            }
            return super.onPreparePanel(featureId, view, menu);
        }
    
        @Override
        public boolean onMenuItemSelected(int featureId, MenuItem item) {
            if (isMyMenu(featureId)) {
                // Handle item selection.
                switch (item.getItemId()) {
                    case R.id.stop_this:
                        stopService(new Intent(this, StopwatchService.class));
                        return true;
                }
            }
            return super.onMenuItemSelected(featureId, item);
        }
    
        @Override
        public void onPanelClosed(int featureId, Menu menu) {
            super.onPanelClosed(featureId, menu);
            if (isMyMenu(featureId)) {
                // When the menu panel closes, either an item is selected from the menu or the
                // menu is dismissed by swiping down. Either way, we end the activity.
                isFinishing = true;
                finish();
            }
        }
    
        /**
         * Returns {@code true} when the {@code featureId} belongs to the options menu or voice
         * menu that are controlled by this menu activity.
         */
        private boolean isMyMenu(int featureId) {
            return featureId == Window.FEATURE_OPTIONS_PANEL ||
                   featureId == WindowUtils.FEATURE_VOICE_COMMANDS;
        }
    }
    

Weitere Informationen finden Sie im Leitfaden zu kontextbezogenen Sprachbefehlen.

Es stehen einige Hilfsmethoden zur Verfügung, mit denen Sie das Aussehen und Verhalten von Menüs ändern können. Weitere Informationen finden Sie unter MenuUtils.