باستخدام أداة "الإنشاء"، يمكنك إنشاء أشكال مصنوعة من مضلّعات. على سبيل المثال، يمكنك إنشاء الأشكال التالية:
لإنشاء مضلع دائري مخصّص في أداة "الإنشاء"، أضِف التبعية
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
يحتوي على الهندسة
التي تمثّل الشكل المطلوب. لرسم هذا الشكل في تطبيق Compose،
يجب الحصول على عنصر Path
منه لتحويل الشكل إلى نموذج يعرف Compose كيفية رسمه.
تقريب زوايا مضلّع
لتقريب زوايا المضلّع، استخدِم المَعلمة CornerRounding
. يأخذ هذا الإجراء
مَعلمتَين، وهما radius
وsmoothing
. يتكون كلّ زاوية مستديرة
من منحنيات مكعبة تتراوح بين منحنى واحد وثلاثة منحنيات، يكون مركزها على شكل قوس دائري بينما تنتقل منحنيات
الجانبَين ("المجاورَين") من حافة الشكل إلى المنحنى المركزي.
النطاق الجغرافي
radius
هو نصف قطر الدائرة المستخدَمة لتقريب رأس.
على سبيل المثال، يتم إنشاء المثلث التالي ذو الزاوية المستديرة على النحو التالي:
تسوية
التمويه هو عامل يحدّد المدة التي يستغرقها الانتقال من
الجزء الدائري من الزاوية إلى الحافة. يؤدي عامل التمويه الذي يُحدَّد على 0
(غير مموَّه، القيمة التلقائية لسمة CornerRounding
) إلى تمويه دائري
تام للزوايا. يؤدي استخدام عامل تمويه غير صفري (يصل إلى الحد الأقصى 1.0) إلى
تقريب الزاوية باستخدام ثلاثة منحنيات منفصلة.
على سبيل المثال، يوضّح المقتطف أدناه الفرق البسيط في ضبط قيمة smoothing على 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
في ميزة "الإنشاء" لتغيير طريقة عرض العنصر القابل للإنشاء، والاستفادة من الظلال التي يتم رسمها حول منطقة الاقتصاص:
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
التفريع الذي يأخذ صفيفًا من النوع float للإحداثيات 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
لإنشاء أشكال
عشوائية، بل هي مخصّصة لتبسيط إنشاء المضلّعات المستديرة والصور المتحرّكة التي تتغيّر بين هذه المضلّعات.
مصادر إضافية
لمزيد من المعلومات والأمثلة، يُرجى الاطّلاع على المراجع التالية:
- مدوّنة: The Shape of Things to Come - Shapes
- مدوّنة: تغيير الشكل في Android
- العرض التوضيحي لأشكال GitHub