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

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

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

שינוי התפלגות הצבעים באמצעות 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
            )
        )
)

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

TileMode פלט
TileMode.Repeated: הקצה חוזר על עצמו מהצבע האחרון לצבע הראשון. TileMode Repeated
TileMode.Mirror: הקצה משתקף מהצבע האחרון לצבע הראשון. 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)
)

גודל ההצללה חלקי 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 השתנה.

הקוד הבא יוצר את ה-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 Brush כדי לצייר רקע, טקסט ועיגול

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

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

מברשת AGSL RuntimeShader

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

כדי ליצור מברשת הצללה, קודם צריך להגדיר את ההצללה כמחרוזת הצללה של 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()

ההצללה שלמעלה מקבלת שני צבעי קלט, מחשבת את המרחק מהפינה השמאלית התחתונה (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: הצללה מותאמת אישית של AGSL שפועלת ב-Compose

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

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

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