יצירת שרטוט בהתאמה אישית

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

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

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

שינוי onDraw()

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

בשלב הראשון יוצרים אובייקט Paint. בקטע הבא נדון בPaint בפירוט רב יותר.

יצירת אובייקטים של שרטוט

android.graphics מפרידה את השרטוט לשני תחומים:

  • מה לצייר, מטופל על ידי Canvas.
  • איך לצייר, מטופל על ידי Paint.

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

לפני שציירת משהו, צריך ליצור אובייקט Paint אחד או יותר. בדוגמה הבאה עושה את זה בשיטה שנקראת init. השיטה הזאת שנקראו מה-constructor של Java, אבל ניתן לאתחל אותו בתוך השורה Kotlin.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
    color = textColor
    if (textHeight == 0f) {
        textHeight = textSize
    } else {
        textSize = textHeight
    }
}

private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    textSize = textHeight
}

private val shadowPaint = Paint(0).apply {
    color = 0x101010
    maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
}

Java

private Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

private void init() {
   textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   textPaint.setColor(textColor);
   if (textHeight == 0) {
       textHeight = textPaint.getTextSize();
   } else {
       textPaint.setTextSize(textHeight);
   }

   piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   piePaint.setStyle(Paint.Style.FILL);
   piePaint.setTextSize(textHeight);

   shadowPaint = new Paint(0);
   shadowPaint.setColor(0xff101010);
   shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
   ...
}

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

טיפול באירועי פריסה

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

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

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

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

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big you can make the pie.
    val diameter = Math.min(ww, hh)
}

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // Account for padding.
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label.
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big you can make the pie.
    float diameter = Math.min(ww, hh);
}

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

דוגמה להטמעה של onMeasure(). כאן הוא מנסה להגדיל את השטח שלו כדי להגדיל את התרשים כתווית:

Kotlin

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // Try for a width based on your minimum.
    val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
    val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

    // Whatever the width is, ask for a height that lets the pie get as big as
    // it can.
    val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
    val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0)

    setMeasuredDimension(w, h)
}

Java

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on your minimum.
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

יש שלושה דברים שחשוב לציין בקוד הזה:

  • החישובים מתייחסים למרווח הפנימי של התצוגה. כפי שצוין קודם לכן, זו האחריות של הצפייה.
  • The Helper method resolveSizeAndState() משמש ליצירת הערכים הסופיים של הרוחב והגובה. כלי העזר הזה יחזיר ערך View.MeasureSpec מתאים באמצעות השוואה בין הגודל הדרוש של התצוגה לערך שהועבר אל onMeasure().
  • אין ערך מוחזר אל onMeasure(). במקום זאת, מסר את התוצאות שלו באמצעות setMeasuredDimension() חובה לקרוא לשיטה הזו. אם משמיטים את הקריאה הזו, בכיתה View גורמת חריגת זמן ריצה.

ציור

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

  • שרטוט טקסט באמצעות drawText() ציון משפחת הגופן באמצעות קריאה setTypeface() ואת הצבע של הטקסט באמצעות קריאה setColor().
  • ציירו צורות פרימיטיביות באמצעות drawRect(), drawOval(), וגם drawArc(). כדי לקבוע אם הצורות ימולאו, יתוארו או שתיהן setStyle()
  • שרטוט צורות מורכבות יותר באמצעות Path בכיתה. כדי להגדיר צורה על ידי הוספת קווים ועקומות ל-Path משרטטים את הצורה באמצעות drawPath(). בדומה לצורות פרימיטיביות, ניתן לתאר נתיבים, למלא אותם או את שניהם, בהתאם ל-setStyle().
  • אפשר להגדיר מילוי הדרגתי על ידי יצירת LinearGradient אובייקטים. שיחת טלפון setShader() כדי להשתמש בLinearGradient על צורות מלאות.
  • שרטט מפות סיביות באמצעות drawBitmap()

הקוד הבא משרטט שילוב של טקסט, קווים וצורות:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    canvas.apply {
        // Draw the shadow.
        drawOval(shadowBounds, shadowPaint)

        // Draw the label text.
        drawText(data[currentItem].label, textX, textY, textPaint)

        // Draw the pie slices.
        data.forEach {item ->
            piePaint.shader = item.shader
            drawArc(
                bounds,
                360 - item.endAngle,
                item.endAngle - item.startAngle,
                true,
                piePaint
            )
        }

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Draw the shadow.
    canvas.drawOval(
            shadowBounds,
            shadowPaint
    );

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, textX, textY, textPaint);

    // Draw the pie slices.
    for (int i = 0; i < data.size(); ++i) {
        Item it = data.get(i);
        piePaint.setShader(it.shader);
        canvas.drawArc(
                bounds,
                360 - it.endAngle,
                it.endAngle - it.startAngle,
                true, 
                piePaint
        );
    }

    // Draw the pointer.
    canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
    canvas.drawCircle(pointerX, pointerY, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

שימוש באפקטים גרפיים

מערכת Android 12 (רמת API 31) מוסיפה את RenderEffect שמפעילה אפקטים גרפיים נפוצים כמו טשטוש, פילטרים צבע, אפקטים של תוכנת ההצללה ב-Android, ועוד View אובייקטים וגם לעיבוד היררכיות. אפשר לשלב אפקטים בתור אפקטים בשרשרת, שכוללים אפקט פנימי וחיצוני, או אפקטים מעורבים. תמיכה בתכונה הזו משתנה בהתאם לעוצמת העיבוד של המכשיר.

אפשר גם להשתמש באפקטים RenderNode עבור View באמצעות התקשרות View.setRenderEffect(RenderEffect).

כדי להטמיע אובייקט RenderEffect:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

אפשר ליצור את התצוגה באופן פרוגרמטי או להגדיל אותה מפריסת XML לאחזר אותו באמצעות קישור תצוגה או findViewById().