באמצעות 'פיתוח' אפשר ליצור צורות מפוליגונים. לדוגמה, אפשר ליצור צורות מהסוגים הבאים:
כדי ליצור פוליגון מעוגל בהתאמה אישית ב-Compose, צריך להוסיף את
התלות של graphics-shapes
app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.0-rc01"
הספרייה הזו מאפשרת ליצור צורות שנוצרו מפוליגונים. בעוד פוליגונים לצורות יש רק קצוות ישרים ופינות חדות, הצורות האלה מאפשרות פינות מעוגלות אופציונליות. כך קל לשנות בין שני סוגים של צורות. קשה לבצע מורפינג בין צורות שרירותיות, והוא נוטה להיות בעיה בזמן התכנון. אבל הספרייה הזאת הופכת את התהליך לפשוט יותר באמצעות שינוי צורות עם מבנים פוליגונים דומים.
יצירת פוליגונים
קטע הקוד הבא יוצר פוליגון בסיסי עם 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
שמכיל את הגיאומטריה
שמייצגים את הצורה המבוקשת. כדי לשרטט את הצורה באפליקציית 'כתיבה',
צריך לקבל ממנו אובייקט Path
כדי להעביר את הצורה לטופס
יודע איך לצייר.
עגלו את הפינות של פוליגון
כדי לעגל את הפינות של פוליגון, משתמשים בפרמטר CornerRounding
. הזה
לוקחת שני פרמטרים: radius
ו-smoothing
. כל פינה מעוגלת מורכבת
של 1-3 עקומות ממעלה שלישית, שבמרכזן יש צורה של קשת עגולה,
עקומות צידיות ('צדדיות') עוברות מהקצה של הצורה לעקומה המרכזית.
רדיוס
radius
הוא רדיוס המעגל המשמש לעגל קודקוד.
לדוגמה, משולש הפינות המעוגל הבא נוצר כך:
צבעים חלקים יותר
החלקה היא גורם שקובע כמה זמן לוקח עד
החלק המעוגל והעגול של הפינה עד הקצה. גורם החלקה: 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
כמו
DrawScope#translate()
.
צורות שונות
אובייקט 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 של אנימציה בכתיבה את ערך ההתקדמות לאורך זמן. לדוגמה, אפשר ליצור אנימציה ללא הגבלה בין שתי הצורות האלה:
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
מגביל בכתיבה כדי לשנות את אופן העיבוד של תוכן קומפוזבילי, וכדי לקחת
היתרון של הצלליות שמשרטטות סביב אזור החיתוך:
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) ) }
כתוצאה מכך:
יכול להיות שהעיבוד הזה לא יהיה שונה ממה שעבר עיבוד, אבל הוא מאפשר לצורך שימוש בתכונות אחרות בכתיבה. לדוגמה, הטכניקה הזו יכולה להיות שמשמש לחיתוך תמונה ולהחלת צל מסביב לאזור שנחתך:
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
,
להתאים אותו לעומס ולתרגם אותו כך שיתאים לעומס. שימו לב להעברה של
ההתקדמות כדי שניתן יהיה להכין את הצורה:
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
לא מיועדת לשימוש באופן שרירותי
אבל הוא נועד במיוחד לפשט את תהליך היצירה של מצולעים מעוגלים
לשנות את שניהם.
מקורות מידע נוספים
למידע נוסף ודוגמאות תוכלו להיעזר במקורות המידע הבאים: