יום חמישי, דצמבר 2

Options Menu, Context Menu


נטפל ביצירת תפריטים ונדגים בניית תפריטים בעזרת אפליקציה קטנה ומעניינת.
אנדרואיד מספק שני סוגים של תפריטים:
  1.  options menu - תפריט ראשי שמופיע עם לחיצה על כפתור menu.
  2. context menu - תפריט המוצמד לאלמנט view, למשל ListView, ומפעילים אותו עם לחיצה ארוכה על אותו אלמנט.

לצורך המחשה, הנה תמונות המציגות את ה-options menu וה- context menu שמחובר ל ListView.

תמונה 1: Option Menu

  • על הצג הנ"ל מופיעים 5 פריטים בנוסף לכפתור ה-More. 
  • כשיש בתפריט יותר מ-6 פריטים, לחיצה על More תרחיב את התצוגה ותציג רק את הפריטים שלא נכנסו לתצוגה הראשית.
  • המספר המקסימלי של פריטים שיוצגו בלי צורך בהרחבה ע"י ה-More  הוא תלוי מכשיר.
  • לכל פריט אפשר להצמיד טקסט - ראה English או Hebrew למעלה.
  • אפשר להצמיד icon לכל פריט - ראה מפלצות למעלה.
  • ה-icons לא יופיעו בתצוגה המורחבת.
תמונה 2:  Context Menu



ה-context menu הנ"ל הופעל  ע"י לחיצה ארוכה על פריט באלמנט ה-view שאליו הוא "מוצמד". במקרה שלנו מדובר ב-ListView.

מה מבצעת הדוגמא שלנו:


הדוגמא מציגה רשימת שמות של חיות (ListView), ומאפשרת ביצוע פעולות דרך תפריטים.
  • ע"י ה-Options menu: בחירה בין תצוגת שמות החיות בעברית או באנגלית.
  • ע"י ה-Context menu: שליחת שם החיה ל-TTS: Text To Speech. כבעזרת התפריט אפשר לבחור בין הגיה של דובר צרפתית (French Speaker)לבין דובר אנגלית.


מימוש ה-TTS:
ה-TTS ממומש כ-class נפרד ששמו-  MyTts. לא נעמיק בו כאן, היות שהוא מאד דומה למימוש ה-TTS בו דנו בפוסט של ה-ListView.
 נתמקד ב-class השני, שמממש את ה-menus ושמו MyMenuXml. הוא subclass של MyTts הנ"ל, כך שהתמיכה ב-TTS מובנית ויכולה להיות שקופה עבורינו: super.onCreat יגרום לבניית מערכת ה-TTS.

נרד למימוש התפריטים. את המבנה של שני סוגי התפריטים הנ"ל אפשר להגדיר  עם קובץ XML או בתוך קוד הג'אווה. בשלב ראשון נציג את ה-xml.  ואחכ" נראה את האלטרנטיבה. על היתרונות בשימוש ב- xml לצורך אפיון אלמנטים ב-UI כבר דנו בכמה הזדמנויות קודמות (דומה להפרדה בין התוכנית לעיצוב).

שלבי העבודה בהכנת ה-option menu וה-context menu מאד דומים. מדובר בסה"כ בשלושה שלבים עיקריים:
  1. שלב 1: הגדרת המבנה עם קובץ xml. אם לא משתמשים ב-xml - עבור לשלב 2.
  2. שלב 2: "פריסת" קובץ ה-xml אותו בנינו בשלב 1 (inflating). מימוש המתודה onCreateOptionsMenu או onCreateContextMenu, בהתאם לסוג התפריט.  מתודה זו מופעלת פעם אחת בלבד - כשלוחצים על כפתור ה-menu בפעם הראשונה\כשמציגים את ה-context menu בפעם הראשונה.
  3. שלב 3: מימוש המתודה  שמופעלת עם בחירת אלמנט בתפריט. onOptionsItemSelected או onContextItemSelected בהתאם לסוג התפריט.
בנסוף ל-3 הפעולות הנ"ל, נזכיר את registerForContextMenu שמחברת את ה-context menu ל-view ואותה נראה מיד.
נזכיר גם את (unregisterForContextMenu(View view שעושה את התהליך ההפוך ומנתקת את ה-context menu ובה לא נשתמש בשלב זה.

 מתודות נוספות הקשורות לתפריטים, בהן לא ניגע בדוגמא שלנו:

  • onContextMenuClosed או onOptionsMenuClosed המופעלות עם סגירת חלון התפריט. הסגירה יכולה להיות תוצאה של חזרה אחורה או לחיצה לבחירת פריט בתפריט.
  • onPrepareOptionsMenu נקראת בכל פעם כשלוחצים על כפתור ה-menu, כלומר לפני הצגת התפריט (בנידוד ל onCreateMenu שמופעלת רק בפעם הראשונה. זו הזדמנות לשנות\לעדכן את התפריט.
  • openContextMenu או openOptionsMenu- הצגת התפריט ע"י פקודת ג'אווה.
נרד עוד קצת וניגש לקבצים:
נציג את התוכנה בהתאם לשלושת השלבים שתוארו לעי"ל.
הפעם נציג בנפרד את קטעי התוכנית שמטפלים ב-options menu וב-context menu, למרות שהם כאמור דורשים פעולות כמעט זהות. נתחיל עם ה-options, ובהמשך נציג את ה-context menu.
מימוש ה-Options Menu
נזכיר שהתפריט מציג 8 כפתורים לבחירת שפת התצוגה של רשימת החיות.
נתחיל בשלב 1: קובץ ה-xml. את הקובץ יש לשמור ב-/res/menu. שמו: my_options_menu.xml והנה כל תוכנו:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <menu xmlns:android="http://schemas.android.com/apk/res/android">
  3.     <item android:id="@+id/english1"
  4.           android:icon="@drawable/my_monster1"
  5.           android:title="@string/options_menu_english" />
  6.              <item android:id="@+id/english2"
  7.           android:icon="@drawable/my_monster1"
  8.           android:title="@string/options_menu_english" />
  9.              <item android:id="@+id/english3"
  10.           android:icon="@drawable/my_monster1"
  11.           android:title="@string/options_menu_english" />
  12.              <item android:id="@+id/english4"
  13.           android:icon="@drawable/my_monster1"
  14.           android:title="@string/options_menu_english" />
  15.           
  16.     <item android:id="@+id/hebrew1"
  17.           android:icon="@drawable/my_monster2"
  18.           android:title="@string/options_menu_hebrew" />
  19.    <item android:id="@+id/hebrew2"
  20.           android:icon="@drawable/my_monster2"
  21.           android:title="@string/options_menu_hebrew" />
  22.    <item android:id="@+id/hebrew3"
  23.           android:icon="@drawable/my_monster2"
  24.           android:title="@string/options_menu_hebrew" />
  25.    <item android:id="@+id/hebrew4"
  26.           android:icon="@drawable/my_monster2"
  27.           android:title="@string/options_menu_hebrew" />
  28.    <item android:id="@+id/hebrew5"
  29.           android:icon="@drawable/my_monster2"
  30.           android:title="@string/options_menu_hebrew" />
  31. </menu>
מדובר ב-8 פריטי תפריט. נסתכל על הפריט האחרון לדוגמא (שורות 28-30):
שורה 28 מגדירה את ה-id של הפריט. בעזרת ה-id התוכנית תוכל לשלוף את הפריט דרך class R.
שורה 29 מגדירה את ה-icon שיוצמד לפריט (לא חובה).
שורה 30 מגדירה את ה-text שיוצמד אליו. הטקסט עצמו נמצא בקובץ strings.
כל החבילה עטופה בתג menu. 
שלב 2: פריסת ה-xml. 

  1.     @Override
  2.     public boolean onCreateOptionsMenu(Menu menu) {
  3.         MenuInflater inflater = getMenuInflater();
  4.         inflater.inflate(R.menu.my_options_menu, menu);
  5.         return(super.onCreateOptionsMenu(menu));
  6.     }
הפעולה מתבצעת כאמור בתוך ה-callback שמופעל רק בלחיצה הראשונה על כפתור ה-menu.
שורה 3 יוצרת אובייקט מסוג MenuInflater. אובייקט זה תומך במתודה בה נשתמש בשורה הבאה.
שורה 4 מפעילה את המתודה inflate הפורסת את הקובץ my_options_menu.xml.
שורה 5 מפעילה את המתודה של ה-super class - חובה קרוא לה.

שלב 3: selection callback
למעשה הכל כבר מוכן. כעת נציג את ה-callback שמשוגר עם בחירת פריט בתפריט.

   
  1.     @Override
  2.     public boolean onOptionsItemSelected(MenuItem item) {
  3.         super.onOptionsItemSelected(item);
  4.         switch (item.getItemId()) {
  5.             case R.id.english1:
  6.             case R.id.english2:
  7.             case R.id.english3:
  8.             case R.id.english4:
  9.                 listView.setAdapter(arrayAdapter);
  10.                 return(true);
  11.         
  12.             case R.id.hebrew1:
  13.             case R.id.hebrew2:
  14.             case R.id.hebrew3:
  15.             case R.id.hebrew4:
  16.             case R.id.hebrew5:
  17.                 listView.setAdapter(arrayAdapterHeb);
  18.                 return(true);
  19.         }
  20.         return(false);
  21.     }
שורה 3 : קריאה ל-callback של ה-super class.
שורה 4: switch עפ"י ה-id של הפריט שנבחר. 4 הפריטים הראשונים (שורות 5-8), יבחרו תצוגה באנגלית. 4 הפריטים האחרונים (שורות 12-16), יבחרו תצוגה באנגלית.
סידור התפריט באנגלית או בעברית מתבצעה בשורות 9  ו- 17 בהתאמה. 
שני מתאמים ArrayApters הוכנו מראש. אחד מחובר לרשימת החיות בעברית ואחד לרשימה באנגלית. נשוב בסוף לדון ב-ListView. 
סיימנו את הדיון על ה-Options Menu. כרגיל באנדרואיד - זה לא ממש מסובך.

מימוש ה- ContextMenu
כאן נזכיר שהתפריט מציג שני פריטים: 
English Speaker ו-French Speaker. בהתאם לבחירה יופעל ה-TTS המתאים.

שלב 1: הגדרת מבנה התפריט בקובץ ה-xml:
הקובץ לפניכם:


  1.  <?xml version="1.0" encoding="utf-8"?>
  2. <menu xmlns:android="http://schemas.android.com/apk/res/android">
  3.     <item android:id="@+id/english"
  4.           android:icon="@drawable/my_monster1"
  5.           android:title="@string/context_menu_english" />
  6.     <item android:id="@+id/french"
  7.           android:icon="@drawable/my_monster2"
  8.           android:title="@string/context_menu_french" />        
  9. </menu>

 ל- Context menu  שני פריטים בלבד. נסתכל על הראשון - שורות 3-5:
שורה 3: ה-id.
שורה 4: ה-icon - לא נתמך ע"י תפריט ה-context לכן זה מיותר.
שורה 5: מייצג את הטקסט על הפריט, מכוון לקובץ strings.xml בו נמצא הכיתוב עצמו.


שלב 2: פריסת ה-xml בג'אווה.

  1.         @Override
  2.     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
  3.             MenuInflater inflater = getMenuInflater();
  4.             inflater.inflate(R.menu.my_list_context_menu, menu);
  5.         super.onCreateContextMenu(menu, v, menuInfo);
  6.         return;
  7.     }
קובץ ה-xml שלנו הוא my_list_context_menu.xml - שורה 4. מעבר לזה, הפעולות זהות לאלה ששלב 2 של ה-Options Menu.

שלב 3: ה-selection callback:
  1.     @Override
  2.     public boolean onContextItemSelected(MenuItem item) {
  3.         super.onContextItemSelected(item);
  4.         AdapterView.AdapterContextMenuInfo menuInfo;
  5.    menuInfo =(AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
  6.         int index = menuInfo.position;
  7.         switch (item.getItemId()) {
  8.             case R.id.english:
  9.                 speakEng( getResources().getStringArray(R.array.Animals)[index]);
  10.             return(true);
  11.             case R.id.french:
  12.                 speakFr( getResources().getStringArray(R.array.Animals)[index]
  13.             return(true);
  14.         }
  15.         return(false);
  16.     }
  17. }


שלב 1: שורות 4-6 נועדו למצוא את ה-index של הפריט שנבחר בתוך ה-ListView. בעזרת ה-index נמצא את שם החיה אותו נשלח ל-TTS. מציאת ה-index נעשית ע"י מציאת המיקום של ה-item של ה-Context menu. משתמשים במתודה
שורה 5: המתודה getMenuInfo מביאה את האינפורמציה על ה-view (במקרה שלנו ListView), שמחוברת לפריט ה-menu שנבחר. בין השאר, האינפורמציה כוללת את המיקום על ה-view.
שורה 6 : המתודה position שולפת את נתון המיקום מתוך האינפורמציה שמביא ה-getMenuInfo. בעזרת האינדקס הזה כאמור נשלוף את שם החיה ממערך השמות.
שורות 7-11: switch על ה-id של הפריט שנבחר ב-context menu. יש שם בסה"כ שני פריטים:
 english (שורה 8) ו-french (שורה 11).
עבור שני המקרים נשלחת המילה מתוך הרשימה ל- TTS ע"י המתודה speakEng או speakFr עבור הבחירה של אנגלית או צרפתית בהתאמה. הטקסט שנשלח ל-TTS נשלף מתוך מערך השמות Animal שנמצא בקובץ arrays.xml. הנה שורה 9 (דומה לשורה 12):
speakEng( getResources().getStringArray(R.array.Animals)[index]);

את getResources פגשנו מספר פעמים בעבר - כולל בפוסט של ה-spinner. דרכה אפשר להגיעה לכל ה-resources כולל arrays, drawables וכו. במקרה שלפנינו שלפנו את האיבר מתוך Animals המוצבע ע"י index.

בזאת הקפנו את כל הפעילות הקשורה ב- context menu.

 הכנת ה-ListView
הדוגמא נועדה להדגים שימוש ב-menus, וסקרנו את הקוד שנגע בנושא.
נעיף עוד מבט על ה-onCreate שם יוצרים את רשימת החיות שממומשת ע"י ListView. ראינו כבר יצירה של ListView  בפוסטים קודמים, בכ"ז הנה המתודה האחרונה אותה עדיין לא כיסינו. ועוד עניין קטן בנוסף לבניית ה-ListView-הזכרנו את הצורך לחבר בין ה-context menu ל-view עם המתודה registerForContextMenu - נראה את זה כאן ב-onCreate:


  1.     @Override
  2.     public void onCreate(Bundle icicle) {
  3.         super.onCreate(icicle);
  4.         setContentView(R.layout.main);
  5.         listView = (ListView)findViewById(R.id.list);
  6.         arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, getResources().getStringArray(R.array.Animals));
  7.         arrayAdapterHeb = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, getResources().getStringArray(R.array.Animals1));
  8.         listView.setAdapter(arrayAdapterHeb);
  9.         registerForContextMenu(listView);
  10.     }

בשורה 6 ובשורה 7 נוצרים שני  ListAdapters. הראשון מחבר בין  simple_list_item_1 (זה כזכור ListView סטנדרטי שהוכן מראש ע"י אנדרואיד) לבין הרשימה Animals בקובץ arrays.xml.
ה-Adapter השני,  arrayAdapterHeb, שנוצר בשורה 7, מחבר את רשימת החיות בעברית - Animals1.
צרפתי את רשימת החיות בעברית. הרשימה באנגלית בנויה באופן דומה ונמצאת בקובץ arrays.xml.
שם קובץ: arraysh.xml בספריית layout:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="Animals1">             
        <item>כלב</item>             
        <item>חתול</item>             
        <item>דג</item>             
        <item>חמור</item>
        <item>נמר</item>             
        <item>אריה</item>
        <item>זברה</item>             
        <item>דולפין</item>             
        <item>פרפר</item>
        <item>סוס</item>             
    </array>   
</resources>
 
שורה 8 מחברת את ה-arrayAdapterHeb ל-ListView, אבל בעזרת תפריט ה options menu שלנו אפשר לקבוע אם ה-Adapter העברי או האנגלי יהיו פעילים - ראה onOptionsItemSelected למעלה.
את שורה 9 המחברת את ה context menu ל-view כבר הזכרנו.


איפיון ה-Menu ללא XML

אפשר לבצע את איפיון מבנה ה-menu בתוך הג'אווה ולוותר על הגדרת ה-menu בקובץ xml. צריך בסה"כ להחליף מתודה אחת (*): 
  • onCreateOptionsMenu עבור Option Menu
  • onCreateContextMenu עבור Context Menu
(*): בנוסף להחלפת המתודה, אין צורך למשוך את ה-id של פריטי התפריט מתוך R class, אלא הוא ניתן כפרמטר ב-API - ראה הסבר בהמשך.
לפניכם שתי המתודות הנ"ל למימוש הדוגמא אותה תארנו, ללא xml:

  1.      @Override
  2.     public boolean onCreateOptionsMenu(Menu menu) {
  3.         menu.add(Menu.NONE, ENGLISH1, Menu.NONE, "English1");
  4.         menu.add(Menu.NONE, HEBREW1, Menu.NONE, "Hebrew1");
  5.         menu.add(Menu.NONE, ENGLISH2, Menu.NONE, "English2");
  6.         menu.add(Menu.NONE, HEBREW2, Menu.NONE, "Hebrew2");
  7.         menu.add(Menu.NONE, ENGLISH3, Menu.NONE, "English3");
  8.         menu.add(Menu.NONE, HEBREW3, Menu.NONE, "Hebrew3");
  9.         menu.add(Menu.NONE, ENGLISH4, Menu.NONE, "English4");
  10.         return(super.onCreateOptionsMenu(menu));
  11.     }
שורות 3-9 -המתודה מוסיפה פריטים לתפריט. add מקבלת 4 פרמטרים:
  1. Group Id:מאפשר חיבור של מספר פריטים בתפריט לקבוצה, עליה אפשר לבצע פעולות במשותף: להסתיר או להראות את כל הקבוצה.
  2. Item Id: זה המציין בו נשתמש לזהוי הפריט במקום ה-id שניתן ב-xml.
  3. Order: מקומו של הפריט ברשימה. NONE אם לא משנה.
  4. title text: הטקסט שיופיע על הפריט.

ובאופן דומה, בניית ה-menu עבור ה-context menu:

  1.         @Override
  2.     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
  3.         menu.add(Menu.NONE, ENGLISH, Menu.NONE, "ENGLISH");
  4.         menu.add(Menu.NONE, FRENCH, Menu.NONE, "FRENCH");
  5.         super.onCreateContextMenu(menu, v, menuInfo);
  6.         return;
  7.     }








7 תגובות:

  1. לקח לי זמן עד שהצלחתי , וזה גם נתן לי כמה דגשים.
    כמו כן יש טעות קטנה , התקייה שצריך לפתוח זה לא meu אלה menu , כמו כן כאשר רוצים לכתוב פריט android:title="@string/context_menu_english"
    אז context_menu_english צריך להיות מוגדר בתוך strings.xml (כמובן שגם את ניתוב התמונות צריך להגדיר)

    השבמחק
  2. הי לאוניד, מצוין שהצלחת. שגיאת הכתיב תוקנה - תודה. לגבי ה-string@, נכון, זה הסימון שמצביע על קובץ string. האופציה השניה, כתיבה בתוך ה-layout כך:
    text="text in layout file
    "

    השבמחק
  3. היי רונן
    אשמח אם תעדכן את הלינק לקבצים
    תודה

    השבמחק
  4. הערה: שמגדירים את קובץ ה- menu.xml חשוב להגדיר אותו בתיקיית ה-layout.
    ומצד שני, את קובץ ה-array.xml עדיף להגדיר ב- res ולא ב- layout גם
    מכיוון שהוא מכיל מקור, וגם כי זה יוצר יותר סדר ונכונות לוגית.
    חוץ מזה מדריך נפלא, ובכלל כל הבלוג הזה ראוי להערכה!. המשך כך, לי
    אישית תורם המון!.

    השבמחק
  5. רונן תודה רבה מאוד מאוד עוזר.
    שאלה - איך מוסיפים כפתור settings למכשירים החדשים יותר שאין להם כפתור options? אני חושב שהאפליקציות מחויבות באיזה settings bar כזה לא?
    תודה!

    השבמחק
  6. שאלה בקשר לשפות שונות ב TTS. אני עברתי על הקוד שעשית בשביל צרפתית ואנגלית והוספתי איטלקית וגרמנית. באמולטור זה עבד מעולה, אבל כשניסיתי להעלות את האפליקציה על האנדרואיד שלי, הוא דיבר רק באנגלית גם אם בחרתי צרפתית, איטלקית או גרמנית. אתה יודע אולי למה זה קרה, או איך לתקן את זה?
    תודה!

    השבמחק