גרפיקה בכתיבה

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

שרטוט בסיסי עם משנים ו-DrawScope

הדרך העיקרית לצייר משהו בהתאמה אישית ב-Compose היא באמצעות שימוש במאפייני שינוי, כמו Modifier.drawWithContent,‏ Modifier.drawBehind ו-Modifier.drawWithCache.

לדוגמה, כדי לצייר משהו מאחורי רכיב ה-Composable, אפשר להשתמש במאפיין drawBehind כדי להתחיל להריץ פקודות ציור:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

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

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

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

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

מלבן ורוד שמצויר על רקע לבן ותופס רבע מהמסך
איור 1. מלבן שצויר באמצעות לוח הציור במצב כתיבה.

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

מערכת קואורדינטות

כדי לצייר משהו על המסך, צריך לדעת את ההיסט (x ו-y) ואת הגודל של הפריט. בשיטות רבות של ציור ב-DrawScope, המיקום והגודל מסופקים על ידי ערכי פרמטרים שמוגדרים כברירת מחדל. פרמטרי ברירת המחדל בדרך כלל ממקמים את הפריט בנקודה [0, 0] באזור הציור, ומספקים size כברירת מחדל שממלא את כל אזור הציור, כמו בדוגמה שלמעלה – אפשר לראות שהמלבן ממוקם בפינה הימנית העליונה. כדי לשנות את הגודל והמיקום של הפריט, צריך להבין את מערכת הקואורדינטות ב-Compose.

המקור של מערכת הקואורדינטות ([0,0]) נמצא בפיקסל הימני העליון באזור הציור. הערך של x עולה ככל שמתקדמים ימינה, והערך של y עולה ככל שמתקדמים למטה.

רשת שמציגה את מערכת הקואורדינטות, עם הפינה הימנית העליונה [0, 0] והפינה השמאלית התחתונה [width, height]
איור 2. מערכת קואורדינטות של השרטוט / רשת השרטוט.

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

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

טרנספורמציות בסיסיות

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

קנה המידה

משתמשים ב-DrawScope.scale() כדי להגדיל את הגודל של פעולות השרטוט בפקטור מסוים. פעולות כמו scale() חלות על כל פעולות הציור בתוך פונקציית ה-lambda המתאימה. לדוגמה, הקוד הבא מגדיל את scaleX פי 10 ואת scaleY פי 15:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

מעגל ששינו את הגודל שלו בצורה לא אחידה
איור 3. החלת פעולת שינוי גודל על עיגול ב-Canvas.

תרגום

משתמשים ב-DrawScope.translate() כדי להזיז את פעולות הציור למעלה, למטה, שמאלה או ימינה. לדוגמה, הקוד הבא מעביר את הציור 100 פיקסלים ימינה ו-300 פיקסלים למעלה:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

מעגל שהוזז מהמרכז
איור 4. החלת פעולת תרגום על עיגול ב-Canvas.

סיבוב

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

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

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

מוטמע

משתמשים ב-DrawScope.inset() כדי לשנות את פרמטרי ברירת המחדל של DrawScope הנוכחי, לשנות את גבולות הציור ולתרגם את הציורים בהתאם:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

הקוד הזה מוסיף למעשה ריווח פנימי לפקודות הציור:

מלבן עם ריווח פנימי מסביב
איור 6. החלת שוליים פנימיים על פקודות שרטוט.

טרנספורמציות מרובות

כדי להחיל כמה טרנספורמציות על הציורים, משתמשים בפונקציה DrawScope.withTransform(), שיוצרת ומחילת טרנספורמציה אחת שמשלבת את כל השינויים הרצויים. השימוש ב-withTransform() יעיל יותר מאשר ביצוע קריאות מקוננות לטרנספורמציות נפרדות, כי כל הטרנספורמציות מתבצעות יחד בפעולה אחת, במקום ש-Compose יצטרך לחשב ולשמור כל אחת מהטרנספורמציות המקוננות.

לדוגמה, הקוד הבא מחיל גם תרגום וגם סיבוב על המלבן:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

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

פעולות נפוצות שקשורות לציור

ציור טקסט

כדי לצייר טקסט ב-Compose, בדרך כלל משתמשים ב-Text composable. עם זאת, אם אתם נמצאים בDrawScope או שאתם רוצים לצייר את הטקסט שלכם באופן ידני עם התאמה אישית, אתם יכולים להשתמש בשיטה DrawScope.drawText().

כדי לצייר טקסט, יוצרים TextMeasurer באמצעות rememberTextMeasurer וקוראים ל-drawText עם הכלי למדידה:

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

תמונה שבה רואים את המילה Hello שצוירה בלוח הציור
איור 8. ציור טקסט באזור העריכה.

מדידת טקסט

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

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

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

קטע הקוד הזה יוצר רקע ורוד לטקסט:

טקסט עם כמה שורות שתופס ⅔ מהשטח המלא, עם מלבן ברקע
איור 9. טקסט עם כמה שורות שתופס ⅔ מהשטח המלא, עם מלבן ברקע.

שינוי האילוצים, גודל הגופן או כל מאפיין שמשפיע על גודל התוצאות יגרום לדיווח על גודל חדש. אפשר להגדיר גודל קבוע גם ל-width וגם ל-height, ואז הטקסט יפעל לפי TextOverflow שהוגדר. לדוגמה, הקוד הבא מציג טקסט בשליש מהגובה ובשליש מהרוחב של האזור שניתן להרכבה, ומגדיר את TextOverflow ל-TextOverflow.Ellipsis:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

הטקסט מוצג עכשיו באילוצים עם שלוש נקודות בסוף:

טקסט שמצויר על רקע ורוד, עם שלוש נקודות שמסתירות את הטקסט.
איור 10. TextOverflow.Ellipsis עם אילוצים קבועים למדידת הטקסט.

ציור תמונה

כדי לצייר ImageBitmap באמצעות DrawScope, טוענים את התמונה באמצעות ImageBitmap.imageResource() ואז קוראים ל-drawImage:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

תמונה של כלב שצוירה ב-Canvas
איור 11. מציירים ImageBitmap ב-Canvas.

ציור צורות בסיסיות

יש הרבה פונקציות לציור צורות ב-DrawScope. כדי לצייר צורה, משתמשים באחת מפונקציות הציור המוגדרות מראש, כמו drawCircle:

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

פלט

drawCircle()

ציור מעגל

drawRect()

draw rect

drawRoundedRect()

draw rounded rect

drawLine()

ציור קו

drawOval()

ציור אליפסה

drawArc()

ציור קשת

drawPoints()

נקודות ציור

ציור נתיב

נתיב הוא סדרה של הוראות מתמטיות שמובילות לציור אחרי ההפעלה. ‫DrawScope יכול לשרטט נתיב באמצעות השיטה DrawScope.drawPath().

לדוגמה, נניח שרצית לצייר משולש. אפשר ליצור נתיב באמצעות פונקציות כמו lineTo() ו-moveTo(), על סמך הגודל של אזור הציור. אחר כך, קוראים ל-drawPath() עם הנתיב החדש שנוצר כדי לקבל משולש.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

משולש סגול הפוך שצויר ב'יצירה'
איור 12. יצירה ושרטוט של Path בהודעה חדשה.

גישה לאובייקט Canvas

ב-DrawScope, אין לכם גישה ישירה לאובייקט Canvas. אפשר להשתמש בפונקציה DrawScope.drawIntoCanvas() כדי לקבל גישה לאובייקט Canvas עצמו, שאפשר להפעיל עליו פונקציות.

לדוגמה, אם יש לכם Drawable מותאם אישית שאתם רוצים לצייר על אזור הציור, אתם יכולים לגשת לאזור הציור ולהפעיל את Drawable#draw(), ולהעביר את האובייקט Canvas:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

‫ShapeDrawable שחור בצורת אליפסה שתופס את כל הגודל
איור 13. גישה ללוח הציור כדי לצייר Drawable.

מידע נוסף

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