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

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

ציור בסיסי באמצעות מקשי שינוי ו-DrawScope

הדרך העיקרית לצייר משהו בהתאמה אישית ב-Compose היא באמצעות מודификаторים, כמו Modifier.drawWithContent,‏ Modifier.drawBehind ו-Modifier.drawWithCache.

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

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

אם כל מה שאתם צריכים הוא רכיב מורכב שאפשר לצייר בו, תוכלו להשתמש ברכיב המורכב Canvas. ה-composable של 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. מלבן שצויר באמצעות לוח הציור ב-Compose.

מידע נוסף על גורמים שונים לשינוי צורת הציור זמין במסמכי העזרה בנושא Graphics Modifiers.

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

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

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

רשת שמציגה את מערכת הקואורדינטות, עם הפינה הימנית העליונה [0, 0] והפינה השמאלית התחתונה [רוחב, גובה]
איור 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() חלות על כל פעולות הציור בתוך פונקציית הלמהדה המתאימה. לדוגמה, הקוד הבא מגדיל את הערך של 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, בדרך כלל אפשר להשתמש ב-composable‏ Text. עם זאת, אם אתם נמצאים ב-DrawScope או שאתם רוצים לצייר את הטקסט באופן ידני עם התאמה אישית, תוכלו להשתמש ב-method‏ DrawScope.drawText().

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

val textMeasurer = rememberTextMeasurer()

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

הצגת המילה Hello שצוירה בלוח הציור
איור 8. ציור טקסט ב-Canvas.

מדידת טקסט

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

ב-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. לדוגמה, הקוד הבא יוצר טקסט ב-⅓ מהגובה וב-⅓ מהרוחב של אזור ה-Composable, ומגדיר את 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)
})

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

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

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

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

API

פלט

drawCircle()

draw circle

drawRect()

draw rect

drawRoundedRect()

draw rounded rect

drawLine()

draw line

drawOval()

draw oval

drawArc()

draw arc

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 וסימון בו ב-Compose.

גישה לאובייקט 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.

מידע נוסף

למידע נוסף על ציור ב-Compose, תוכלו לעיין במקורות המידע הבאים: