Karty na żywo są widoczne w bieżącej sekcji osi czasu i zawierają informacje, które są aktualne w danym momencie.
Karty na żywo przydają się, gdy użytkownicy są aktywnie zaangażowani w działanie, ale chcą okresowo sprawdzać Google Glass w celu uzyskania dodatkowych informacji. Dotyczy to na przykład sprawdzania ich czasu co kilka minut biegu albo sterowania odtwarzaczem, aby pomijał lub wstrzymał utwór.
Jeśli po raz pierwszy tworzysz aplikacje dla Glass, przeczytaj najpierw przewodnik dotyczący bieżących zadań. Dokument ten opisuje, jak zrobić kompletne oprogramowanie Glass z aktywną kartą zgodnie z naszymi sprawdzonymi metodami projektowania.
Jak działają
Dzięki nim karty pozostają w bieżącej sekcji osi czasu, dopóki są istotne. W przeciwieństwie do kart statycznych karty aktywne nie są zachowywane na osi czasu, a użytkownicy muszą je jawnie usuwać po zakończeniu pracy.
Użytkownicy zwykle zaczynają korzystać z karty w tle, wypowiadając polecenie głosowe z menu głównego. Spowoduje to uruchomienie w tle usługi renderowania karty. Następnie może ją kliknąć, aby wyświetlić pozycje menu, które można wykonać na karcie, np. zamknąć ją z osi czasu.
Kiedy ich używać
Karty transmisji na żywo są zaprojektowane z myślą o trwających zadaniach, z których użytkownicy mogą często wchodzić i wychodzić. Dotyczy to na przykład wyświetlania z aktywnym stanem działania, animowanej mapy podczas nawigacji czy odtwarzacza muzyki.
Inną zaletą aktywnych kart jest to, że dobrze sprawdzają się w interfejsach, które wymagają interakcji z użytkownikami w czasie rzeczywistym i aktualizacji interfejsu w czasie rzeczywistym.
Gdy korzystasz z aktywnych kart, oś czasu nadal ma wpływ na wrażenia użytkownika. Przesuwanie palcem do przodu lub do tyłu na aktywnej karcie powoduje jej poruszanie się po niej, a nie na samej karcie. Ekran włącza się i wyłącza w zależności od zachowania systemu (po 5 sekundach bez interakcji z użytkownikiem lub po otrzymaniu ponaglenia).
Aktywne karty zapewniają jednak dostęp do wielu tych samych funkcji co zagłębienia, np. do czujnika czy danych GPS. Pozwoli to na zapewnienie użytkownikom atrakcyjnych wrażeń, a jednocześnie umożliwi użytkownikom pozostanie w osi czasu i wykonywanie innych czynności, takich jak sprawdzanie wiadomości.
Architektura
Karty na żywo wymagają długotrwałego kontekstu, aby były widoczne przez cały czas, gdy są widoczne. Dlatego zarządzaj nimi w usłudze w tle.
Następnie możesz opublikować i wyrenderować aktywną kartę zaraz po uruchomieniu usługi lub w odpowiedzi na inne zdarzenia monitorowane przez tę usługę. Możesz renderować aktywne karty z małą częstotliwością (raz na kilka sekund) lub dużą częstotliwością (maksymalnie tyle razy, ile system może odświeżyć).
Gdy aktywna karta nie jest już aktualna, zniszcz usługę, aby zatrzymać renderowanie.
Renderowanie o niskiej częstotliwości
Renderowanie z niską częstotliwością jest ograniczone do niewielkiego zestawu widoków na Androidzie i może aktualizować się tylko co kilka sekund.
To prosty sposób na tworzenie kart z prostymi treściami, które nie wymagają ciągłego renderowania ani częstych aktualizacji.
Renderowanie o wysokiej częstotliwości
Renderowanie o wysokiej częstotliwości pozwala korzystać z większej liczby opcji dostępnych w ramach platformy graficznej Androida.
System daje dostęp do rzeczywistej powierzchni tylnej karty transmisji na żywo, na którą rysujesz bezpośrednio za pomocą widoków i układów 2D, a nawet skomplikowanej grafiki 3D w trybie OpenGL.
Tworzenie kart na żywo o niskiej częstotliwości
Renderowanie o niskiej częstotliwości wymaga interfejsu dostarczonego przez obiekt RemoteViews, który obsługuje ten podzbiór układów i widoków Androida:
Użyj renderowania o niskiej częstotliwości, gdy:
- Potrzebujesz tylko wymienionych wcześniej standardowych interfejsów API widoku danych na Androida.
- Wymagają one stosunkowo rzadkich aktualizacji (co kilka sekund między odświeżeniami).
Pamiętaj:
- Aby można było opublikować kartę, aktywne karty muszą zawsze mieć zadeklarowaną wartość
PendingIntent
zsetAction()
. - Aby wprowadzić zmiany w karcie po jej opublikowaniu, przed jej ponownym opublikowaniem wywołaj
setViews()
ze zaktualizowanym obiektem RemoteViews.
Aby utworzyć karty na żywo o niskiej częstotliwości:
Utwórz układ lub widok, który chcesz renderować. Poniżej znajduje się przykład układu wymyślonego meczu koszykówki:
<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" />
Utwórz usługę, która zarządza opublikowaną kartą i renderuje Twój układ lub widok. Ta przykładowa usługa aktualizuje wynik wymyślonego meczu koszykówki co 30 sekund.
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; } }
Tworzenie kart na żywo o wysokiej częstotliwości
Renderowanie o wysokiej częstotliwości umożliwia rysowanie bezpośrednio na powierzchni aktywnej karty.
Użyj renderowania o wysokiej częstotliwości, gdy:
- Musisz często aktualizować aktywną kartę (wiele razy na sekundę).
- Potrzebujesz elastyczności w zakresie renderowania. Renderowanie o wysokiej częstotliwości pozwala korzystać z widoków i układów Androida do złożonej grafiki OpenGL.
Pamiętaj:
- Zawsze należy tworzyć usługę w tle do renderowania na powierzchni aktywnej karty.
- Aktywne karty muszą zawsze mieć parametr
PendingIntent
zadeklarowany za pomocąsetAction()
. - Użyj
GLRenderer
, jeśli renderujesz interfejs OpenGL iDirectRenderingCallback
we wszystkich innych przypadkach.
Korzystanie z DirectRenderingCallback
Aby tworzyć karty na żywo ze standardowymi widokami i rysunkami na Androidzie:
Utwórz klasę, która implementuje funkcję
DirectRenderingCallback
. Implementacja wywołań zwrotnych w tych interfejsach pozwala wykonywać działania podczas ważnych zdarzeń w cyklu życia powierzchni karty aktywnej.W poniższym przykładzie tworzymy wątek w tle, który będzie okresowo renderowany, ale możesz aktualizować kartę w odpowiedzi na zdarzenia zewnętrzne (na przykład aktualizacje czujnika lub lokalizacji).
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); } } } }
Ustaw instancję
DirectRenderingCallback
jako wywołanie zwrotneLiveCard
SurfaceHolder
. Dzięki temu aktywna karta będzie wiedzieć, jakiej logiki użyć do renderowania.// 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; } }
Korzystanie z trybu OpenGL
Utwórz klasę, która wykorzystuje funkcję
GlRenderer
. Implementacja wywołań zwrotnych w tym interfejsie umożliwia wykonywanie działań podczas ważnych zdarzeń w cyklu życia powierzchni karty aktywnej. W tym przykładzie narysowano kolorową, obracającą się sześcian.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(); } }
Utwórz usługę, która zarządza opublikowaną kartą i ustawia klasę
CubeRenderer
jako mechanizm renderowania aktywnej karty.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;
}
}
Wyświetlanie aktywnej karty
Po opublikowaniu aktywnej karty za pomocą LiveCard.publish()
przekazujesz jej parametr, który określa, czy ma ona od razu fokus.
Aby oś czasu przeskoczyła na kartę tuż po opublikowaniu, użyj funkcji LiveCard.PublishMode.REVEAL
.
Aby opublikować kartę bez powiadomienia i umożliwić użytkownikom samodzielne przechodzenie do niej, skorzystaj z LiveCard.PublishMode.SILENT
.
Dodatkowo metoda LiveCard.navigate()
umożliwia przechodzenie do karty po jej opublikowaniu. Jeśli np. użytkownicy próbują uruchomić kartę transmisji na żywo z głównego menu głosowego, która już się rozpoczęła, możesz przejść do aktywnej karty za pomocą tej metody.
Tworzenie i wyświetlanie menu
Karty na żywo nie mogą wyświetlać własnego systemu menu, więc musisz utworzyć aktywność, aby wyświetlić menu aktywnej karty.
Działanie w menu może wtedy zawierać pozycje pozwalające zatrzymać aktywną kartę, rozpocząć odtwarzanie lub dowolne inne działanie, które chcesz wykonać. Możesz też dodać działania związane z ustawieniami systemu, np. regulacja głośności, jako pozycję menu. Więcej informacji znajdziesz w sekcji Ustawienia początkowe.
Tworzę zasoby menu
Tworzenie zasobów menu przebiega tak samo jak na platformie Android, ale w przypadku Google Glass przestrzegaj tych wytycznych:
- Do każdej pozycji menu dodaj ikonę elementu menu o wymiarach 50 × 50 pikseli. Ikona menu musi być biała na przezroczystym tle. Zobacz ikony pozycji menu w Google Glass, by zobaczyć przykłady lub pobrać je na własny użytek.
- Użyj krótkiej nazwy, która opisuje działanie, jak w tytule. Najlepiej działają czasowniki imperatywne (np. Udostępnij lub Odpowiedz wszystkim).
- Google Glass nie wyświetla aktywnych kart bez pozycji menu. Udostępnij przynajmniej element menu Zatrzymaj, aby użytkownicy mogli usunąć aktywną kartę z osi czasu.
Widżet CheckBox (pole wyboru) nie jest obsługiwany.
<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>
Tworzę aktywność do obsługi wywołań zwrotnych menu
Musisz zdefiniować aktywność menu, która będzie wywoływana, gdy użytkownik ją kliknie.
Aby prawidłowo tworzyć, wyświetlać i odrzucać menu w aktywności związanej z menu, zastąp te metody wywołania zwrotnego Activity
:
onCreateOptionsMenu()
zwiększa zasób menu XML.onAttachedToWindow()
pokazuje menu, gdy aktywność jest aktywna.onPrepareOptionsMenu()
pokazuje lub ukrywa pozycje menu, jeśli jest to konieczne. Możesz na przykład wyświetlać różne pozycje menu w zależności od tego, co robią użytkownicy. Możesz na przykład wyświetlać różne pozycje menu na podstawie danych kontekstowych.onOptionsItemSelected()
obsługuje wybór użytkowników.onOptionsMenuClosed()
, aby zakończyć aktywność, tak by nie pojawiała się już na aktywnej karcie.
Musisz tu zakończyć aktywność, aby została prawidłowo zakończona, gdy menu zostanie zamknięte przez wybór lub przesunięcie palcem w dół.
/**
* 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();
}
}
Zapewnienie przejrzystości działań w menu
Jeśli chcesz zachować spójność ze stylem Glass, wybierz przezroczystość menu, dzięki czemu aktywna karta będzie nadal widoczna pod menu:
Utwórz plik
res/values/styles.xml
i zadeklaruj styl, który sprawi, że tło aktywności będzie przezroczyste:<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>
W pliku
AndroidManifest.xml
przypisz motyw do działania w menu:<?xml version="1.0" encoding="utf-8"?> <manifest ... > ... <application ... > ... <activity android:name=".MenuActivity" android:theme="@style/MenuTheme" ...> </activity> </application> </manifest>
Wyświetlanie menu
Podaj PendingIntent
dla działania karty za pomocą setAction()
. Oczekująca intencja służy do uruchamiania aktywności w menu, gdy użytkownik kliknie kartę:
Intent menuIntent = new Intent(this, MenuActivity.class);
mLiveCard.setAction(PendingIntent.getActivity(this, 0, menuIntent, 0));
mLiveCard.publish(LiveCard.PublishMode.REVEAL); // or SILENT
Obsługa kontekstowych poleceń głosowych
Określ, czy
MenuActivity
obsługuje kontekstowe polecenia głosowe:// Initialize your LiveCard as usual. mLiveCard.setVoiceActionEnabled(true); mLiveCard.publish(LiveCard.PublishMode.REVEAL); // or SILENT
Zmodyfikuj
MenuActivity
, aby obsługiwały wywołania głosowe:/** * 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; } }
Więcej informacji znajdziesz w przewodniku kontekstowych poleceń głosowych.
Narzędzia menu
Dostępnych jest kilka metod pomocniczych, które pozwalają zmodyfikować wygląd i działanie menu. Więcej informacji znajdziesz na stronie MenuUtils
.