باستخدام الإنشاء، يمكنك إنشاء أشكال يتم إنشاؤها من المضلّعات. على سبيل المثال، يمكنك إنشاء الأنواع التالية من الأشكال:
![مضلّع أزرق في وسط منطقة الرسم](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/shape_examples_compose.png?hl=ar)
لإنشاء مضلع مستدير مخصص في 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() )
![مضلّع أزرق في وسط منطقة الرسم](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/basic_polygon.png?hl=ar)
في هذا المثال، تنشئ المكتبة RoundedPolygon
الذي يحتوي على الشكل الهندسي الذي يمثّل الشكل المطلوب. لرسم هذا الشكل في تطبيق Compose، يجب الحصول على كائن Path
منه لتحويل الشكل إلى نموذج يعرف تطبيق Compose كيفية رسمه.
استدارة زوايا المضلّع
لتقريب زوايا المضلّع، استخدِم المَعلمة CornerRounding
. ويستغرق ذلك معاملين، هما radius
وsmoothing
. تتكون كل زاوية مستديرة من 1 إلى 3 منحنيات مكعبة، يحتوي مركزها على شكل قوس دائري، بينما ينتقل منحنى الجانبين ("المحيطين") من حافة الشكل إلى منحنى الوسط.
النطاق الجغرافي
radius
هو نصف قطر الدائرة المستخدمة من أجل تقريب الرأس.
على سبيل المثال، يتم إنشاء المثلث المستدير التالي على النحو التالي:
![مثلث بزوايا مستديرة](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/triangle_rounded_corners.png?hl=ar)
![يحدد نصف القطر المستدير r حجم التقريب الدائري للزوايا الدائرية](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/rounding_corner_polygon.png?hl=ar)
r
الحجم المستدير للزوايا الدائرية.تسوية
التجانس هو عامل يحدد المدة التي يستغرقها الانتقال من الجزء المستدير من الزاوية إلى الحافة. يؤدي عامل التجانس 0 (غير المتجانس، القيمة التلقائية لـ CornerRounding
) إلى تقريب الزوايا بشكل دائري تمامًا. يؤدي عامل التجانس غير الصفري (ما يصل إلى 1.0 كحد أقصى) إلى تقريب الزاوية بثلاثة منحنيات منفصلة.
![ينتج عن عامل التجانس 0 (غير متجانس) منحنى مكعّب واحد يتبع دائرة حول الزاوية بها نصف قطر دائري محدد، كما في المثال السابق](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/smoothing_polygon.png?hl=ar)
![ينتج عامل التجانس غير الصفري ثلاثة منحنيات مكعبة لتقريب رأس المضلّع: المنحنى الدائري الداخلي (كما في السابق) بالإضافة إلى منحنيَين محاطين يتحركان بين المنحنى الداخلي وحواف المضلّع.](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/smoothing_polygon_non_zero.png?hl=ar)
على سبيل المثال، يوضح المقتطف أدناه الاختلاف الدقيق في ضبط التجانس على 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) )
![مثلثان أسودان يوضحان الفرق في
معامل التجانس.](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/smoothing_difference.png?hl=ar)
الحجم والموضع
يتم إنشاء شكل تلقائيًا بنصف قطر 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() )
في المثال أعلاه، يكون التقدم بالضبط في منتصف الطريق بين الشكلين (مثلث مستدير ومربع)، لإنتاج النتيجة التالية:
![50% من المسافة بين مثلث دائري ومربّع](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/morph_between_two_shapes.png?hl=ar)
في معظم السيناريوهات، يتم التحويل كجزء من الحركة، وليس فقط عرض ثابت. لإضافة تأثيرات متحركة بينهما، يمكنك استخدام واجهات برمجة تطبيقات Animation 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() )
![يتم التحوّل بلا نهاية بين مربّع ومثلث دائري.](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/infinite_morph_polygon.gif?hl=ar)
استخدام المضلّع كمقطع
من الشائع استخدام معدِّل
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) ) }
ينتج عن ذلك ما يلي:
![مضلّع سداسي يحتوي على النص "helloCompose" في المنتصف.](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/polygon_clip.png?hl=ar)
وقد لا يبدو ذلك مختلفًا عما كان يتم عرضه من قبل، ولكنه يتيح الاستفادة من ميزات أخرى في 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) ) }
![كلب سداسي الشكل مع ظل يغطّي حول الحواف](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/clip_with_shadow.png?hl=ar)
تحوّل الزر عند النقر
يمكنك استخدام مكتبة 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)) }
ينتج عن ذلك الصورة المتحركة التالية عند النقر على المربّع:
![يتم تطبيق التحوّل كنقرة بين شكلَين.](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/morph_click.gif?hl=ar)
تحريك شكل لا نهائي
لتحريك شكل لا نهائي، استخدِم 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) ) } }
يقدم هذا الرمز النتيجة الممتعة التالية:
![يدان تشكلان شكل القلب](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/shape_rotation.gif?hl=ar)
مضلّعات مخصصة
إذا لم تغطي الأشكال التي تم إنشاؤها من المضلّعات العادية حالة استخدامك، فيمكنك إنشاء شكل أكثر تخصيصًا باستخدام قائمة رؤوس. على سبيل المثال، قد ترغب في إنشاء شكل قلب مثل هذا:
![يدان تشكلان شكل القلب](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/heart_shape.png?hl=ar)
يمكنك تحديد الرؤوس الفردية لهذا الشكل باستخدام الحمل الزائد RoundedPolygon
الذي يأخذ صفيفة عائمة للإحداثيات س، ص.
لتقسيم مضلّع القلب، لاحِظ أنّ نظام الإحداثيات القطبية لتحديد النقاط يسهّل ذلك مقارنةً باستخدام نظام الإحداثيات الديكارتية (س، ص)، حيث تبدأ السمة 0°
على الجانب الأيمن، وتتوجّه في اتجاه عقارب الساعة، مع وضع علامة 270°
في اتجاه الساعة 12:
![يدان تشكلان شكل القلب](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/heart_shape_coordinates.png?hl=ar)
يمكن الآن تحديد الشكل بطريقة أسهل من خلال تحديد الزاوية (†) ونصف القطر من المركز عند كل نقطة:
![يدان تشكلان شكل القلب](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/heart_no_rounding.png?hl=ar)
يمكن الآن إنشاء الرؤوس وتمريرها إلى الدالة 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) )
وينتج عن ذلك قلب وردي:
![يدان تشكلان شكل القلب](https://developer.android.com/static/develop/ui/compose/images/graphics/shapes/heart_shape.png?hl=ar)
إذا لم تغطّي الأشكال السابقة حالة استخدامك، يمكنك استخدام الفئة Path
لرسم شكل مخصّص أو تحميل ملف ImageVector
من القرص. مكتبة graphics-shapes
ليست مخصّصة لاستخدام الأشكال العشوائية، ولكنها تهدف تحديدًا إلى تبسيط إنشاء المضلّعات المستديرة وتحويل الصور المتحركة بينها.
مصادر إضافية
لمزيد من المعلومات والأمثلة، يُرجى الاطّلاع على المراجع التالية: