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

הערך 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) מפל צבעים אופקי
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
            )
        )
)

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

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

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

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

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

אם לא יודעים מה הגודל של אזור הציור (לדוגמה, אם המשתנה Brush מוקצה ל-Text), אפשר להרחיב את 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 השתנה.

הקוד הבא יוצר את ה-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 כדי לצייר את הפיקסלים של הטקסט.

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

מברשת RuntimeShader של AGSL

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

כדי ליצור מברשת 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 ומגדירים את המשתנים הקבועים של 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)
    )
}

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

Shader של AGSL בהתאמה אישית שפועל ב-Compose
איור 7: שדרוג AGSL מותאם אישית שפועל ב-Compose

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

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

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