SurfaceView ו-GLSurfaceView

ממשק המשתמש של מסגרת האפליקציות של Android מבוסס על היררכיה של אובייקטים שמתחילים ב-View. כל רכיבי ממשק המשתמש עוברים סדרה של מדידות ותהליך פריסה שמתאימים אותם לאזור מלבני. לאחר מכן, כל אובייקטי התצוגה הגלויים עוברים עיבוד (render) על גבי משטח שהוגדר על ידי WindowManager כשהאפליקציה הועברה לחזית. חוט ממשק המשתמש של האפליקציה מבצע פריסה ורינדור למאגר לכל פריים.

SurfaceView

SurfaceView הוא רכיב שאפשר להשתמש בו כדי להטמיע שכבה מורכבת נוספת בהיררכיית התצוגה. ל-SurfaceView יש את אותם פרמטרים של פריסה כמו לתצוגות אחרות, כך שאפשר לבצע בו שינויים כמו בכל תצוגה אחרת, אבל התוכן של SurfaceView שקוף.

כשמבצעים רינדור באמצעות מקור מאגר חיצוני, כמו הקשר GL או מקודד מדיה, צריך להעתיק מאגרים ממקור המאגר כדי להציג את המאגרים במסך. אפשר לעשות זאת באמצעות SurfaceView.

כשרכיב התצוגה של SurfaceView עומד להיות גלוי, המסגרת מבקשת מ-SurfaceControl לבקש מ-SurfaceFlinger משטח חדש. כדי לקבל קריאות חזרה כשהמשטח נוצר או נהרס, משתמשים בממשק SurfaceHolder. כברירת מחדל, המשטח החדש שנוצר ממוקם מאחורי המשטח של ממשק המשתמש של האפליקציה. אפשר לשנות את סדר Z שמוגדר כברירת מחדל כדי להציב את המשטח החדש בחלק העליון.

עיבוד באמצעות SurfaceView מועיל במקרים שבהם צריך לבצע עיבוד למשטח נפרד, למשל כשמבצעים עיבוד באמצעות Camera API או הקשר של OpenGL ES. כשמבצעים רינדור באמצעות SurfaceView, ‏SurfaceFlinger יוצר מאגרים ישירות למסך. בלי SurfaceView, צריך לשלב מאגרים במשטח מחוץ למסך, ולאחר מכן לשלב את המאגרים במסך. לכן, עיבוד באמצעות SurfaceView מבטל את הצורך בעבודה נוספת. אחרי היצירה באמצעות SurfaceView, משתמשים בשרשור של ממשק המשתמש כדי לתאם עם מחזור החיים של הפעילות ולבצע שינויים בגודל או במיקום של התצוגה לפי הצורך. לאחר מכן, ה-Hardware Composer משלב את ממשק המשתמש של האפליקציה עם השכבות האחרות.

ה-surface החדש הוא הצד של היוצר ב-BufferQueue, שהצרכן שלו הוא שכבת SurfaceFlinger. אפשר לעדכן את ה-surface באמצעות כל מנגנון שיכול להזין BufferQueue, כמו פונקציות Canvas שסופקו על ידי ה-surface, צירוף EGLSurface ורישום על ה-surface באמצעות GLES, או הגדרת מקודד מדיה לכתיבה על ה-surface.

SurfaceView ומחזור החיים של הפעילות

כשמשתמשים ב-SurfaceView, צריך ליצור את הרינדור של פני השטח משרשור שאינו שרשור ה-UI הראשי.

בפעילות עם SurfaceView יש שתי מכונות מצב נפרדות אבל תלויות זו בזו:

  • אפליקציה onCreate/onResume/onPause
  • יצירה, שינוי או השמדה של משטח

כשהפעילות מתחילה, אתם מקבלים קריאות חזרה בסדר הזה:

  1. onCreate()
  2. onResume()
  3. surfaceCreated()
  4. surfaceChanged()

אם תלחצו על 'הקודם', תוצג לכם:

  1. onPause()
  2. surfaceDestroyed() (הקריאה מתבצעת ממש לפני שהמשטח ייעלם)

אם תסובבו את המסך, הפעילות תפורק ותיווצר מחדש, וכך תקבלו את המחזור המלא. כדי לבדוק אם מדובר בהפעלה מחדש מהירה, בודקים את הערך של isFinishing(). אפשר להתחיל או להפסיק פעילות במהירות כה רבה, כך ש-surfaceCreated() מתרחש אחרי onPause().

אם מקישים על לחצן ההפעלה כדי להכהות את המסך, מופיע רק onPause() בלי surfaceDestroyed(). פני השטח יישארו פעילים והעיבוד יוכל להמשיך. אם תמשיכו לבקש אירועים של כוריאוגרפים, תוכלו להמשיך לקבל אותם. אם מסך הנעילה מאלץ כיוון אחר, יכול להיות שהפעילות תופעל מחדש כשהמסך יוסר. אחרת, תוכלו לצאת מהמסך הריק עם אותה פני שטח כמו קודם.

משך החיים של השרשור יכול להיות קשור למשטח או לפעילות, בהתאם למה שאתם רוצים שיקרה כשהמסך יהיה ריק. אפשר להפעיל או להפסיק את הפעילות בהתחלה או בסיום של הפעילות, או ביצירה או בהרס של פני השטח.

הפעלה/עצירה של הפעילות מפעילה/מפסיקת את הפעילות של השרשור, ומתואמת עם מחזור החיים של האפליקציה. מתחילים את שרשור המרינר ב-onResume() ומפסיקים אותו ב-onStop(). כשיוצרים ומגדירים את השרשור, לפעמים הממשק כבר קיים ולפעמים לא (לדוגמה, הוא עדיין פעיל אחרי שמפעילים את המסך באמצעות לחצן ההפעלה). צריך להמתין עד שהמשטח נוצר כדי לבצע אתחול בשרשור. אי אפשר לבצע את האיפוס בקריאה החוזרת (callback) של surfaceCreate() כי היא לא תופעל שוב אם לא נוצר מחדש המשטח. במקום זאת, שולחים שאילתה לגבי מצב פני השטח או שומרים אותו במטמון, ומעבירים אותו לשרשור של ה-renderer.

כדאי להפעיל או להפסיק את ה-thread בזמן היצירה או ההרס של ה-surface, כי ה-surface וה-renderer קשורים זה לזה באופן לוגי. אתם מתחילים את השרשור אחרי יצירת הממשק, וכך מונעים בעיות מסוימות בתקשורת בין השרשורים. הודעות שנוצרו או השתנו בממשק מועברות פשוט. כדי לוודא שהעיבוד יפסק כשהמסך יהיה ריק וימשיך כשהמסך יהיה מלא, צריך להורות ל-Choreographer להפסיק להפעיל את הפונקציה החוזרת (callback) לציור המסגרת. onResume() ממשיכה את קריאות החזרה (callbacks) אם חוט ה-renderer פועל. עם זאת, אם אתם יוצרים אנימציה על סמך הזמן שחלף בין הפריימים, יכול להיות שיהיה פער גדול לפני שהאירוע הבא יגיע. שימוש בהודעה מפורשת להשהיה/להמשך יכול לפתור את הבעיה הזו.

בשתי האפשרויות, בין שהמשך החיים של השרשור קשור ל-Activity ובין שהוא קשור למשטח, ההתמקדות היא באופן שבו השרשור של ה-Renderer מוגדר ובשאלה אם הוא פועל. בעיה קשורה היא חילוץ המצב מהשרשור כשהפעילות מפסיקה לפעול (ב-onStop() או ב-onSaveInstanceState()). במקרים כאלה, הפתרון הטוב ביותר הוא לקשר את משך החיים של השרשור לפעילות, כי אחרי שהצטרפתם לשרשור ה-renderer, תוכלו לגשת למצב של השרשור שעבר רינדור בלי פרימיטיבים של סנכרון.

GLSurfaceView

הכיתה GLSurfaceView מספקת כיתות עזר לניהול הקשרים של EGL, לתקשורת בין חוטים ולאינטראקציה עם מחזור החיים של הפעילות. אין צורך להשתמש ב-GLSurfaceView כדי להשתמש ב-GLES.

לדוגמה, GLSurfaceView יוצר שרשור לעיבוד וידאו ומגדיר בו הקשר EGL. המצב ינוקה באופן אוטומטי כשהפעילות תושהה. ברוב האפליקציות אין צורך לדעת דבר על EGL כדי להשתמש ב-GLES עם GLSurfaceView.

ברוב המקרים, GLSurfaceView יכול להקל על העבודה עם GLES. במצבים מסוימים, הוא עלול להפריע.