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

הערך 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 מוקצה לטקסט), אפשר להרחיב את 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 זמינות במקורות המידע הבאים: