יצירת רכיבים של תצוגה בהתאמה אישית

אפשר לנסות את הדרך של כתיבת הודעה
‫Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשק משתמש ב-Android. איך עובדים עם פריסות בכתיבת אימייל

‫Android מציעה מודל מתוחכם ועוצמתי מבוסס-רכיבים לבניית ממשק המשתמש, שמבוסס על מחלקות הפריסה הבסיסיות View ו-ViewGroup. הפלטפורמה כוללת מגוון של מחלקות משנה מוכנות מראש View וViewGroup – שנקראות ווידג'טים ופריסות, בהתאמה – שבהן אפשר להשתמש כדי לבנות את ממשק המשתמש.

רשימה חלקית של הווידג'טים הזמינים כוללת את Button,‏ TextView,‏ EditText,‏ ListView,‏ CheckBox,‏ RadioButton,‏ Gallery,‏ Spinner, ואת הווידג'טים המיוחדים יותר AutoCompleteTextView,‏ ImageSwitcher ו-TextSwitcher.

בין הפריסות הזמינות אפשר למצוא את LinearLayout, FrameLayout, RelativeLayout, ועוד. דוגמאות נוספות מופיעות במאמר בנושא פריסות נפוצות.

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

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

  • אפשר ליצור סוג View בהתאמה אישית מלאה – לדוגמה, ידית של "שליטה בעוצמת הקול", שמוצגת באמצעות גרפיקה דו-ממדית ונראית כמו אמצעי בקרה אלקטרוני אנלוגי.
  • אפשר לשלב קבוצה של View רכיבים לרכיב יחיד חדש, למשל כדי ליצור תיבת בחירה משולבת (שילוב של רשימה קופצת ושדה טקסט חופשי), אמצעי בקרה לבחירה עם שני חלונות (חלון ימני וחלון שמאלי עם רשימה בכל אחד מהם, שבהם אפשר להקצות מחדש את הפריט לרשימה אחרת) וכן הלאה.
  • אתם יכולים לשנות את אופן העיבוד של רכיב EditText במסך. אפליקציית הדוגמה NotePad משתמשת בזה בצורה טובה כדי ליצור דף מחברת עם שורות.
  • אפשר לתעד אירועים אחרים – כמו הקשות על מקשים – ולטפל בהם בצורה מותאמת אישית, למשל במשחק.

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

הגישה הבסיסית

הנה סקירה כללית של מה שצריך לדעת כדי ליצור רכיבים משלכם:View

  1. להרחיב מחלקה או מחלקת משנה קיימת View באמצעות מחלקה משלכם.
  2. ביטול חלק מהשיטות ממחלקת העל. השיטות של מחלקת האב שניתן לשנות מתחילות ב-on – לדוגמה, onDraw(),‏ onMeasure() ו-onKeyDown(). זה דומה לאירועים on ב-Activity או ב-ListActivity שאתם מבטלים כדי להשתמש ב-lifecycle ובפונקציות אחרות.
  3. משתמשים במחלקת התוסף החדשה. אחרי שהפעולה תושלם, תוכלו להשתמש במחלקת התוסף החדשה במקום בתצוגה שהיא התבססה עליה.

רכיבים בהתאמה אישית מלאה

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

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

כדי ליצור רכיב בהתאמה אישית מלאה, כדאי לשקול את האפשרויות הבאות:

  • התצוגה הכי גנרית שאפשר להרחיב היא View, ולכן בדרך כלל מתחילים בהרחבה שלה כדי ליצור את רכיב הסופר החדש.
  • אתם יכולים לספק constructor שיכול לקבל מאפיינים ופרמטרים מ-XML, ואתם יכולים להשתמש במאפיינים ובפרמטרים כאלה משלכם, כמו הצבע והטווח של מד עוצמת הקול או הרוחב והשיכוך של המחט.
  • סביר להניח שתרצו ליצור גם מאזינים לאירועים, פונקציות גישה למאפיינים ופונקציות לשינוי מאפיינים, וגם התנהגות מתוחכמת יותר במחלקת הרכיבים.
  • כמעט בטוח שתרצו לשנות את onMeasure(), וכנראה שתצטרכו גם לשנות את onDraw() אם אתם רוצים שהרכיב יציג משהו. לשתי האפשרויות יש התנהגות שמוגדרת כברירת מחדל, אבל ברירת המחדל של onDraw() לא עושה כלום, וברירת המחדל של onMeasure() תמיד מגדירה גודל של 100x100, שסביר להניח שלא תרצו.
  • אפשר גם לבטל שיטות אחרות של on, לפי הצורך.

הרחבה של onDraw()‎ ו-onMeasure()‎

השיטה onDraw() מחזירה Canvas שאפשר להטמיע בו כל מה שרוצים: גרפיקה דו-ממדית, רכיבים סטנדרטיים או מותאמים אישית אחרים, טקסט עם סגנון או כל דבר אחר שעולה על הדעת.

התהליך ב-onMeasure() קצת יותר מורכב. ‫onMeasure() הוא חלק קריטי בחוזה העיבוד בין הרכיב לבין הקונטיינר שלו. צריך לבטל את ההגדרה של onMeasure() כדי לדווח ביעילות ובדייקנות על המדידות של החלקים שהוא מכיל. הדרישות של ההורה לגבי המגבלות – שמועברות ל-method‏ onMeasure() – והדרישה לקרוא ל-method‏ setMeasuredDimension() עם הרוחב והגובה שנמדדו אחרי החישוב, הופכות את התהליך למורכב יותר. אם לא קוראים לשיטה הזו משיטת onMeasure() שהוחלפה, מתקבלת חריגה בזמן המדידה.

באופן כללי, הטמעה של onMeasure() נראית כך:

  • השיטה onMeasure() שמוגדרת כברירת מחדל נקראת עם מפרטים של רוחב וגובה, שמתייחסים אליהם כאל דרישות להגבלות על המידות של הרוחב והגובה שאתם יוצרים. הפרמטרים widthMeasureSpec ו-heightMeasureSpec הם קודים של מספרים שלמים שמייצגים מאפיינים. במאמרי העזרה בנושא View.onMeasure(int, int) אפשר למצוא הפניה מלאה לסוגי ההגבלות שנדרשות במפרטים האלה. במאמרי העזרה האלה מוסבר גם תהליך המדידה כולו.
  • השיטה onMeasure() של הרכיב מחשבת את הרוחב והגובה של המדידה, שנדרשים כדי להציג את הרכיב. הוא צריך לנסות לעמוד במפרטים שהועברו, אבל הוא יכול לחרוג מהם. במקרה כזה, ההורה יכול לבחור מה לעשות, כולל חיתוך, גלילה, הפעלת חריגה או בקשה מ-onMeasure() לנסות שוב, אולי עם מפרטי מדידה שונים.
  • אחרי שמחשבים את הרוחב והגובה, קוראים לשיטה setMeasuredDimension(int width, int height) עם המידות המחושבות. אם לא עושים את זה, מתקבלת חריגה.

ריכזנו כאן סיכום של שיטות סטנדרטיות אחרות שהמסגרת מפעילה בתצוגות:

קטגוריה שיטות תיאור
יצירה יצרנים יש צורה של בנאי שנקראת כשהתצוגה נוצרת מקוד, וצורה שנקראת כשהתצוגה מורחבת מקובץ פריסה. הטופס השני מנתח ומחיל מאפיינים שמוגדרים בקובץ הפריסה.
onFinishInflate() הפונקציה נקראת אחרי שמוצגת תצוגה וכל רכיבי הצאצא שלה מורחבים מ-XML.
פריסה onMeasure(int, int) הפונקציה נקראת כדי לקבוע את דרישות הגודל של התצוגה הזו ושל כל הצאצאים שלה.
onLayout(boolean, int, int, int, int) הפונקציה הזו נקראת כשהתצוגה הזו צריכה להקצות גודל ומיקום לכל רכיבי הצאצא שלה.
onSizeChanged(int, int, int, int) מופעלת כשהגודל של התצוגה הזו משתנה.
שרטוט onDraw(Canvas) הפונקציה נקראת כשהתצוגה צריכה לעבד את התוכן שלה.
עיבוד אירועים onKeyDown(int, KeyEvent) מופעל כשמתרחש אירוע של לחיצה על מקש.
onKeyUp(int, KeyEvent) מופעל כשמתרחש אירוע של שחרור מקש.
onTrackballEvent(MotionEvent) מופעל כשמתרחש אירוע תנועה של כדור עקיבה.
onTouchEvent(MotionEvent) מופעל כשמתרחש אירוע תנועה במסך מגע.
מיקוד onFocusChanged(boolean, int, Rect) הפונקציה נקראת כשהתצוגה מקבלת או מאבדת את הפוקוס.
onWindowFocusChanged(boolean) הפונקציה מופעלת כשהחלון שמכיל את התצוגה מקבל או מאבד את המיקוד.
צירוף onAttachedToWindow() הקריאה מתבצעת כשהתצוגה מצורפת לחלון.
onDetachedFromWindow() הפונקציה נקראת כשהתצוגה מנותקת מהחלון שלה.
onWindowVisibilityChanged(int) מופעלת כשהרשאות הגישה של החלון שמכיל את התצוגה משתנות.

פקדים מורכבים

אם אתם לא רוצים ליצור רכיב בהתאמה אישית מלאה, אלא ליצור רכיב לשימוש חוזר שמורכב מקבוצה של אמצעי בקרה קיימים, כדאי ליצור רכיב מורכב (או אמצעי בקרה מורכב). לסיכום, ההגדרה הזו מאגדת מספר אמצעי בקרה או תצוגות יותר בסיסיים לקבוצה לוגית של פריטים שאפשר להתייחס אליהם כאל דבר אחד. לדוגמה, תיבה משולבת יכולה להיות שילוב של שדה EditText בשורה אחת ולחצן סמוך עם רשימה קופצת מצורפת. אם המשתמש מקיש על הלחצן ובוחר משהו מהרשימה, השדה EditText מתמלא, אבל הוא יכול גם להקליד משהו ישירות בשדה EditText אם הוא מעדיף.

ב-Android, יש עוד שתי תצוגות שזמינות בקלות כדי לעשות את זה: Spinner וAutoCompleteTextView. בכל מקרה, המושג הזה של תיבה משולבת הוא דוגמה טובה.

כדי ליצור רכיב מורכב:

  • בדומה ל-Activity, אפשר להשתמש בגישה ההצהרתית (מבוססת-XML) כדי ליצור את הרכיבים הכלולים, או להטמיע אותם באופן פרוגרמטי מהקוד. נקודת ההתחלה הרגילה היא Layout כלשהו, ולכן צריך ליצור מחלקה שמרחיבה Layout. במקרה של תיבת בחירה משולבת, אפשר להשתמש ב-LinearLayout עם כיוון אופקי. אפשר להוסיף פריסות אחרות, כך שהרכיב המורכב יכול להיות מורכב ומובנה באופן שרירותי.
  • ב-constructor של המחלקה החדשה, לוקחים את כל הפרמטרים שהמחלקה העליונה מצפה להם ומעבירים אותם קודם ל-constructor של המחלקה העליונה. לאחר מכן, תוכלו להגדיר את התצוגות האחרות לשימוש ברכיב החדש. כאן יוצרים את השדה EditText ואת רשימת החלונות הקופצים. יכול להיות שתציגו מאפיינים ופרמטרים משלכם ב-XML שהבונה יכול לשלוף ולהשתמש בהם.
  • אפשר גם ליצור פונקציות listener לאירועים שהתצוגות שכלולות עשויות ליצור. דוגמה: שיטת listener ל-listener של לחיצה על פריט ברשימה, לעדכון התוכן של EditText אם בוחרים פריט ברשימה.
  • אפשר גם ליצור מאפיינים משלכם באמצעות פונקציות גישה ופונקציות שינוי. לדוגמה, אפשר להגדיר את הערך EditText בהתחלה ברכיב ולשאול על התוכן שלו כשצריך.
  • אפשר גם לשנות את הערכים של onDraw() ושל onMeasure(). בדרך כלל אין צורך בכך כשמרחיבים Layout, כי לפריסה יש התנהגות ברירת מחדל שסביר להניח שתפעל בצורה תקינה.
  • אפשר גם לבטל את ההגדרה של שיטות אחרות של on, כמו onKeyDown(), למשל כדי לבחור ערכי ברירת מחדל מסוימים מתוך הרשימה הקופצת של תיבת משולבת כשמקישים על מקש מסוים.

יש כמה יתרונות לשימוש ב-Layout כבסיס לבקרה מותאמת אישית, כולל:

  • אפשר לציין את הפריסה באמצעות קובצי XML הצהרתיים, כמו במסך פעילות, או ליצור תצוגות באופן פרוגרמטי ולהוסיף אותן לפריסה מהקוד.
  • השיטות onDraw() ו-onMeasure(), ורוב השיטות האחרות on, מתנהגות בצורה מתאימה, כך שלא צריך לבטל את ברירת המחדל שלהן.
  • אפשר ליצור במהירות תצוגות מורכבות באופן שרירותי ולעשות בהן שימוש חוזר כאילו הן רכיב יחיד.

שינוי סוג תצוגה קיים

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

לדוגמה, אפליקציית הדוגמה NotePad מדגימה היבטים רבים של השימוש בפלטפורמת Android. אחת מהן היא הרחבת תצוגה של EditText כדי ליצור פנקס עם שורות. זו לא דוגמה מושלמת, ויכול להיות שממשקי ה-API לביצוע הפעולה הזו ישתנו, אבל היא ממחישה את העקרונות.

אם עדיין לא עשיתם את זה, מייבאים את דוגמת NotePad ל-Android Studio או מעיינים במקור באמצעות הקישור שמופיע למעלה. כדאי לעיין בהגדרה של LinedEditText בקובץ NoteEditor.java.

הנה כמה דברים שכדאי לשים לב אליהם בקובץ הזה:

  1. ההגדרה

    הכיתה מוגדרת בשורה הבאה:
    public static class LinedEditText extends EditText

    LinedEditText מוגדר ככיתה פנימית בפעילות NoteEditor, אבל הוא ציבורי ולכן אפשר לגשת אליו כ-NoteEditor.LinedEditText מחוץ לכיתה NoteEditor.

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

    LinedEditText הוא הרחבה של EditText, שהיא התצוגה שצריך להתאים אישית במקרה הזה. אחרי שמסיימים, הכיתה החדשה יכולה להחליף את התצוגה הרגילה של EditText הכיתה.

  2. הפעלת הכיתה

    כמו תמיד, קודם קוראים למחלקת הבסיס. זה לא בנאי ברירת מחדל, אבל זה בנאי עם פרמטרים. ה-EditText נוצר עם הפרמטרים האלה כשהוא מורחב מקובץ פריסה בפורמט XML. לכן, הפונקציה ליצירת אובייקט צריכה לקבל אותם ולהעביר אותם גם לפונקציה ליצירת אובייקט של מחלקת העל.

  3. שיטות שבוטלו

    בדוגמה הזו, השיטה onDraw() מוחלפת רק בשיטה אחרת, אבל יכול להיות שתצטרכו להחליף שיטות אחרות כשתיצרו רכיבים מותאמים אישית משלכם.

    בדוגמה הזו, שינוי של שיטת onDraw() מאפשר לצבוע את הקווים הכחולים בבד הציור של התצוגה EditText. הקנבס מועבר לשיטה onDraw() שהוחלפה. ה-method super.onDraw() נקראת לפני שה-method מסתיימת. חובה להפעיל את ה-method של מחלקת העל. במקרה כזה, מפעילים אותו בסוף אחרי שצובעים את הקווים שרוצים לכלול.

  4. רכיב בהתאמה אישית

    עכשיו יש לכם רכיב מותאם אישית, אבל איך אפשר להשתמש בו? בדוגמה של NotePad, הרכיב בהתאמה אישית משמש ישירות מהפריסה הדקלרטיבית, ולכן צריך לעיין בקובץ note_editor.xml בתיקייה res/layout:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />

    הרכיב המותאם אישית נוצר כתצוגה גנרית ב-XML, והמחלקה מצוינת באמצעות החבילה המלאה. המחלקות הפנימיות שאתם מגדירים מופנות באמצעות הסימון NoteEditor$LinedEditText, שהוא דרך סטנדרטית להפניה למחלקות פנימיות בשפת התכנות Java.

    אם רכיב התצוגה המותאם אישית לא מוגדר כמחלקה פנימית, אפשר להצהיר על רכיב התצוגה באמצעות שם אלמנט ה-XML ולהחריג את המאפיין class. לדוגמה:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />

    שימו לב שהמחלקה LinedEditText היא עכשיו קובץ מחלקה נפרד. כשמכניסים את הכיתה NoteEditor לכיתה אחרת, הטכניקה הזו לא עובדת.

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

יצירת רכיבים בהתאמה אישית היא לא מסובכת יותר ממה שצריך.

רכיב מתוחכם יותר יכול לבטל עוד יותר on שיטות ולהציג שיטות עזר משלו, ובכך להתאים אישית את המאפיינים וההתנהגות שלו באופן משמעותי. המגבלה היחידה היא הדמיון שלכם והדרישות שלכם מהרכיב.