מברשת: הדרגתיות וגוון

Brush ב-Compose מתאר איך משהו מצויר על המסך: הוא קובע את הצבעים שמצוירים באזור הציור (כלומר, מעגל, ריבוע, נתיב). יש כמה מברשות מובנות ששימושיות לציור, כמו LinearGradient, RadialGradient או מברשת רגילה SolidColor.

אפשר להשתמש במברשות עם קריאות לציור מסוג Modifier.background(),‏ TextStyle או DrawScope כדי להחיל את סגנון הציור על התוכן שמציירים.

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

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
עיגול שצויר עם מעבר צבע אופקי
איור 1: עיגול שצויר עם מעבר צבע אופקי

מברשות הדרגתיות

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

רשימה של מברשות שיפוע זמינות והפלט המתאים שלהן:

סוג המברשת של מעבר הצבע פלט
Brush.horizontalGradient(colorList) Horizontal Gradient
Brush.linearGradient(colorList) לינארי הדרגתי
Brush.verticalGradient(colorList) מפל צבעים אנכי
Brush.sweepGradient(colorList)
הערה: כדי לקבל מעבר חלק בין הצבעים, צריך להגדיר את הצבע האחרון כצבע ההתחלה.
מפל צבעים קשתי
Brush.radialGradient(colorList) Radial Gradient

שינוי חלוקת הצבעים באמצעות colorStops

כדי לשנות את האופן שבו הצבעים מופיעים בהדרגה, אפשר לשנות את הערך colorStops של כל אחד מהם. ‫colorStops צריך להיות שבר בין 0 ל-1. ערכים שגדולים מ-1 יגרמו לכך שהצבעים האלה לא יוצגו כחלק מהמעבר.

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

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(Brush.horizontalGradient(colorStops = colorStops))
)

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

מברשת שהוגדרה עם עצירות צבע שונות
תרשים 2: מברשת שהוגדרה עם עצירות צבע שונות

חזרה על דפוס עם TileMode

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

הקוד הבא יחזור על דוגמת הגרדיאנט 4 פעמים, כי הערך של endX מוגדר ל-50.dp והגודל מוגדר ל-200.dp:

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val tileSize = with(LocalDensity.current) {
    50.dp.toPx()
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(
            Brush.horizontalGradient(
                listColors,
                endX = tileSize,
                tileMode = TileMode.Repeated
            )
        )
)

בטבלה הבאה מפורטות הפעולות של מצבי התצוגה השונים של החלונות בדוגמה HorizontalGradient שלמעלה:

TileMode פלט
TileMode.Repeated: צבע הקצה חוזר על עצמו מהצבע האחרון לראשון. TileMode Repeated
TileMode.Mirror: הצבעים של הקצה משתקפים מהצבע האחרון לראשון. שיקוף במצב אריחים
TileMode.Clamp: הקצה מוצמד לצבע הסופי. לאחר מכן, המערכת תצבע את שאר האזור בצבע הכי קרוב. הצמדה במצב אריחים
TileMode.Decal: הצגה רק עד לגודל הגבולות. ‫TileMode.Decal משתמש בשחור שקוף כדי לדגום תוכן מחוץ לגבולות המקוריים, ואילו TileMode.Clamp דוגם את צבע הקצה. מדבקה במצב אריח

TileMode פועל באופן דומה עבור שאר הגרדיאנטים הכיווניים, ההבדל הוא הכיוון שבו מתרחש החזרה.

שינוי גודל המברשת

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

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

בדוגמה הזו, מחלקים את הגודל ב-4 כדי לחזור על התבנית 4 פעמים:

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val customBrush = remember {
    object : ShaderBrush() {
        override fun createShader(size: Size): Shader {
            return LinearGradientShader(
                colors = listColors,
                from = Offset.Zero,
                to = Offset(size.width / 4f, 0f),
                tileMode = TileMode.Mirror
            )
        }
    }
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(customBrush)
)

גודל ה-Shader חלקי 4
איור 3: גודל Shader חלקי 4

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

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(
            Brush.radialGradient(
                listOf(Color(0xFF2be4dc), Color(0xFF243484))
            )
        )
)

הגדרה של מעבר צבע רדיאלי ללא שינויים בגודל
איור 4: מוקדי הדרגתי שהוגדרו ללא שינויים בגודל

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

val largeRadialGradient = object : ShaderBrush() {
    override fun createShader(size: Size): Shader {
        val biggerDimension = maxOf(size.height, size.width)
        return RadialGradientShader(
            colors = listOf(Color(0xFF2be4dc), Color(0xFF243484)),
            center = size.center,
            radius = biggerDimension / 2f,
            colorStops = listOf(0f, 0.95f)
        )
    }
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(largeRadialGradient)
)

רדיוס גדול יותר בשיפוע רדיאלי, על סמך גודל האזור
איור 5: רדיוס גדול יותר בהדרגה רדיאלית, על סמך גודל האזור

חשוב לציין שהגודל בפועל שמועבר ליצירה של shader נקבע לפי המקום שבו הוא מופעל. כברירת מחדל, הפונקציה Brush תקצה מחדש את Shader באופן פנימי אם הגודל שונה מהגודל של היצירה האחרונה של Brush, או אם אובייקט מצב שנעשה בו שימוש ביצירת הצללה השתנה.

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

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
val brush = Brush.horizontalGradient(colorStops = colorStops)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .drawBehind {
            drawRect(brush = brush) // will allocate a shader to occupy the 200 x 200 dp drawing area
            inset(10f) {
      /* Will allocate a shader to occupy the 180 x 180 dp drawing area as the
       inset scope reduces the drawing  area by 10 pixels on the left, top, right,
      bottom sides */
                drawRect(brush = brush)
                inset(5f) {
        /* will allocate a shader to occupy the 170 x 170 dp drawing area as the
         inset scope reduces the  drawing area by 5 pixels on the left, top,
         right, bottom sides */
                    drawRect(brush = brush)
                }
            }
        }
)

שימוש בתמונה כמברשת

כדי להשתמש ב-ImageBitmap כ-Brush, טוענים את התמונה כ-ImageBitmap ויוצרים מברשת ImageShader:

val imageBrush =
    ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))

// Use ImageShader Brush with background
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(imageBrush)
)

// Use ImageShader Brush with TextStyle
Text(
    text = "Hello Android!",
    style = TextStyle(
        brush = imageBrush,
        fontWeight = FontWeight.ExtraBold,
        fontSize = 36.sp
    )
)

// Use ImageShader Brush with DrawScope#drawCircle()
Canvas(onDraw = {
    drawCircle(imageBrush)
}, modifier = Modifier.size(200.dp))

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

מכחול ImageShader בשימושים שונים
איור 6: שימוש במברשת ImageShader כדי לצייר רקע, טקסט ומעגל

שימו לב שהטקסט מוצג עכשיו גם באמצעות ImageBitmap כדי לצבוע את הפיקסלים של הטקסט.

דוגמה מתקדמת: מברשת בהתאמה אישית

מברשת AGSL RuntimeShader

AGSL מציע קבוצת משנה של יכולות Shader של GLSL. אפשר לכתוב שיידרים ב-AGSL ולהשתמש בהם עם מברשת בכתיבה מהירה.

כדי ליצור מברשת Shader, קודם מגדירים את ה-Shader כמחרוזת Shader של AGSL:

@Language("AGSL")
val CUSTOM_SHADER = """
    uniform float2 resolution;
    layout(color) uniform half4 color;
    layout(color) uniform half4 color2;

    half4 main(in float2 fragCoord) {
        float2 uv = fragCoord/resolution.xy;

        float mixValue = distance(uv, vec2(0, 1));
        return mix(color, color2, mixValue);
    }
""".trimIndent()

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

לאחר מכן, יוצרים את Shader Brush ומגדירים את המשתנים האחידים עבור resolution – גודל אזור הציור, ו-color ו-color2 שרוצים להשתמש בהם כקלט לשיפוע המותאם אישית:

val Coral = Color(0xFFF3A397)
val LightYellow = Color(0xFFF8EE94)

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
@Preview
fun ShaderBrushExample() {
    Box(
        modifier = Modifier
            .drawWithCache {
                val shader = RuntimeShader(CUSTOM_SHADER)
                val shaderBrush = ShaderBrush(shader)
                shader.setFloatUniform("resolution", size.width, size.height)
                onDrawBehind {
                    shader.setColorUniform(
                        "color",
                        android.graphics.Color.valueOf(
                            LightYellow.red, LightYellow.green,
                            LightYellow
                                .blue,
                            LightYellow.alpha
                        )
                    )
                    shader.setColorUniform(
                        "color2",
                        android.graphics.Color.valueOf(
                            Coral.red,
                            Coral.green,
                            Coral.blue,
                            Coral.alpha
                        )
                    )
                    drawRect(shaderBrush)
                }
            }
            .fillMaxWidth()
            .height(200.dp)
    )
}

אחרי שמריצים את הפקודה, אפשר לראות את הפלט הבא במסך:

Custom AGSL Shader running in Compose
איור 7: Shader מותאם אישית של AGSL שפועל ב-Compose

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

מקורות מידע נוספים

דוגמאות נוספות לשימוש ב-Brush ב-Compose זמינות במקורות המידע הבאים: