باستخدام الميزة Compose، يمكنك إنشاء أشكال مكوّنة من مضلّعات. على سبيل المثال، يمكنك عمل الأنواع التالية من الأشكال:
لإنشاء مضلّع مستدير مخصّص في Compose، أضِف التبعية graphics-shapes
إلى app/build.gradle
:
implementation "androidx.graphics:graphics-shapes:1.0.0-alpha05"
تتيح لك هذه المكتبة إنشاء أشكال تم إنشاؤها من المضلّعات. وعلى الرغم من أنّ الأشكال المضلّعة لا تحتوي إلا على حواف مستقيمة وزوايا حادة، تتيح لك هذه الأشكال إنشاء زوايا مستديرة اختيارية. إنه يجعل من السهل التحوّل بين شكلين مختلفين. يُعد التحوّل صعبًا بين الأشكال العشوائية، ويميل إلى أن يكون مشكلة في وقت التصميم. لكن هذه المكتبة تجعل الأمر بسيطًا من خلال التحويل بين هذه الأشكال ذات الهياكل المضلّعة المتشابهة.
إنشاء مضلّعات
يؤدي المقتطف التالي إلى إنشاء شكل مضلّع أساسي يحتوي على 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
هو نصف قطر الدائرة المستخدَم لتدوير رأس كل رأس.
على سبيل المثال، يتم إنشاء مثلث الزاوية المستدير التالي على النحو التالي:
تسوية
التجانس هو عامل يحدد المدة التي يستغرقها الانتقال من الجزء الدائري في الزاوية إلى الحافة. ينتج عن عامل التجانس 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() )
في المثال أعلاه، يكون التقدم بالضبط في منتصف الطريق بين الشكلين (مثلث مستدير ومربع)، مما يؤدي إلى النتيجة التالية:
في معظم السيناريوهات، يتم التحوّل كجزء من الرسوم المتحركة، وليس مجرد عرض ثابت. لإضافة تأثير متحرك بين هاتين الميزتين، يمكنك استخدام واجهات برمجة التطبيقات للصور المتحركة العادية في 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
، مع تحجيمه وترجمته ليتناسب مع الحجم المناسب. لاحظ تمرير التقدم بحيث
يمكن تحريك الشكل:
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
الذي يأخذ صفيفًا عائمًا من إحداثي س، ص.
لتقسيم مضلّع القلب، يُرجى العِلم أنّ نظام الإحداثيات القطبية
لتحديد النقاط يسهّل هذا الأمر مقارنةً باستخدام نظام الإحداثيات الديكارتية (س،ص)، حيث يبدأ 0°
على الجانب الأيمن ويتابع في اتجاه عقارب الساعة
مع ضبط 270°
في موضع الساعة 12:
يمكن الآن تحديد الشكل بطريقة أسهل من خلال تحديد الزاوية (O) ونصف القطر من المركز عند كل نقطة:
يمكن الآن إنشاء الرؤوس وتمريرها إلى الدالة 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
غير مخصّصة للاستخدام في الأشكال العشوائية، ولكنها تهدف تحديدًا إلى تبسيط عملية إنشاء مضلّعات مستديرة وتحويلها إلى صور متحركة بينها.
مراجع إضافية
لمزيد من المعلومات والأمثلة، يُرجى الاطّلاع على المراجع التالية:
- المدوّنة: The Shape of Things to come - Shapes
- المدوّنة: تحوّل الأشكال في Android
- عرض توضيحي لأشكال "جيت هب"