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

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

ציור בסיסי באמצעות מקשי שינוי ו-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.

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

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

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

פעולות שרטוט נפוצות

ציור טקסט

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

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

val textMeasurer = rememberTextMeasurer()

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

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

מדידת טקסט

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

כשאתם מתחילים לכתוב, תוכלו להשתמש ב-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()
)

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

טקסט מרובה שורות שתופס גודל של 2⁄3 מהשטח המלא, עם מלבן רקע
איור 9. טקסט בכמה שורות שמשתמש ב-⅔ מהשטח הכולל, עם מלבן רקע.

שינוי האילוצים, גודל הגופן או כל מאפיין שמשפיע על הגודל שנמדד גורם לדיווח על גודל חדש. אפשר להגדיר גודל קבוע גם ל-width וגם ל-height, ואז הטקסט יתאים לגודל של TextOverflow. לדוגמה, הקוד הבא מעבד את הטקסט ב-1⁄3 מהגובה וב-1⁄3 מהרוחב של השטח הקומפוזבילי, ומגדיר את 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, תוכלו לעיין במקורות המידע הבאים: