라이브 카드

실시간 카드는 타임라인의 현재 섹션에 표시되고 현재 시점의 관련 정보를 표시합니다.

라이브 카드는 사용자가 작업에 적극적으로 참여하고 있지만 추가 정보를 정기적으로 Glass에서 확인하고자 할 때 유용합니다. 예를 들어 몇 분마다 실행되는 시간을 확인하거나 노래를 건너뛰거나 일시중지하고 싶을 때 음악 플레이어를 제어할 수 있습니다.

Glass용으로 처음 개발하는 경우 먼저 진행 중인 작업 가이드를 읽어보세요. 이 문서에서는 Google의 디자인 권장사항에 따라 실시간 카드로 완전한 Glassware를 빌드하는 방법을 설명합니다.

작동 방식

실시간 카드는 관련된 현재 기간 동안 카드를 타임라인의 현재 섹션에서 유지할 수 있는 방법을 제공합니다. 정적 카드와 달리 실시간 카드는 타임라인에 유지되지 않으며 사용자는 카드를 완료한 후 명시적으로 제거합니다.

사용자는 일반적으로 기본 메뉴에서 음성 명령을 말하여 실시간 카드를 시작합니다. 기본 메뉴에서는 카드를 렌더링하는 백그라운드 서비스를 시작합니다. 그런 다음 카드를 탭하여 타임라인에 카드를 닫는 등 카드에 작업을 할 수 있는 메뉴 항목을 표시할 수 있습니다.

용도

라이브 카드는 실행 중인 상태를 보여주는 디스플레이, 탐색 중 애니메이션 지도 또는 음악 플레이어와 같이 사용자가 자주 드나드는 지속적인 작업을 위해 설계되었습니다.

라이브 카드의 또 다른 이점은 사용자와 실시간으로 상호작용하고 UI를 실시간으로 업데이트해야 하는 UI에 적합하다는 것입니다.

라이브 카드를 사용하는 경우 타임라인에서 여전히 사용자 환경을 제어할 수 있으므로 라이브 카드에서 앞뒤로 스와이프하면 라이브 카드 자체에서 작동하는 대신 타임라인을 탐색합니다. 또한 시스템의 작동 방식에 따라 화면이 켜지거나 꺼집니다 (사용자 상호작용 없이 5초 후 또는 헤드 업 시에는).

하지만 라이브 카드는 센서 또는 GPS 데이터와 같이 몰입과 동일한 여러 기능에 액세스할 수 있습니다. 이를 통해 사용자는 타임라인 환경을 유지하면서 메시지 확인과 같은 다른 작업을 수행할 수 있도록 하면서 매력적인 환경을 만들 수 있습니다.

아키텍처

라이브 카드가 표시되는 전체 기간 동안 소유하려면 장기 실행 컨텍스트가 필요하므로 백그라운드 서비스에서 카드를 관리합니다.

그러면 서비스가 시작되자마자 또는 서비스가 모니터링하는 다른 이벤트에 대한 응답으로 라이브 카드를 게시하고 렌더링할 수 있습니다. 라이브 카드를 저주파수 (몇 초에 한 번) 또는 고주파수 (시스템이 새로고침할 수 있는 최대 횟수)로 렌더링할 수 있습니다.

라이브 카드가 더 이상 관련이 없으면 서비스를 제거하여 렌더링을 중지합니다.

저주파 렌더링

저주파 렌더링은 소수의 Android 뷰로 제한되며 몇 초마다 한 번만 디스플레이를 업데이트할 수 있습니다.

지속적인 렌더링이나 빈번한 업데이트가 필요 없는 간단한 콘텐츠로 라이브 카드를 만드는 간단한 방법입니다.

고주파수 렌더링

고주파수 렌더링을 통해 Android 그래픽 프레임워크에서 사용할 수 있는 옵션을 더 많이 사용할 수 있습니다.

시스템은 2D 뷰와 레이아웃 또는 OpenGL의 복잡한 3D 그래픽을 사용하여 직접 그리는 라이브 카드의 실제 백업 표면을 제공합니다.

 

빈도가 낮은 실시간 카드 만들기

저주파 렌더링에는 RemoteViews 객체에서 제공하는 UI가 필요하며, 다음과 같은 Android 레이아웃 및 뷰 하위 집합을 지원합니다.

다음과 같은 경우 저주파 렌더링을 사용합니다.

  • 이전에 나열된 표준 Android 뷰 API만 필요합니다.
  • 비교적 드물게 업데이트 (새로고침 사이에 몇 초)만 필요합니다.

주의사항

  • 실시간 카드에는 항상 카드를 게시하기 위해 setAction()로 선언된 PendingIntent가 있어야 합니다.
  • 게시 후 카드를 변경하려면 다시 게시하기 전에 업데이트된 RemoteViews 객체로 카드에서 setViews()를 호출합니다.

빈도가 낮은 실시간 카드를 만드는 방법은 다음과 같습니다.

  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 그래픽을 표시할 수 있습니다.

주의사항

  • 항상 라이브 카드 표면에서 렌더링할 백그라운드 서비스를 만들어야 합니다.
  • 라이브 카드에는 항상 setAction()로 선언된 PendingIntent가 있어야 합니다.
  • OpenGL을 렌더링한다면 GLRenderer를 사용하고 다른 모든 경우에는 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 메뉴 항목 아이콘을 참고하거나 직접 다운로드하여 사용할 수 있습니다.
  • 작업을 설명하고 제목 대소문자에 해당하는 짧은 이름을 사용합니다. 명령형 동사는 효과적입니다 (예: 공유 또는 전체 답장).
  • 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>
    

메뉴 표시

setAction()를 사용하여 카드 작업의 PendingIntent를 제공합니다. 대기 중인 인텐트는 사용자가 카드를 탭할 때 메뉴 활동을 시작하는 데 사용됩니다.

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을 참조하세요.