การ์ดสดจะปรากฏในส่วนปัจจุบันของไทม์ไลน์และแสดงข้อมูลที่เกี่ยวข้อง ณ เวลาปัจจุบัน
การ์ดเรียลไทม์เหมาะที่จะใช้เมื่อผู้ใช้มีส่วนร่วมในงานหนึ่งๆ อยู่แล้วแต่ต้องการดูข้อมูลเพิ่มเติมเป็นระยะๆ ใน Glass เช่น ตรวจสอบเวลาในการวิ่งทุกๆ 2-3 นาที หรือควบคุมโปรแกรมเล่นเพลงเมื่อต้องการข้ามหรือหยุดเพลงชั่วคราว
หากนี่เป็นครั้งแรกที่คุณพัฒนา Glass โปรดอ่านคู่มืองานต่อเนื่องก่อน เอกสารนั้นพูดถึงวิธีสร้าง Glasslasware ที่สมบูรณ์ด้วยการ์ดเรียลไทม์ตามแนวทางปฏิบัติแนะนําในการออกแบบของเรา
วิธีการทำงาน
การ์ดแบบสดช่วยให้วิธีแสดงการ์ดในส่วนปัจจุบันของไทม์ไลน์ตราบใดที่การ์ดเหล่านั้นมีความเกี่ยวข้อง การ์ดสดแตกต่างจากการ์ดแบบคงที่ตรงที่ไทม์ไลน์จะหายไป และระบบจะนําการ์ดออกอย่างชัดเจนหลังจากดําเนินการเสร็จแล้ว
โดยทั่วไปแล้ว ผู้ใช้จะเริ่มใช้การ์ดสดด้วยการพูดคําสั่งเสียงในเมนูหลัก ซึ่งจะเริ่มบริการในเบื้องหลังที่แสดงการ์ด จากนั้นแตะการ์ดเพื่อแสดงรายการในเมนูที่สามารถดําเนินการบนการ์ดได้ เช่น ปิดไปจากไทม์ไลน์
ใช้เมื่อใด
การ์ดสดออกแบบมาเพื่อรองรับงานต่อเนื่องที่ผู้ใช้กระโดดเข้าและออกบ่อยๆ ได้ เช่น จอแสดงผลที่แสดงสถานะการทํางานของการดําเนินการ แผนที่แบบเคลื่อนไหวระหว่างการนําทาง หรือโปรแกรมเล่นเพลง
ประโยชน์อีกอย่างของการ์ดเรียลไทม์คือการ์ดเหมาะสําหรับ UI ที่จําเป็นต้องมีการโต้ตอบแบบเรียลไทม์กับผู้ใช้ และการอัปเดต UI แบบเรียลไทม์
เมื่อใช้การ์ดเรียลไทม์ ไทม์ไลน์ยังคงควบคุมประสบการณ์ของผู้ใช้ได้ ดังนั้นการเลื่อนไปข้างหน้าหรือย้อนกลับบนการ์ดสดจะเป็นการไปยังส่วนต่างๆ ของไทม์ไลน์แทนการดําเนินการกับการ์ดสดนั้น นอกจากนี้ หน้าจอจะเปิดและปิดตามลักษณะการทํางานของระบบ (หลังจาก 5 วินาทีที่ไม่มีการโต้ตอบของผู้ใช้หรือระหว่างการขยับศีรษะ)
อย่างไรก็ตาม การ์ดเรียลไทม์อาจเข้าถึงฟีเจอร์ต่างๆ ได้หลายอย่างเหมือนกับการแช่ เช่น เซ็นเซอร์หรือข้อมูล GPS การดําเนินการนี้จะช่วยให้คุณยังคงสร้างประสบการณ์ที่น่าสนใจได้ขณะเดียวกันก็ยังช่วยให้ผู้ใช้อยู่ในไทม์ไลน์อื่นๆ เพื่อทําสิ่งต่างๆ ได้ เช่น ตรวจสอบข้อความ
สถาปัตยกรรม
การ์ดสดต้องอาศัยบริบทที่ยาวนานในการเป็นเจ้าของการ์ดตลอดช่วงเวลาที่มองเห็น จึงควรจัดการการ์ดดังกล่าวในบริการในเบื้องหลัง
จากนั้นคุณจะเผยแพร่และแสดงผลการ์ดสดได้ทันทีที่บริการเริ่มหรือเพื่อตอบสนองต่อเหตุการณ์อื่นๆ ที่บริการตรวจสอบ คุณแสดงผลการ์ดเรียลไทม์ได้ความถี่ต่ํา (1 ครั้งทุกๆ 2-3 วินาที) หรือความถี่สูง (สูงสุดเท่าที่ระบบรีเฟรชได้)
เมื่อการ์ดการถ่ายทอดสดไม่เกี่ยวข้องอีกต่อไป ให้ทําลายบริการเพื่อหยุดการแสดงผล
การแสดงผลความถี่ต่ํา
การแสดงผลที่มีความถี่จํากัดจะจํากัดอยู่ที่มุมมอง Android กลุ่มเล็กๆ เท่านั้น และจะอัปเดตการแสดงผลได้ทุกๆ 2-3 วินาทีเท่านั้น
เป็นวิธีง่ายๆ ในการสร้างการ์ดสดที่มีเนื้อหาเรียบง่ายซึ่งไม่จําเป็นต้องแสดงผลอย่างสม่ําเสมอหรือมีการอัปเดตเป็นประจํา
การแสดงผลที่มีความถี่สูง
การแสดงผลที่มีความถี่สูงช่วยให้คุณใช้ตัวเลือกเพิ่มเติม ในเฟรมเวิร์กกราฟิกของ Android ได้
ระบบจะแสดงภาพพื้นหลังจริงของการ์ดสดที่คุณวาดไว้ได้โดยตรงโดยใช้มุมมอง 2 มิติและเลย์เอาต์ หรือแม้แต่กราฟิก 3 มิติที่ซับซ้อนด้วย OpenGL
การสร้างบัตรข้อมูลสดที่มีความถี่ต่ํา
การแสดงผลที่มีความถี่ต่ําต้องใช้ UI ที่ได้รับจากออบเจ็กต์ 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 x 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()
ขยายทรัพยากรเมนู XMLonAttachedToWindow()
แสดงเมนูเมื่อกิจกรรมอยู่ในโฟกัส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 ที่รอดําเนินการจะใช้เพื่อเริ่มกิจกรรมเมนูเมื่อผู้ใช้แตะการ์ด ดังนี้
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; } }
ดูคําแนะนําเพิ่มเติมได้ในคําสั่งเสียงตามบริบท
ยูทิลิตีเมนู
วิธีการช่วยมี 2-3 วิธีพร้อมใช้งานเพื่อแก้ไขรูปลักษณ์และลักษณะการทํางานของเมนู ดูข้อมูลเพิ่มเติมที่MenuUtils