החלק החשוב ביותר של תצוגה מותאמת אישית הוא המראה שלה. שרטוט בהתאמה אישית יכול להיות קל או מורכב, בהתאם לצורכי האפליקציה שלכם. המסמך הזה כולל כמה מהפעולות הנפוצות ביותר.
מידע נוסף זמין במאמר הבא: סקירה כללית של פריטי גרפיקה.
שינוי 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()
.