با Compose می توانید اشکالی ایجاد کنید که از چند ضلعی ساخته شده اند. به عنوان مثال، می توانید انواع شکل های زیر را بسازید:
برای ایجاد یک چند ضلعی گرد سفارشی در 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
ایجاد می کند که هندسه شکل درخواستی را نشان می دهد. برای ترسیم آن شکل در یک برنامه 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() )
در مثال بالا، پیشرفت دقیقاً در نیمه راه بین دو شکل (مثلث گرد و مربع) است که نتیجه زیر را ایجاد می کند:
در اکثر سناریوها، شکلگیری بهعنوان بخشی از یک انیمیشن انجام میشود، و نه فقط یک رندر ثابت. برای متحرک سازی بین این دو، می توانید از API های استاندارد Animation در 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 برای تغییر نحوه رندر شدن یک composable و استفاده از سایه هایی که در اطراف ناحیه برش کشیده می شوند، معمول است:
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 میگیرد، مشخص کنید.
برای شکستن چند ضلعی قلب، توجه کنید که سیستم مختصات قطبی برای مشخص کردن نقاط، این کار را آسانتر از استفاده از سیستم مختصات دکارتی (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
برای استفاده برای اشکال دلخواه در نظر گرفته نشده است، بلکه به طور خاص برای ساده سازی ایجاد چند ضلعی های گرد و انیمیشن های مورف بین آنها طراحی شده است.
منابع اضافی
برای اطلاعات بیشتر و نمونه ها به منابع زیر مراجعه کنید: