החלק הכי חשוב בתצוגה מותאמת אישית הוא המראה שלה. ציור בהתאמה אישית יכול להיות פשוט או מורכב, בהתאם לצרכים של האפליקציה. במאמר הזה מוסבר על כמה מהפעולות הנפוצות ביותר.
מידע נוסף זמין במאמר סקירה כללית על Drawables.
שינוי מברירת המחדל של onDraw()
השלב הכי חשוב בציור תצוגה מותאמת אישית הוא להחליף את השיטה onDraw(). הפרמטר של onDraw() הוא אובייקט Canvas שאפשר להשתמש בו בתצוגה כדי לצייר את עצמה. המחלקות Canvas מגדירות שיטות לשרטוט טקסט, קווים, מפות סיביות (bitmap) ופרימיטיבים רבים אחרים של גרפיקה. אפשר להשתמש בשיטות האלה ב-onDraw() כדי ליצור ממשק משתמש (UI) בהתאמה אישית.
קודם כל יוצרים אובייקט Paint.
בקטע הבא מוסבר על Paint בפירוט.
יצירת אובייקטים של שרטוט
המסגרת
android.graphics
מחלקת את הציור לשני אזורים:
- מה לצייר, מטופל על ידי
Canvas. - איך מציירים, בטיפול של
Paint.
לדוגמה, Canvas מספק שיטה לציור קו, ו-Paint מספק שיטות להגדרת הצבע של הקו.
Canvas יש שיטה לשרטוט מלבן, ו-Paint
מגדיר אם למלא את המלבן בצבע או להשאיר אותו ריק.
Canvas מגדיר צורות שאפשר לצייר על המסך, ו-Paint מגדיר את הצבע, הסגנון, הגופן וכו' של כל צורה שמציירים.
לפני שמציירים משהו, צריך ליצור אובייקט אחד או יותר של Paint. בדוגמה הבאה, הפעולה הזו מתבצעת בשיטה שנקראת init. השיטה הזו נקראת מהבונה מ-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)); ... }
יצירת אובייקטים מראש היא אופטימיזציה חשובה. התצוגות מצוירות מחדש לעיתים קרובות, ורבים מאובייקטי הציור דורשים אתחול יקר.
יצירת אובייקטים של ציור בתוך השיטה 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); }
יש שלושה דברים חשובים שכדאי לשים לב אליהם בקוד הזה:
- החישובים מתבצעים תוך התחשבות במרווח הפנימי של התצוגה. כמו שציינתי קודם, זו האחריות של התצוגה.
- השיטה helper
resolveSizeAndState()משמשת ליצירת ערכי הרוחב והגובה הסופיים. פונקציית העזר הזו מחזירה ערך מתאים שלView.MeasureSpecעל ידי השוואה בין הגודל הנדרש של התצוגה לבין הערך שמועבר אלView.MeasureSpec.onMeasure() - לפונקציה
onMeasure()אין ערך החזרה. במקום זאת, ה-method מעבירה את התוצאות שלה באמצעות קריאה ל-setMeasuredDimension(). חובה לקרוא ל-method הזה. אם משמיטים את הקריאה הזו, המחלקה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 ולאחזר אותה באמצעות View binding או
findViewById().