باستخدام Compose، يمكنك إنشاء أشكال مكوّنة من مضلّعات. على سبيل المثال، يمكنك إنشاء الأنواع التالية من الأشكال:
لإنشاء مضلّع مخصّص مستدير في 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. يتكوّن كل ركن مستدير من منحنى مكعّب واحد إلى ثلاثة منحنيات، ويكون مركزها على شكل قوس دائري، بينما ينتقل المنحنيان الجانبيان ("المحيطان") من حافة الشكل إلى المنحنى المركزي.
نصف القطر
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، مثل
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
التحميل الزائد الذي يأخذ مصفوفة عائمة من إحداثيات 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 إلى استخدامها مع الأشكال العشوائية، ولكنّها تهدف تحديدًا إلى تبسيط إنشاء المضلّعات المستديرة والرسوم المتحركة التي تتحوّل بينها.
مراجع إضافية
لمزيد من المعلومات والأمثلة، يُرجى الاطّلاع على المراجع التالية:
- مدونة: الشكل المستقبلي للأشياء - الأشكال
- مدوّنة: تحويل الأشكال في Android
- عرض توضيحي لـ Shapes على Github