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

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 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 זמין במסמכי התיעוד.

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

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