การ์ดสด

การ์ดสดจะปรากฏในส่วนปัจจุบันของไทม์ไลน์และแสดงข้อมูลที่เกี่ยวข้อง ณ เวลาปัจจุบัน

การ์ดเรียลไทม์เหมาะที่จะใช้เมื่อผู้ใช้มีส่วนร่วมในงานหนึ่งๆ อยู่แล้วแต่ต้องการดูข้อมูลเพิ่มเติมเป็นระยะๆ ใน Glass เช่น ตรวจสอบเวลาในการวิ่งทุกๆ 2-3 นาที หรือควบคุมโปรแกรมเล่นเพลงเมื่อต้องการข้ามหรือหยุดเพลงชั่วคราว

หากนี่เป็นครั้งแรกที่คุณพัฒนา Glass โปรดอ่านคู่มืองานต่อเนื่องก่อน เอกสารนั้นพูดถึงวิธีสร้าง Glasslasware ที่สมบูรณ์ด้วยการ์ดเรียลไทม์ตามแนวทางปฏิบัติแนะนําในการออกแบบของเรา

วิธีการทำงาน

การ์ดแบบสดช่วยให้วิธีแสดงการ์ดในส่วนปัจจุบันของไทม์ไลน์ตราบใดที่การ์ดเหล่านั้นมีความเกี่ยวข้อง การ์ดสดแตกต่างจากการ์ดแบบคงที่ตรงที่ไทม์ไลน์จะหายไป และระบบจะนําการ์ดออกอย่างชัดเจนหลังจากดําเนินการเสร็จแล้ว

โดยทั่วไปแล้ว ผู้ใช้จะเริ่มใช้การ์ดสดด้วยการพูดคําสั่งเสียงในเมนูหลัก ซึ่งจะเริ่มบริการในเบื้องหลังที่แสดงการ์ด จากนั้นแตะการ์ดเพื่อแสดงรายการในเมนูที่สามารถดําเนินการบนการ์ดได้ เช่น ปิดไปจากไทม์ไลน์

ใช้เมื่อใด

การ์ดสดออกแบบมาเพื่อรองรับงานต่อเนื่องที่ผู้ใช้กระโดดเข้าและออกบ่อยๆ ได้ เช่น จอแสดงผลที่แสดงสถานะการทํางานของการดําเนินการ แผนที่แบบเคลื่อนไหวระหว่างการนําทาง หรือโปรแกรมเล่นเพลง

ประโยชน์อีกอย่างของการ์ดเรียลไทม์คือการ์ดเหมาะสําหรับ UI ที่จําเป็นต้องมีการโต้ตอบแบบเรียลไทม์กับผู้ใช้ และการอัปเดต UI แบบเรียลไทม์

เมื่อใช้การ์ดเรียลไทม์ ไทม์ไลน์ยังคงควบคุมประสบการณ์ของผู้ใช้ได้ ดังนั้นการเลื่อนไปข้างหน้าหรือย้อนกลับบนการ์ดสดจะเป็นการไปยังส่วนต่างๆ ของไทม์ไลน์แทนการดําเนินการกับการ์ดสดนั้น นอกจากนี้ หน้าจอจะเปิดและปิดตามลักษณะการทํางานของระบบ (หลังจาก 5 วินาทีที่ไม่มีการโต้ตอบของผู้ใช้หรือระหว่างการขยับศีรษะ)

อย่างไรก็ตาม การ์ดเรียลไทม์อาจเข้าถึงฟีเจอร์ต่างๆ ได้หลายอย่างเหมือนกับการแช่ เช่น เซ็นเซอร์หรือข้อมูล GPS การดําเนินการนี้จะช่วยให้คุณยังคงสร้างประสบการณ์ที่น่าสนใจได้ขณะเดียวกันก็ยังช่วยให้ผู้ใช้อยู่ในไทม์ไลน์อื่นๆ เพื่อทําสิ่งต่างๆ ได้ เช่น ตรวจสอบข้อความ

สถาปัตยกรรม

การ์ดสดต้องอาศัยบริบทที่ยาวนานในการเป็นเจ้าของการ์ดตลอดช่วงเวลาที่มองเห็น จึงควรจัดการการ์ดดังกล่าวในบริการในเบื้องหลัง

จากนั้นคุณจะเผยแพร่และแสดงผลการ์ดสดได้ทันทีที่บริการเริ่มหรือเพื่อตอบสนองต่อเหตุการณ์อื่นๆ ที่บริการตรวจสอบ คุณแสดงผลการ์ดเรียลไทม์ได้ความถี่ต่ํา (1 ครั้งทุกๆ 2-3 วินาที) หรือความถี่สูง (สูงสุดเท่าที่ระบบรีเฟรชได้)

เมื่อการ์ดการถ่ายทอดสดไม่เกี่ยวข้องอีกต่อไป ให้ทําลายบริการเพื่อหยุดการแสดงผล

การแสดงผลความถี่ต่ํา

การแสดงผลที่มีความถี่จํากัดจะจํากัดอยู่ที่มุมมอง Android กลุ่มเล็กๆ เท่านั้น และจะอัปเดตการแสดงผลได้ทุกๆ 2-3 วินาทีเท่านั้น

เป็นวิธีง่ายๆ ในการสร้างการ์ดสดที่มีเนื้อหาเรียบง่ายซึ่งไม่จําเป็นต้องแสดงผลอย่างสม่ําเสมอหรือมีการอัปเดตเป็นประจํา

การแสดงผลที่มีความถี่สูง

การแสดงผลที่มีความถี่สูงช่วยให้คุณใช้ตัวเลือกเพิ่มเติม ในเฟรมเวิร์กกราฟิกของ Android ได้

ระบบจะแสดงภาพพื้นหลังจริงของการ์ดสดที่คุณวาดไว้ได้โดยตรงโดยใช้มุมมอง 2 มิติและเลย์เอาต์ หรือแม้แต่กราฟิก 3 มิติที่ซับซ้อนด้วย OpenGL

 

การสร้างบัตรข้อมูลสดที่มีความถี่ต่ํา

การแสดงผลที่มีความถี่ต่ําต้องใช้ UI ที่ได้รับจากออบเจ็กต์ remoteViews ซึ่งรองรับชุดย่อยและมุมมอง Android ต่อไปนี้

ใช้การแสดงผลที่มีความถี่ต่ําในกรณีต่อไปนี้

  • คุณต้องใช้ API มุมมองของ Android มาตรฐานที่ระบุไว้ก่อนหน้านี้เท่านั้น
  • คุณเพียงต้องอัปเดตไม่บ่อย (ไม่กี่วินาทีระหว่างการรีเฟรช)

ข้อควรทราบ

  • การ์ดสดต้องมีการประกาศ 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 กับกราฟิก OpenGL ที่ซับซ้อนได้

ข้อควรทราบ

  • คุณควรสร้างบริการพื้นหลังเพื่อแสดงผลบนแพลตฟอร์มของบัตรที่ใช้งานอยู่อยู่เสมอ
  • การ์ดสดต้องมีการประกาศ PendingIntent ด้วย setAction() เสมอ
  • ใช้ GLRenderer หากคุณแสดงผล OpenGL และ DirectRenderingCallback สําหรับเคสอื่นๆ ทั้งหมด

การใช้ 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 x 50 พิกเซล ไอคอนเมนูต้องเป็นสีขาวบนพื้นหลังโปร่งใส ดูไอคอนเมนูรายการกระจกเพื่อดูตัวอย่างหรือดาวน์โหลดไฟล์เพื่อการใช้งานของคุณเอง
  • ใช้ชื่อย่อที่อธิบายการดําเนินการและคํานึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ คํากริยาที่จําเป็น ก็ทํางานได้ดี (เช่น แชร์หรือตอบทั้งหมด)
  • Glass ไม่แสดงการ์ดสดที่ไม่มีรายการเมนู อย่างน้อยที่สุด ให้ระบุรายการในเมนูหยุดเพื่อให้ผู้ใช้นําการ์ดสดออกจากไทม์ไลน์ได้
  • ไม่รองรับวิดเจ็ต 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 ที่รอดําเนินการจะใช้เพื่อเริ่มกิจกรรมเมนูเมื่อผู้ใช้แตะการ์ด ดังนี้

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

ดูคําแนะนําเพิ่มเติมได้ในคําสั่งเสียงตามบริบท

วิธีการช่วยมี 2-3 วิธีพร้อมใช้งานเพื่อแก้ไขรูปลักษณ์และลักษณะการทํางานของเมนู ดูข้อมูลเพิ่มเติมที่MenuUtils