Живые карточки появляются в текущем разделе временной шкалы и отображают информацию, актуальную в текущий момент.
Живые карточки отлично подходят, когда пользователи активно участвуют в выполнении задачи, но хотят периодически проверять Glass на наличие дополнительной информации. Например, проверять время пробежки каждые несколько минут или управлять музыкальным проигрывателем, когда они хотят пропустить песню или приостановить ее.
Если вы впервые разрабатываете для Glass, сначала прочтите руководство по текущим задачам . В этом документе рассказывается, как создать полноценную стеклянную посуду с живой картой, следуя нашим лучшим практикам проектирования.
Как они работают
Живые карты позволяют картам оставаться в текущем разделе временной шкалы до тех пор, пока они актуальны. В отличие от статических карточек, живые карточки не сохраняются на временной шкале, и пользователи явно удаляют их после того, как закончили с ними работать.
Пользователи обычно запускают живые карты, произнося голосовую команду в главном меню, которая запускает фоновую службу, отображающую карту. Затем они могут нажать на карточку, чтобы отобразить пункты меню, которые могут действовать на карточку, например удалить ее со временной шкалы.
Когда их использовать
Живые карты предназначены для текущих задач, к которым пользователи могут часто подключаться и выходить из них, например, дисплей, показывающий текущий статус действия, анимированная карта во время навигации или музыкальный проигрыватель.
Еще одним преимуществом живых карточек является то, что они хорошо подходят для пользовательских интерфейсов, требующих взаимодействия с пользователями в реальном времени и обновлений пользовательского интерфейса в реальном времени.
При использовании живых карточек временная шкала по-прежнему контролирует взаимодействие с пользователем, поэтому смахивание вперед или назад по живой карточке позволяет перемещаться по временной шкале, а не воздействовать на саму живую карточку. Кроме того, экран включается и выключается в зависимости от поведения системы (через 5 секунд без взаимодействия с пользователем или во время движения головы вверх).
Однако живые карты имеют доступ ко многим из тех же функций, что и погружение , например, к данным датчиков или GPS. Это позволяет вам по-прежнему создавать привлекательные впечатления, позволяя пользователям оставаться на временной шкале, чтобы делать другие вещи, например проверять сообщения.
Архитектура
Живым картам требуется длительный контекст, чтобы владеть ими в течение всего времени, пока они видимы, поэтому управляйте ими в фоновом режиме.
Затем вы можете опубликовать и отобразить живую карточку сразу после запуска службы или в ответ на другие события, отслеживаемые службой. Вы можете отображать живые карты с низкой частотой (раз в несколько секунд) или с высокой частотой (столько раз, сколько система может обновить).
Когда действующая карта перестанет быть актуальной, уничтожьте службу, чтобы прекратить ее предоставление.
Низкочастотный рендеринг
Низкочастотный рендеринг ограничен небольшим набором представлений Android и может обновлять изображение только раз в несколько секунд.
Это простой способ создавать живые открытки с простым содержанием, не требующим постоянного рендеринга или частых обновлений.
Высокочастотный рендеринг
Высокочастотный рендеринг позволяет использовать больше возможностей, доступных в графической платформе Android.
Система предоставляет вам реальную подложку живой карты, на которой вы рисуете непосредственно, используя 2D-виды и макеты или даже сложную 3D-графику с OpenGL.
Создание низкочастотных живых карт
Для низкочастотного рендеринга требуется пользовательский интерфейс, предоставляемый объектом RemoteViews , который поддерживает следующее подмножество макетов и представлений Android:
Используйте низкочастотный рендеринг, когда:
- Вам потребуются только стандартные API-интерфейсы представления Android, перечисленные ранее.
- Вам требуются только относительно редкие обновления (несколько секунд между обновлениями).
Иметь в виду:
- Живые карты всегда должны иметь
PendingIntent
, объявленный с помощьюsetAction()
для временной шкалы для публикации карты. - Чтобы внести изменения в карту после публикации, перед повторной публикацией вызовите
setViews()
для карты с обновленным объектом RemoteViews .
Чтобы создать низкочастотные живые карты:
Создайте макет или представление, которое вы хотите визуализировать. В следующем примере показан макет воображаемой баскетбольной игры:
<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" />
Создайте сервис, который управляет живой карточкой и отображает ваш макет или представление. В этом примере служба обновляет счет воображаемого баскетбольного матча каждые 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 и логикой рисования:
Создайте класс, реализующий
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); } } } }
Установите экземпляр
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
Создайте класс, реализующий
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(); } }
Создайте службу, которая управляет живой карточкой и устанавливает класс
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
, чтобы правильно создавать, отображать и закрывать меню в вашей активности меню:
-
onCreateOptionsMenu()
раздувает ресурс XML-меню. -
onAttachedToWindow()
показывает меню, когда действие находится в фокусе. -
onPrepareOptionsMenu()
показывает или скрывает пункты меню, если это необходимо. Например, вы можете отображать разные пункты меню в зависимости от того, что делают пользователи. Например, вы можете отображать разные пункты меню на основе некоторых контекстных данных. -
onOptionsItemSelected()
обрабатывает выбор пользователя. -
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, сделайте действие меню полупрозрачным, чтобы живая карточка все еще была видна под меню:
Создайте файл
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>
В файле
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
Поддержка контекстных голосовых команд
Укажите, что ваша
MenuActivity
поддерживает контекстные голосовые команды :// Initialize your LiveCard as usual. mLiveCard.setVoiceActionEnabled(true); mLiveCard.publish(LiveCard.PublishMode.REVEAL); // or SILENT
Измените свою
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
для получения дополнительной информации.