البطاقات المباشرة

تظهر البطاقات المباشرة في القسم الحالي من المخطط الزمني تعرض معلومات ذات صلة في الوقت الحالي.

تكون البطاقات المباشرة مناسبة عندما يكون المستخدمون نشطين المشاركة في مهمة، ولكنها ترغب في فحص Glass للحصول على معلومات تكميلية. على سبيل المثال، يمكن أن يؤدي التحقق من وقتهم التشغيل كل بضع دقائق أو التحكم في مشغِّل الموسيقى عندما يريدون تخطّي أغنية أو إيقافها مؤقتًا

إذا كانت هذه أول مرة تعمل فيها على تطوير Glass، اقرأ دليل المهام المستمرة أولاً. يتناول هذا المستند كيفية إنشاء خطة أدوات زجاجية مع بطاقة مباشرة، وفقًا لأفضل ممارسات التصميم لدينا.

آلية عملها

تتيح البطاقات المباشرة إبقاء البطاقات في الوقت الحالي. من الجدول الزمني طالما أنها ذات صلة. على عكس البطاقات الثابتة، لا يستمر ظهور البطاقات المباشرة في المخطط الزمني، ويبدأ المستخدمون وإزالتها بعد الانتهاء منها.

يبدأ المستخدمون عادةً بطاقات مباشرة من خلال توجيه طلب صوتي. في القائمة الرئيسية، ما يؤدي إلى بدء خدمة تعمل في الخلفية لعرض البطاقة. ويمكنهم بعد ذلك النقر على البطاقة لعرض العناصر التي يمكن تطبيقها في القائمة على البطاقة، مثل إزالتها من المخطط الزمني.

حالات الاستخدام

تم تصميم البطاقات المباشرة للمهام المستمرة التي يمكن للمستخدمين البدء فيها. أو بشكل متكرر، مثل شاشة تعرض حالة التنفيذ لأحد الإجراءات، أو خريطة متحركة أثناء التنقل، أو مشغّل موسيقى.

من الفوائد الأخرى للبطاقات المباشرة أنها مناسبة تمامًا واجهات المستخدم التي تتطلّب تفاعلاً في الوقت الفعلي مع المستخدمين وتحديثات فورية لواجهة المستخدم

عند استخدام البطاقات المباشرة، يظل بإمكان المخطط الزمني التحكّم في بيانات المستخدم. تجربة المستخدم، لذا التمرير سريعًا للأمام أو للخلف على البطاقة المباشرة في المخطط الزمني بدلاً من ذلك على البطاقة المباشرة نفسها. بالإضافة إلى ذلك، يتم تشغيل الشاشة وإيقافها بناءً على كيفية عمل النظام (بعد 5 ثوانٍ بدون تفاعل من المستخدم أو أثناء تحريك رأسك باستمرار).

ومع ذلك، يمكن للبطاقات المباشرة الوصول إلى العديد من الميزات نفسها التي ينفذها الغطس، مثل أداة الاستشعار أو بيانات نظام تحديد المواقع العالمي (GPS). لا يزال بإمكانك إنشاء تجارب مقنعة مع السماح للمستخدمين بالبقاء في تجربة الجدول الزمني لإجراء أشياءً مثل التحقق من الرسائل.

البنية

تتطلب البطاقات المباشرة سياقًا طويلاً للحصول عليها طوال الوقت كانت مرئية، لذا عليك إدارتها في خدمة تُشغَّل في الخلفية.

ويمكنك بعد ذلك نشر بطاقة مباشرة وعرضها عندما تصبح الخدمة متاحة بدء التشغيل أو استجابةً لأحداث أخرى تراقبها الخدمة. يمكنك عرض البطاقات المباشرة بمعدّل تكرار منخفض (مرة كل بضع ثوانٍ). أو معدّل تكرار مرتفع (لعدد المرات التي يمكن فيها تحديث النظام).

عندما تصبح البطاقة المنشورة غير ذات صلة، عليك إتلاف الخدمة لإيقاف العرض.

العرض منخفض التكرار

يقتصر العرض بتردد منخفض على مجموعة صغيرة من أجهزة Android يمكنهم فقط تحديث الشاشة مرة واحدة كل بضع ثوانٍ.

إنها طريقة بسيطة لإنشاء بطاقات مباشرة باستخدام محتوى بسيط لا يتطلب عرضًا ثابتًا أو تحديثات متكررة.

العرض بمعدّل تكرار مرتفع

يتيح لك العرض عالي التكرار استخدام المزيد من الخيارات. المتوفرة في إطار عمل رسومات Android.

يمنحك النظام السطح الخلفي الفعلي بطاقة مباشرة ترسم عليها مباشرةً باستخدام العرض الثنائي الأبعاد أو التنسيقات أو التنسيقات أو حتى الرسومات الثلاثية الأبعاد المعقدة باستخدام OpenGL.

 

إنشاء بطاقات مباشرة منخفضة التردد

يتطلب العرض ذو التكرار المنخفض واجهة مستخدم توفرها RemoteViews الذي يتوافق مع المجموعة الفرعية التالية من تنسيقات Android وطرق العرض:

استخدام العرض بمعدل تكرار منخفض في الحالات التالية:

  • ما عليك سوى استخدام واجهات برمجة التطبيقات القياسية لـ Android View التي تم إدراجها سابقًا.
  • أنت تحتاج فقط إلى تحديثات غير متكررة نسبيًا (تتراوح بضع ثوانٍ بين تحديثًا).

تنبيه:

  • يجب أن تحتوي البطاقات المباشرة دائمًا على 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 × 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 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 لمزيد من المعلومات.