סקירה כללית של מודעות השרטוט

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

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

יש שתי דרכים להגדיר וליצור מופע של Drawable, בנוסף לשימוש בבוני המחלקה:

  • הגדלה של משאב תמונה (קובץ bitmap) שנשמר בפרויקט.
  • ליצור אוביקט תצוגה (inflate) ממשאב XML שמגדיר את מאפייני הפריט הגרפי שניתן להזזה.

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

יצירת משאבים מסוג drawable מתמונות של משאבים

אפשר להוסיף גרפיקה לאפליקציה על ידי הפניה לקובץ תמונה ממקורות הפרויקט. סוגי הקבצים הנתמכים הם PNG (מומלץ),‏ JPG (קביל) ו-GIF (לא מומלץ). הטכניקה הזו מתאימה במיוחד לסמלי אפליקציות, ללוגו ולגרפיקה אחרת, כמו אלה שמשמשים במשחקים.

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

הערה: יכול להיות שמשאבי תמונות שמוצבים בספרייה res/drawable/ יעברו אופטימיזציה אוטומטית באמצעות דחיסת תמונות ללא אובדן נתונים על ידי הכלי aapt במהלך תהליך הבנייה. לדוגמה, אפשר להמיר קובץ PNG בצבע אמיתי שלא דורש יותר מ-256 צבעים לקובץ PNG של 8 ביט עם פלטת צבעים. התוצאה היא תמונה באיכות זהה, אבל היא תופסת פחות זיכרון. כתוצאה מכך, קובצי ה-binary של התמונות שמוצבים בספרייה הזו יכולים להשתנות משך זמן של תהליך build. אם אתם מתכננים לקרוא תמונה כזרם ביטים כדי להמיר אותה למפת סיביות, כדאי להכניס את התמונות לתיקייה res/raw/, שבה הכלי aapt לא משנה אותן.

בקטע הקוד הבא אפשר לראות איך ליצור ImageView שמשתמש בתמונה שנוצרה מפריט גרפי שניתן להזזה ולהוסיף אותה לפריסה:

Kotlin

private lateinit var constraintLayout: ConstraintLayout

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Instantiate an ImageView and define its properties
    val i = ImageView(this).apply {
        setImageResource(R.drawable.my_image)
        contentDescription = resources.getString(R.string.my_image_desc)

        // set the ImageView bounds to match the Drawable's dimensions
        adjustViewBounds = true
        layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT)
    }

    // Create a ConstraintLayout in which to add the ImageView
    constraintLayout = ConstraintLayout(this).apply {

        // Add the ImageView to the layout.
        addView(i)
    }

    // Set the layout as the content view.
    setContentView(constraintLayout)
}

Java

ConstraintLayout constraintLayout;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  // Create a ConstraintLayout in which to add the ImageView
  constraintLayout = new ConstraintLayout(this);

  // Instantiate an ImageView and define its properties
  ImageView i = new ImageView(this);
  i.setImageResource(R.drawable.my_image);
  i.setContentDescription(getResources().getString(R.string.my_image_desc));

  // set the ImageView bounds to match the Drawable's dimensions
  i.setAdjustViewBounds(true);
  i.setLayoutParams(new ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT));

  // Add the ImageView to the layout and set the layout as the content view.
  constraintLayout.addView(i);
  setContentView(constraintLayout);
}

במקרים אחרים, יכול להיות שתרצו לטפל במשאב התמונה כאובייקט Drawable, כמו בדוגמה הבאה:

Kotlin

val myImage: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)

Java

Resources res = context.getResources();
Drawable myImage = ResourcesCompat.getDrawable(res, R.drawable.my_image, null);

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

קטע ה-XML הבא מראה איך מוסיפים פריט גרפי שניתן להזזה לרכיב ImageView בפריסת ה-XML:

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/my_image"
        android:contentDescription="@string/my_image_desc" />

מידע נוסף על שימוש במשאבי פרויקט זמין במאמר משאבים ונכסים.

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

יצירת משאבי drawable מ-XML

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

אחרי שמגדירים את Drawable ב-XML, שומרים את הקובץ בספרייה res/drawable/ של הפרויקט. בדוגמה הבאה מוצג ה-XML שמגדיר משאב TransitionDrawable, שמוגדר בירושה מ-Drawable:

<!-- res/drawable/expand_collapse.xml -->
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/image_expand"/>
    <item android:drawable="@drawable/image_collapse"/>
</transition>

לאחר מכן מאחזרים את האובייקט ויוצרים ממנו מופע על ידי קריאה ל- Resources#getDrawable() והעברת מזהה המשאב של קובץ ה-XML. כל מחלקת משנה של Drawable שתומכת בשיטה inflate() יכולה להיות מוגדרת ב-XML וליצור מופע שלה באפליקציה.

כל מחלקה של drawable שתומכת ב-XML inflation משתמשת במאפייני XML ספציפיים שעוזרים להגדיר את מאפייני האובייקט. הקוד הבא יוצר מופע של TransitionDrawable ומגדיר אותו כתוכן של אובייקט ImageView:

Kotlin

val transition= ResourcesCompat.getDrawable(
        context.resources,
        R.drawable.expand_collapse,
        null
) as TransitionDrawable

val image: ImageView = findViewById(R.id.toggle_image)
image.setImageDrawable(transition)

// Description of the initial state that the drawable represents.
image.contentDescription = resources.getString(R.string.collapsed)

// Then you can call the TransitionDrawable object's methods.
transition.startTransition(1000)

// After the transition is complete, change the image's content description
// to reflect the new state.

Java

Resources res = context.getResources();
TransitionDrawable transition =
    (TransitionDrawable) ResourcesCompat.getDrawable(res, R.drawable.expand_collapse, null);

ImageView image = (ImageView) findViewById(R.id.toggle_image);
image.setImageDrawable(transition);

// Description of the initial state that the drawable represents.
image.setContentDescription(getResources().getString(R.string.collapsed));

// Then you can call the TransitionDrawable object's methods.
transition.startTransition(1000);

// After the transition is complete, change the image's content description
// to reflect the new state.

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

נכסי drawable של צורות

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

ShapeDrawable היא מחלקה משנית של Drawable. לכן, אפשר להשתמש ב-ShapeDrawable בכל מקום שבו מצפים ל-Drawable. לדוגמה, אפשר להשתמש באובייקט ShapeDrawable כדי להגדיר את הרקע של תצוגה על ידי העברתו לשיטה setBackgroundDrawable() של התצוגה. אפשר גם לצייר את הצורה כתצוגה מותאמת אישית משלה ולהוסיף אותה לפריסה באפליקציה.

ל-ShapeDrawable יש method משלה draw(), ולכן אפשר ליצור מחלקת משנה של View שמציירת את האובייקט ShapeDrawable במהלך האירוע onDraw(), כמו בדוגמת הקוד הבאה:

Kotlin

class CustomDrawableView(context: Context) : View(context) {
    private val drawable: ShapeDrawable = run {
        val x = 10
        val y = 10
        val width = 300
        val height = 50
        contentDescription = context.resources.getString(R.string.my_view_desc)

        ShapeDrawable(OvalShape()).apply {
            // If the color isn't set, the shape uses black as the default.
            paint.color = 0xff74AC23.toInt()
            // If the bounds aren't set, the shape can't be drawn.
            setBounds(x, y, x + width, y + height)
        }
    }

    override fun onDraw(canvas: Canvas) {
        drawable.draw(canvas)
    }
}

Java

public class CustomDrawableView extends View {
  private ShapeDrawable drawable;

  public CustomDrawableView(Context context) {
    super(context);

    int x = 10;
    int y = 10;
    int width = 300;
    int height = 50;
    setContentDescription(context.getResources().getString(
            R.string.my_view_desc));

    drawable = new ShapeDrawable(new OvalShape());
    // If the color isn't set, the shape uses black as the default.
    drawable.getPaint().setColor(0xff74AC23);
    // If the bounds aren't set, the shape can't be drawn.
    drawable.setBounds(x, y, x + width, y + height);
  }

  protected void onDraw(Canvas canvas) {
    drawable.draw(canvas);
  }
}

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

Kotlin

private lateinit var customDrawableView: CustomDrawableView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    customDrawableView = CustomDrawableView(this)

    setContentView(customDrawableView)
}

Java

CustomDrawableView customDrawableView;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  customDrawableView = new CustomDrawableView(this);

  setContentView(customDrawableView);
}

אם רוצים להשתמש בתצוגה המותאמת אישית בפריסת ה-XML, המחלקה CustomDrawableView צריכה לבטל את ה-constructor של View(Context, AttributeSet), שמופעל כשהמחלקה מורחבת מ-XML. בדוגמה הבאה אפשר לראות איך להצהיר על CustomDrawableView בפריסת ה-XML:

<com.example.shapedrawable.CustomDrawableView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />

המחלקות ShapeDrawable, כמו הרבה סוגים אחרים של drawable בחבילה android.graphics.drawable, מאפשרות להגדיר מאפיינים שונים של האובייקט באמצעות שיטות ציבוריות. אלה כמה דוגמאות למאפיינים שאולי תרצו לשנות: שקיפות אלפא, פילטר צבע, דית'רינג, אטימות וצבע.

אפשר גם להגדיר צורות פרימיטיביות של רכיבי drawable באמצעות משאבי XML. מידע נוסף זמין במאמר Shape drawable (ציור של צורה) בקטע Drawable resource types (סוגים של משאבים שניתן לצייר).

‫NinePatch drawables

NinePatchDrawable גרפיקה היא תמונה של מפת סיביות שאפשר למתוח ולהשתמש בה כרקע של תצוגה. מערכת Android משנה את הגודל של הגרפיקה באופן אוטומטי כדי להתאים לתוכן של התצוגה. דוגמה לשימוש בתמונת NinePatch היא הרקע שמשמש ללחצנים רגילים ב-Android – הלחצנים צריכים להימתח כדי להתאים למחרוזות באורכים שונים. גרפיקת NinePatch היא תמונה רגילה בפורמט PNG שכוללת גבול נוסף של פיקסל אחד. הקובץ צריך להיקרא 9.png ולהישמר בספרייה res/drawable/ של הפרויקט.

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

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

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

תמונה 1 מציגה דוגמה לגרפיקת NinePatch שמשמשת להגדרת לחצן:

תמונה של אזור שניתן למתיחה ותיבת ריפוד

איור 1: דוגמה לגרפיקת NinePatch שמגדירה לחצן

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

הכלי Draw 9-patch מציע דרך נוחה במיוחד ליצור תמונות NinePatch באמצעות עורך גרפיקה מסוג WYSIWYG. הוא אפילו מציג אזהרות אם יש סיכון ליצירת ארטיפקטים של ציור באזור שניתן למתיחה שהגדרתם, כתוצאה משכפול הפיקסלים.

קוד ה-XML הבא של פריסת דוגמה מדגים איך להוסיף גרפיקה של NinePatch לכמה לחצנים. תמונת ה-NinePatch נשמרת ב-res/drawable/my_button_background.9.png.

<Button android:id="@+id/tiny"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerInParent="true"
        android:text="Tiny"
        android:textSize="8sp"
        android:background="@drawable/my_button_background"/>

<Button android:id="@+id/big"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"
        android:text="Biiiiiiig text!"
        android:textSize="30sp"
        android:background="@drawable/my_button_background"/>

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

באיור 2 מוצגים שני הלחצנים שנוצרו מ-XML ומ-NinePatch image שמוצגים למעלה. שימו לב איך הרוחב והגובה של הכפתור משתנים בהתאם לטקסט, ותמונת הרקע מתרחבת כדי להתאים את עצמה.

תמונה של לחצנים קטנים ורגילים

איור 2: לחצנים שעברו עיבוד באמצעות משאב XML וגרפיקה של NinePatch

נכסי drawable בהתאמה אישית

אם רוצים ליצור ציורים מותאמים אישית, אפשר להרחיב את המחלקה Drawable (או כל אחת ממחלקות המשנה שלה).

השיטה הכי חשובה להטמעה היא draw(Canvas) כי היא מספקת את האובייקט Canvas שבו צריך להשתמש כדי לספק את הוראות הציור.

הקוד הבא מציג מחלקת משנה פשוטה של Drawable שמציירת עיגול:

Kotlin

class MyDrawable : Drawable() {
    private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0) }

    override fun draw(canvas: Canvas) {
        // Get the drawable's bounds
        val width: Int = bounds.width()
        val height: Int = bounds.height()
        val radius: Float = Math.min(width, height).toFloat() / 2f

        // Draw a red circle in the center
        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, redPaint)
    }

    override fun setAlpha(alpha: Int) {
        // This method is required
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        // This method is required
    }

    override fun getOpacity(): Int =
        // Must be PixelFormat.UNKNOWN, TRANSLUCENT, TRANSPARENT, or OPAQUE
        PixelFormat.OPAQUE
}

Java

public class MyDrawable extends Drawable {
    private final Paint redPaint;

    public MyDrawable() {
        // Set up color and text size
        redPaint = new Paint();
        redPaint.setARGB(255, 255, 0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
        // Get the drawable's bounds
        int width = getBounds().width();
        int height = getBounds().height();
        float radius = Math.min(width, height) / 2;

        // Draw a red circle in the center
        canvas.drawCircle(width/2, height/2, radius, redPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        // This method is required
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        // This method is required
    }

    @Override
    public int getOpacity() {
        // Must be PixelFormat.UNKNOWN, TRANSLUCENT, TRANSPARENT, or OPAQUE
        return PixelFormat.OPAQUE;
    }
}

אחר כך אפשר להוסיף את ה-drawable בכל מקום שרוצים, למשל ל-ImageView כמו שמוצג כאן:

Kotlin

val myDrawing = MyDrawable()
val image: ImageView = findViewById(R.id.imageView)
image.setImageDrawable(myDrawing)
image.contentDescription = resources.getString(R.string.my_image_desc)

Java

MyDrawable mydrawing = new MyDrawable();
ImageView image = findViewById(R.id.imageView);
image.setImageDrawable(mydrawing);
image.setContentDescription(getResources().getString(R.string.my_image_desc));

ב-Android 7.0 (רמת API‏ 24) ומעלה, אפשר גם להגדיר מופעים של רכיב ה-Drawable המותאם אישית באמצעות XML בדרכים הבאות:

  • שימוש בשם המחלקה שמוגדר במלואו כשם של רכיב ה-XML. בגישה הזו, מחלקת ה-drawable המותאמת אישית צריכה להיות מחלקה ציבורית ברמה העליונה:
    <com.myapp.MyDrawable xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="#ffff0000" />
  • שימוש ב-drawable כשם תג ה-XML וציון השם המלא של המחלקה מתוך מאפיין המחלקה. אפשר להשתמש בגישה הזו גם למחלקות ציבוריות ברמה העליונה וגם למחלקות פנימיות סטטיות ציבוריות:
    <drawable xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.myapp.MyTopLevelClass$MyDrawable"
        android:color="#ffff0000" />

הוספת גוון לרכיבים שניתנים לציור

ב-Android מגרסה 5.0 (רמת API‏ 21) ואילך, אפשר להוסיף גוון למפות סיביות ולתמונות מסוג nine-patch שמוגדרות כמסכות אלפא. אפשר לצבוע אותם באמצעות משאבי צבע או מאפייני עיצוב שמוגדרים כמשאבי צבע (לדוגמה, ?android:attr/colorPrimary). בדרך כלל, יוצרים את הנכסים האלה רק פעם אחת וצובעים אותם באופן אוטומטי כך שיתאימו לעיצוב.

אפשר להחיל גוון על אובייקטים מסוג BitmapDrawable, NinePatchDrawable או VectorDrawable באמצעות השיטה setTint(). אפשר גם להגדיר את צבע הגוון ואת המצב בפריסות באמצעות המאפיינים android:tint ו-android:tintMode.

חילוץ צבעים בולטים מתמונה

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