即時資訊卡

即時資訊卡會顯示在時間軸的目前部分 顯示目前時間的相關資訊。

即時資訊卡非常適合使用者積極參與 進行了工作,但想定期檢查 Glass 補充資訊。例如查看廣告時間 或控制音樂播放器 要求略過或暫停歌曲

如果您是第一次開發 Glass 閱讀「進行中的工作」指南 首先。本文件將說明 顯示即時資訊卡的玻璃軟體,遵循我們的設計最佳做法。

運作方式

即時資訊卡可讓資訊卡持續顯示在當下 部分。有別於靜態卡片 即時資訊卡不會保留在時間軸中,且使用者明確 等他們使用完畢後再移除

使用者通常可透過語音指令啟動即時資訊卡 ,用於啟動顯示資訊卡的背景服務。 接著,使用者只要輕觸資訊卡,即可顯示可處理的菜單項目 例如從時間軸關閉

使用時機

即時資訊卡是專為使用者能持續參與的工作而設計 包括顯示執行中狀態的螢幕 動作、導航期間的動畫地圖,或音樂播放器。

即時資訊卡的另一個優點是 需要與使用者即時互動的 UI 我們也即時更新使用者介面

使用即時資訊卡時,時間軸仍可控管使用者 只要在即時資訊卡上向前或向後滑動 會瀏覽時間軸,而不是處理即時資訊卡。 此外,螢幕開啟或關閉時,會根據系統的行為 ( 5 秒未與使用者互動,或在抬頭微調時)。

但即時資訊卡有許多相同的功能 「沉浸式」的做法,例如感應器或 GPS 資料。讓你照常打造引人入勝的體驗 同時繼續行程安排 例如檢查訊息

架構

即時資訊卡需要較長的執行時間才能擁有這些資訊卡 因此,請在背景服務中管理這些物件

之後,您就可以在服務推出時,立即發布並顯示即時資訊卡 啟動或回應服務監控的其他事件。 您可以降低頻率 (每幾秒) 顯示即時資訊卡。 或高頻率 (最多可重新整理系統重新整理的次數)。

當動態卡片不再相關時,請銷毀服務, 停止顯示。

低頻率轉譯

低頻率算繪作業僅適用於少數 Android 而且每兩週更新一次 幾秒內。

只要透過 Google 助理建立即時資訊卡, 不需要持續顯示的簡單內容,或 經常更新

高頻率轉譯

高頻率顯示功能可讓您使用更多選項 適用於 Android 圖形架構中的可用物件

系統會提供 透過 2D 直接繪製的即時資訊卡 使用 OpenGL。

 

建立低頻率的即時資訊卡

低頻率轉譯需要由 RemoteViews 物件,可支援下列部分 Android 版面配置和檢視畫面:

低頻率算繪使用時機:

  • 只需要上述的標準 Android View API。
  • 更新頻率只需 重新整理)。

注意事項:

  • 即時資訊卡必須一律具有 PendingIntent敬上 宣告 setAction()。 ,等待系統發布資訊卡的時間
  • 如要在發布後變更資訊卡,請呼叫 setViews()敬上 更新後 RemoteViews 的資訊卡上 物件然後再次發布

如何建立低頻率的即時資訊卡:

  1. 建立您要算繪的版面配置或檢視畫面。以下範例 顯示虛構籃球遊戲的版面配置:

     <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. 建立服務來管理即時資訊卡,並轉譯版面配置或檢視畫面。 這個範例服務會在每次收到虛構籃球比賽時,更新分數 30 秒。

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

建立高頻率的即時資訊卡

高頻率顯示功能可讓您直接繪製 就能顯示即時資訊卡的圖示

使用高頻率顯示的時機:

  • 您需要經常更新即時資訊卡 (每秒更新多次)。
  • 您需要靈活設定要呈現的內容。高頻率轉譯功能 使用 Android View 和版面配置處理複雜的 OpenGL 圖形。

注意事項:

使用 DirectRenderingCallback

如何使用標準 Android 檢視畫面和繪製邏輯建立即時資訊卡:

  1. 建立實作 DirectRenderingCallback、 只要在這些介面中實作回呼,即可在即時資訊卡的介面生命週期的重要事件期間執行動作。

    以下範例會建立背景執行緒來定期轉譯 因應外部事件 (例如 感應器或位置更新通知)。

    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. 設定 DirectRenderingCallback 的執行個體 (為 LiveCard) SurfaceHolder 的回呼。這個 可讓即時資訊卡知道要使用什麼邏輯來算繪本身。

    // 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

  1. 建立實作 GlRenderer。 實作此介面的回呼可讓您在重要事件發生時執行動作 動態資訊卡的途徑生命週期階段本例繪製的是旋轉的彩色方塊。

    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. 建立服務來管理即時資訊卡和組合 CubeRenderer 類別做為即時資訊卡的轉譯器。

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

為即時資訊卡提供焦點

透過 LiveCard.publish() 發布即時資訊卡時,系統會將參數傳遞 來控制焦點是否立即成為焦點

如要在發布後立即顯示時間軸,請使用 LiveCard.PublishMode.REVEAL。 如要在無訊息的情況下發布資訊卡,讓使用者自行瀏覽資訊卡,請使用 LiveCard.PublishMode.SILENT

此外,LiveCard.navigate() 方法,可讓您在資訊卡發布後跳至該資訊卡。舉例來說,如果使用者嘗試啟動 則可透過主要語音選單的即時資訊卡與已啟動的即時資訊卡,跳到即時影像 以增加訂閱人數

建立並顯示選單

即時資訊卡無法顯示自己的選單系統,因此你必須建立活動才能顯示選單 查看即時資訊卡

選單活動可設定項目來停止即時資訊卡, 或者任何您想採取的行動並新增 系統設定活動 (例如音量控制) 顯示為選單項目。如要 資訊,請參閱 起始設定

建立選單資源

建立選單資源與 Android 平台相同,但請按照以下方式操作 Glass 規範:

  • 針對每個選單項目,提供一個 50 × 50 像素的選單項目圖示。菜單 圖示必須是白色,並使用透明背景。詳情請參閱 Glass 選單項目圖示: 或下載影片供自己使用
  • 請使用描述動作的簡稱,並採用每字字首大寫格式。命令式動詞 (例如「分享」或「回覆所有人」)。
  • 沒有選單項目時,Glass 不會顯示即時資訊卡。至少 提供「Stop」選單項目,讓使用者可以將即時資訊卡從 時間軸上。
  • CheckBox 小工具 不支援。

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

建立用於處理選單回呼的活動

您必須定義使用者輕觸即時資訊卡時會叫用的選單活動。

覆寫下列內容 Activity 回呼方法 如要在選單活動中正確建立、顯示及關閉選單,請按照下列步驟操作:

  1. onCreateOptionsMenu()敬上 加載 XML 選單資源。
  2. onAttachedToWindow()敬上 在活動處於焦點時,顯示選單。
  3. onPrepareOptionsMenu()敬上 視需要顯示或隱藏選單項目例如,您可以顯示不同的選單項目 調整設定例如,您可以依據下列條件顯示不同的選單項目: 對於某些內容比對資料
  4. onOptionsItemSelected() 會處理使用者選取工作。
  5. onOptionsMenuClosed()敬上 完成活動後,該活動就不會再顯示於即時資訊卡上。

您必須在這裡完成活動,以便在選項由選項關閉或向下滑動時正確完成。

/**
 * 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();
    }
}

將選單活動設為透明

為了與 Glass 樣式保持一致,請製作選單活動 所以即時資訊卡仍會顯示在下方 選單:

  1. 建立 res/values/styles.xml 檔案並宣告樣式 讓活動的背景保持透明:

    <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. AndroidManifest.xml 檔案中,將主題指派給選單活動:

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

顯示選單

提供 PendingIntent敬上 使用 setAction() 判斷資訊卡動作待處理意圖已用於啟動 使用者輕觸資訊卡時顯示選單活動:

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

支援內容相關語音指令

  1. 表示您的 MenuActivity 支援 情境語音指令

    // Initialize your LiveCard as usual.
    mLiveCard.setVoiceActionEnabled(true);
    mLiveCard.publish(LiveCard.PublishMode.REVEAL); // or SILENT
    
  2. 修改 MenuActivity,支援透過語音流程叫用:

    /**
     * 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;
        }
    }
    

查看 關聯語音指令 指南。

你可以使用幾種輔助方法修改選單的外觀和行為。詳情請見 MenuUtils敬上 瞭解詳情