כרטיסים בשידור חי

כרטיסים פעילים מופיעים בקטע הנוכחי של ציר הזמן ומציגים מידע שרלוונטי כרגע.

כרטיסים פעילים הם תוספת מעולה למשתמשים שעסוקים במשימה, אבל רוצים מדי פעם לבדוק את Glass כדי לקבל מידע משלים. לדוגמה, אם הם בודקים את הזמן שהם רצים כל כמה דקות או שולטים בנגן מוזיקה כשהם רוצים לדלג על שיר או להשהות אותו.

אם זו הפעם הראשונה שאתם מפתחים את Glass, קראו תחילה את המדריך למשימות מתמשכות. במסמך הזה נפרט איך לבנות כרטיס זכוכית מלא עם כרטיס בזמן אמת, בהתאם לשיטות המומלצות שלנו בנושא עיצוב.

כיצד מילות מפתח שליליות עובדות

כרטיסים פעילים מספקים דרך לשמור על עקביות בכרטיסים בקטע הנוכחי של ציר הזמן, כל עוד הם רלוונטיים. בשונה מכרטיסים סטטיים, כרטיסים חיים לא נשארים בציר הזמן, ומשתמשים מסירים אותם באופן מפורש אחרי השימוש בהם.

המשתמשים בדרך כלל מתחילים כרטיסים בשידור חי באמצעות פקודות קוליות בתפריט הראשי, שמפעיל שירות רקע שמעבד את הכרטיס. לאחר מכן הם יוכלו להקיש על הכרטיס כדי להציג את אפשרויות התפריט בכרטיס, למשל מחיקה שלו מציר הזמן.

מתי כדאי להשתמש בנכסים לחיפוש מפיצים

כרטיסים בשידור חי מיועדים למשימות מתמשכות שמשתמשים יכולים להיכנס אליהן או לצאת מהן לעיתים קרובות, כמו מסך שמציג את הסטטוס של פעולה, מפה מונפשת במהלך הניווט או נגן מוזיקה.

יתרון נוסף של כרטיסים בשידור חי הוא שהם מתאימים היטב לממשקי משתמש שמצריכים אינטראקציה בזמן אמת עם משתמשים ועדכונים בזמן אמת לממשק המשתמש.

בזמן השימוש בכרטיסים חיים, בציר הזמן עדיין יש שליטה על חוויית המשתמש, לכן החלקה קדימה או אחורה בכרטיס בזמן אמת תגרום לניווט בציר הזמן עצמו. בנוסף, המסך מופעל וכבוי בהתאם להתנהגות המערכת (אחרי 5 שניות ללא אינטראקציה של המשתמש או במהלך נדנוד הראש למעלה).

עם זאת, לכרטיסים בשידור חי יש גישה לתכונות רבות כמו בחוויה עשירה, כמו חיישן או נתוני GPS. כך עדיין תוכלו ליצור חוויות מעניינות, וגם לאפשר למשתמשים להישאר בחוויית ציר הזמן כדי לבצע פעולות אחרות, כמו לסמן הודעות.

ארכיטקטורה

לכרטיסים בשידור חי נדרש הקשר ארוך, כדי שהם יהיו בבעלותם לאורך כל התקופה שבה הם גלויים, לכן יש לנהל אותם בשירות רקע.

תוכלו לפרסם ולעבד כרטיס בשידור חי מיד כשהשירות מתחיל או בתגובה לאירועים אחרים שהשירות עוקב אחריהם. אתם יכולים לעבד כרטיסים בשידור חי בתדירות נמוכה (פעם בכמה שניות) או בתדירות גבוהה (עד כמה שהמערכת יכולה לרענן).

כשהכרטיס הפעיל כבר לא רלוונטי, ניתן להפסיק את השירות כדי להפסיק את העיבוד.

עיבוד בתדירות נמוכה

העיבוד בתדירות נמוכה מוגבל לקבוצה קטנה של תצוגות Android, וניתן לעדכן את התצוגה רק פעם אחת בכל כמה שניות.

זו דרך פשוטה ליצור כרטיסים בשידור חי עם תוכן פשוט שלא מצריך רינדור קבוע או עדכונים תכופים.

עיבוד בתדר גבוה

רינדור בתדר גבוה מאפשר לנצל את רוב האפשרויות שזמינות במסגרת הגרפיקה של Android.

המערכת מספקת את שטח הגיבוי האחורי של הכרטיס הפעיל שאליו אתם מכוונים ישירות באמצעות תצוגות ופריסות בדו-ממד, או אפילו גרפיקה תלת-ממדית מורכבת באמצעות OpenGL.

 

יצירת כרטיסים בשידור חי בתדירות נמוכה

כדי לעבד תדרים נמוכים, צריך להשתמש בממשק משתמש שמסופק על ידי האובייקט ViewViews, שתומך בקבוצת המשנה הבאה של פריסות ותצוגות של Android:

יש להשתמש ברינדור בתדירות נמוכה כאשר:

  • תצטרכו רק את ממשקי ה-API הרגילים של תצוגות מפורטות ל-Android, שמפורטים למעלה.
  • עליכם לבצע עדכונים בתדירות נמוכה יחסית (מספר שניות בין רענון הנתונים).

חשוב לזכור:

  • כדי שהציר הזמן יפורסם, צריכה להיות הצהרה על PendingIntent בכרטיסים בזמן אמת באמצעות setAction().
  • כדי לבצע שינויים בכרטיס אחרי הפרסום, צריך לקרוא לו setViews() בכרטיס עם התצוגה המעודכנת של ViewViews לפני הפרסום.

כדי ליצור כרטיסים בשידור חי בתדירות נמוכה:

  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, והטמעת קריאה חוזרת (callback) בממשקים האלה מאפשרת לבצע פעולות במהלך אירועים חשובים במחזור החיים של כרטיס המסך בשידור חי.

    בדוגמה הבאה נוצר שרשור ברקע המשמש לעיבוד מדי פעם, אבל אפשר לעדכן את הכרטיס בתגובה לאירועים חיצוניים (לדוגמה, עדכוני חיישנים או עדכוני מיקום).

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

התמקדות ב-Live Card

כשמפרסמים כרטיס חי עם 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>
    

יצירת פעילות לטיפול בקריאות חוזרות (callback) של תפריט

עליכם להגדיר פעילות בתפריט בכרטיס שהופעל על ידי המשתמשים כאשר הוא מקיש עליו.

שינוי השיטות הבאות ב-Activity לקריאה חוזרת (callback) כדי ליצור, להציג ולסגור תפריטים בפעילות בתפריט כמו שצריך:

  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.