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

כדי ליצור מצולע מעוגל בהתאמה אישית ב-Compose, מוסיפים את התלות graphics-shapes
אל app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.1"
הספרייה הזו מאפשרת ליצור צורות שמורכבות ממצולעים. לצורות מצולעות יש רק קצוות ישרים ופינות חדות, אבל אפשר לעגל את הפינות שלהן. כך קל יותר ליצור מעבר בין שתי צורות שונות. קשה ליצור מורפינג בין צורות שרירותיות, והבעיה בדרך כלל מתרחשת בזמן העיצוב. אבל הספרייה הזו מפשטת את התהליך על ידי יצירת מעבר הדרגתי בין הצורות האלה עם מבנים דומים של מצולעים.
יצירת פוליגונים
קטע הקוד הבא יוצר צורת פוליגון בסיסית עם 6 נקודות במרכז אזור הציור:
Box( modifier = Modifier .drawWithCache { val roundedPolygon = RoundedPolygon( numVertices = 6, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2 ) val roundedPolygonPath = roundedPolygon.toPath().asComposePath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Blue) } } .fillMaxSize() )

בדוגמה הזו, הספרייה יוצרת RoundedPolygon
שמכיל את הגיאומטריה שמייצגת את הצורה המבוקשת. כדי לצייר את הצורה הזו באפליקציית Compose, צריך לקבל ממנה אובייקט Path
כדי להכניס את הצורה לטופס ש-Compose יודעת לצייר.
עיגול הפינות של מצולע
כדי לעגל את הפינות של מצולע, משתמשים בפרמטר CornerRounding
. הפונקציה הזו מקבלת שני פרמטרים, radius
ו-smoothing
. כל פינה מעוגלת מורכבת מ-1 עד 3 עקומות קוביות, שהמרכז שלהן הוא בצורת קשת מעגלית, בעוד ששתי העקומות הצדדיות (העקומות הסמוכות) עוברות מהקצה של הצורה לעקומה המרכזית.
רדיוס
radius
הוא הרדיוס של העיגול שמשמש לעיגול קודקוד.
לדוגמה, כך יוצרים משולש עם פינות מעוגלות:


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


לדוגמה, קטע הקוד הבא ממחיש את ההבדל הדק בין הגדרת ההחלקה ל-0 לבין הגדרת ההחלקה ל-1:
Box( modifier = Modifier .drawWithCache { val roundedPolygon = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val roundedPolygonPath = roundedPolygon.toPath().asComposePath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Black) } } .size(100.dp) )

גודל ומיקום
כברירת מחדל, צורה נוצרת עם רדיוס של 1
סביב המרכז (0, 0
). הרדיוס הזה מייצג את המרחק בין המרכז לבין קודקודי החלק החיצוני של המצולע שעליו מבוססת הצורה. שימו לב שעיגול הפינות יוצר צורה קטנה יותר, כי הפינות המעוגלות יהיו קרובות יותר למרכז מאשר הקודקודים המעוגלים. כדי לשנות את הגודל של מצולע, משנים את הערך radius
. כדי לשנות את המיקום, משנים את הערך של centerX
או centerY
של הפוליגון.
אפשר גם לשנות את הגודל, המיקום והסיבוב של האובייקט באמצעות פונקציות טרנספורמציה רגילות כמו DrawScope#translate()
.DrawScope
שינוי צורה
אובייקט Morph
הוא צורה חדשה שמייצגת אנימציה בין שתי צורות מצולעות. כדי ליצור מעבר בין שתי צורות, יוצרים שני אובייקטים של RoundedPolygons
ואובייקט של Morph
שמקבל את שתי הצורות האלה. כדי לחשב צורה בין צורת ההתחלה לצורת הסיום, צריך לספק ערך progress
בין אפס לאחד כדי לקבוע את הצורה בין צורת ההתחלה (0) לצורת הסיום (1):
Box( modifier = Modifier .drawWithCache { val triangle = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val square = RoundedPolygon( numVertices = 4, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f ) val morph = Morph(start = triangle, end = square) val morphPath = morph .toPath(progress = 0.5f).asComposePath() onDrawBehind { drawPath(morphPath, color = Color.Black) } } .fillMaxSize() )
בדוגמה שלמעלה, ההתקדמות היא בדיוק באמצע בין שתי הצורות (משולש מעוגל וריבוע), והתוצאה היא:

ברוב התרחישים, המורפינג מתבצע כחלק מאנימציה, ולא רק כרינדור סטטי. כדי ליצור אנימציה בין שני הערכים האלה, אפשר להשתמש בממשקי ה-API הרגילים של אנימציה ב-Compose כדי לשנות את ערך ההתקדמות לאורך זמן. לדוגמה, אפשר ליצור אנימציה אינסופית של המעבר בין שני הצורות האלה באופן הבא:
val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation") val morphProgress = infiniteAnimation.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(500), repeatMode = RepeatMode.Reverse ), label = "morph" ) Box( modifier = Modifier .drawWithCache { val triangle = RoundedPolygon( numVertices = 3, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f, rounding = CornerRounding( size.minDimension / 10f, smoothing = 0.1f ) ) val square = RoundedPolygon( numVertices = 4, radius = size.minDimension / 2f, centerX = size.width / 2f, centerY = size.height / 2f ) val morph = Morph(start = triangle, end = square) val morphPath = morph .toPath(progress = morphProgress.value) .asComposePath() onDrawBehind { drawPath(morphPath, color = Color.Black) } } .fillMaxSize() )

שימוש במצולע כקליפ
בדרך כלל משתמשים במגדיר clip
ב-Compose כדי לשנות את אופן העיבוד של פונקציה שאפשר להוסיף לה קומפוננטות, וכדי ליהנות מהצללות שנוצרות מסביב לאזור החיתוך:
fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) } class RoundedPolygonShape( private val polygon: RoundedPolygon, private var matrix: Matrix = Matrix() ) : Shape { private var path = Path() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { path.rewind() path = polygon.toPath().asComposePath() matrix.reset() val bounds = polygon.getBounds() val maxDimension = max(bounds.width, bounds.height) matrix.scale(size.width / maxDimension, size.height / maxDimension) matrix.translate(-bounds.left, -bounds.top) path.transform(matrix) return Outline.Generic(path) } }
אחר כך אפשר להשתמש במצולע כקליפ, כמו שמוצג בקטע הקוד הבא:
val hexagon = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val clip = remember(hexagon) { RoundedPolygonShape(polygon = hexagon) } Box( modifier = Modifier .clip(clip) .background(MaterialTheme.colorScheme.secondary) .size(200.dp) ) { Text( "Hello Compose", color = MaterialTheme.colorScheme.onSecondary, modifier = Modifier.align(Alignment.Center) ) }
התוצאה היא:

יכול להיות שהשינוי לא ייראה משמעותי, אבל הוא מאפשר להשתמש בתכונות אחרות ב-Compose. לדוגמה, אפשר להשתמש בטכניקה הזו כדי לחתוך תמונה ולהוסיף צל מסביב לאזור החתוך:
val hexagon = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val clip = remember(hexagon) { RoundedPolygonShape(polygon = hexagon) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .graphicsLayer { this.shadowElevation = 6.dp.toPx() this.shape = clip this.clip = true this.ambientShadowColor = Color.Black this.spotShadowColor = Color.Black } .size(200.dp) ) }

שינוי צורה בלחיצה על לחצן
אתם יכולים להשתמש בספריית graphics-shape
כדי ליצור לחצן שמשנה צורה בין שתי צורות בלחיצה. קודם כול, יוצרים MorphPolygonShape
שמרחיב את Shape
, משנה את הגודל שלו ומתרגם אותו כך שיתאים בצורה נכונה. שימו לב להעברה של progress כדי שאפשר יהיה להנפיש את הצורה:
class MorphPolygonShape( private val morph: Morph, private val percentage: Float ) : Shape { private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) val path = morph.toPath(progress = percentage).asComposePath() path.transform(matrix) return Outline.Generic(path) } }
כדי להשתמש בצורת המעבר הזו, יוצרים שני מצולעים, shapeA
ו-shapeB
. יוצרים את Morph
וזוכרים אותו. לאחר מכן, מחילים את המורפינג על הלחצן כמתאר של קליפ,
באמצעות interactionSource
בלחיצה ככוח המניע מאחורי
האנימציה:
val shapeA = remember { RoundedPolygon( 6, rounding = CornerRounding(0.2f) ) } val shapeB = remember { RoundedPolygon.star( 6, rounding = CornerRounding(0.1f) ) } val morph = remember { Morph(shapeA, shapeB) } val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val animatedProgress = animateFloatAsState( targetValue = if (isPressed) 1f else 0f, label = "progress", animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium) ) Box( modifier = Modifier .size(200.dp) .padding(8.dp) .clip(MorphPolygonShape(morph, animatedProgress.value)) .background(Color(0xFF80DEEA)) .size(200.dp) .clickable(interactionSource = interactionSource, indication = null) { } ) { Text("Hello", modifier = Modifier.align(Alignment.Center)) }
כתוצאה מכך, כשמקישים על התיבה מוצגת האנימציה הבאה:

הוספת אנימציה של שינוי צורה אינסופי
כדי ליצור אנימציה של שינוי צורה שתחזור על עצמה ללא הפסקה, משתמשים ב-rememberInfiniteTransition
.
למטה מוצגת דוגמה לתמונת פרופיל שמשנה את הצורה שלה (ומסתובבת) באופן אינסופי לאורך זמן. בגישה הזו נעשה שימוש בהתאמה קלה של
MorphPolygonShape
שמוצגת למעלה:
class CustomRotatingMorphShape( private val morph: Morph, private val percentage: Float, private val rotation: Float ) : Shape { private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) matrix.rotateZ(rotation) val path = morph.toPath(progress = percentage).asComposePath() path.transform(matrix) return Outline.Generic(path) } } @Preview @Composable private fun RotatingScallopedProfilePic() { val shapeA = remember { RoundedPolygon( 12, rounding = CornerRounding(0.2f) ) } val shapeB = remember { RoundedPolygon.star( 12, rounding = CornerRounding(0.2f) ) } val morph = remember { Morph(shapeA, shapeB) } val infiniteTransition = rememberInfiniteTransition("infinite outline movement") val animatedProgress = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "animatedMorphProgress" ) val animatedRotation = infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( tween(6000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "animatedMorphProgress" ) Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .clip( CustomRotatingMorphShape( morph, animatedProgress.value, animatedRotation.value ) ) .size(200.dp) ) } }
הקוד הזה ייתן את התוצאה המהנה הבאה:

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

אפשר לציין את הקודקודים הנפרדים של הצורה הזו באמצעות העומס העודף RoundedPolygon
שמקבל מערך של מספרים ממשיים של קואורדינטות x ו-y.
כדי לפרק את מצולע הלב, שימו לב שמערכת הקואורדינטות הקוטביות לציון נקודות מקלה על המשימה יותר ממערכת הקואורדינטות הקרטזיות (x,y), שבה 0°
מתחיל בצד ימין וממשיך עם כיוון השעון, ו-270°
נמצא במיקום השעה 12:

עכשיו אפשר להגדיר את הצורה בצורה קלה יותר על ידי ציון הזווית (𝜭) והרדיוס מהמרכז בכל נקודה:

עכשיו אפשר ליצור את הקודקודים ולהעביר אותם לפונקציה RoundedPolygon
:
val vertices = remember { val radius = 1f val radiusSides = 0.8f val innerRadius = .1f floatArrayOf( radialToCartesian(radiusSides, 0f.toRadians()).x, radialToCartesian(radiusSides, 0f.toRadians()).y, radialToCartesian(radius, 90f.toRadians()).x, radialToCartesian(radius, 90f.toRadians()).y, radialToCartesian(radiusSides, 180f.toRadians()).x, radialToCartesian(radiusSides, 180f.toRadians()).y, radialToCartesian(radius, 250f.toRadians()).x, radialToCartesian(radius, 250f.toRadians()).y, radialToCartesian(innerRadius, 270f.toRadians()).x, radialToCartesian(innerRadius, 270f.toRadians()).y, radialToCartesian(radius, 290f.toRadians()).x, radialToCartesian(radius, 290f.toRadians()).y, ) }
צריך לתרגם את הקודקודים לקואורדינטות קרטזיות באמצעות הפונקציה radialToCartesian
הבאה:
internal fun Float.toRadians() = this * PI.toFloat() / 180f internal val PointZero = PointF(0f, 0f) internal fun radialToCartesian( radius: Float, angleRadians: Float, center: PointF = PointZero ) = directionVectorPointF(angleRadians) * radius + center internal fun directionVectorPointF(angleRadians: Float) = PointF(cos(angleRadians), sin(angleRadians))
הקוד שלמעלה נותן לכם את הקודקודים הגולמיים של הלב, אבל כדי לקבל את צורת הלב שבחרתם, צריך לעגל פינות ספציפיות. הפינות במיקומים 90°
ו-270°
לא מעוגלות, אבל הפינות האחרות כן. כדי להגדיר עיגול מותאם אישית לפינות נפרדות, משתמשים בפרמטר perVertexRounding
:
val rounding = remember { val roundingNormal = 0.6f val roundingNone = 0f listOf( CornerRounding(roundingNormal), CornerRounding(roundingNone), CornerRounding(roundingNormal), CornerRounding(roundingNormal), CornerRounding(roundingNone), CornerRounding(roundingNormal), ) } val polygon = remember(vertices, rounding) { RoundedPolygon( vertices = vertices, perVertexRounding = rounding ) } Box( modifier = Modifier .drawWithCache { val roundedPolygonPath = polygon.toPath().asComposePath() onDrawBehind { scale(size.width * 0.5f, size.width * 0.5f) { translate(size.width * 0.5f, size.height * 0.5f) { drawPath(roundedPolygonPath, color = Color(0xFFF15087)) } } } } .size(400.dp) )
התוצאה היא לב ורוד:

אם הצורות הקודמות לא מתאימות לתרחיש השימוש שלכם, אפשר להשתמש במחלקה Path
כדי לצייר צורה בהתאמה אישית, או לטעון קובץ ImageVector
מהדיסק. הספרייה graphics-shapes
לא מיועדת לשימוש בצורות שרירותיות, אלא נועדה במיוחד לפשט את יצירת המצולעים המעוגלים ואת אנימציות המורפינג ביניהם.
מקורות מידע נוספים
מידע נוסף ודוגמאות זמינים במקורות המידע הבאים: