با Compose میتوانید شکلهایی بسازید که از چندضلعیها تشکیل شدهاند. برای مثال، میتوانید انواع شکلهای زیر را بسازید:
برای ایجاد چندضلعی گرد سفارشی در Compose، وابستگی
graphics-shapes را به
app/build.gradle خود اضافه کنید:
implementation "androidx.graphics:graphics-shapes:1.0.1"
این کتابخانه به شما امکان میدهد شکلهایی را که از چندضلعیها ساخته شدهاند ایجاد کنید. درحالیکه شکلهای چندضلعی فقط لبههای صاف و گوشههای تیز دارند، این شکلها امکان داشتن گوشههای گرد اختیاری را فراهم میکنند. این ویژگی تبدیل بین دو شکل مختلف را ساده میکند. دگردیسی بین شکلهای دلخواه دشوار است و معمولاً مشکلی در زمان طراحی است. اما این کتابخانه با دگردیسی بین این شکلها با ساختارهای چندضلعی مشابه، این کار را ساده میکند.
ایجاد چندضلعیها
قطعه کد زیر یک شکل چندضلعی ساده با ۶ نقطه در مرکز ناحیه طراحی ایجاد میکند:
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 اندازه گرد کردن دایرهای گوشههای گرد را تعیین میکند.صاف کردن
هموارسازی عاملی است که تعیین میکند چقدر طول میکشد تا از بخش گرد دایرهای گوشه به لبه برسیم. ضریب هموارسازی ۰ (هموارنشده، مقدار پیشفرض برای CornerRounding) منجر به گرد شدن کاملاً دایرهای گوشه میشود. عامل هموارسازی غیرصفر (تا حداکثر ۱٫۰) منجر به
گرد شدن گوشه با سه منحنی مجزا میشود.
برای مثال، تکهکد زیر تفاوت جزئی در تنظیم هموارسازی روی ۰ درمقابل ۱ را نشان میدهد:
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 بین صفر و یک را ارائه دهید تا شکل آن بین شکلهای شروع (۰) و پایان (۱) تعیین شود:
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() )
در مثال بالا، پیشرفت دقیقاً در نیمه راه بین دو شکل است (مثلث گرد و مربع)، که نتیجه زیر را تولید میکند:
در بیشتر سناریوها، دگردیسی بهعنوان بخشی از پویانمایی انجام میشود و نه فقط بهعنوان یک پرداخت ایستا. برای پویانمایی بین این دو، میتوانید از Animation APIs in 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° در موقعیت ساعت ۱۲:
اکنون میتوان شکل را با مشخص کردن زاویه (𝜭) و شعاع از مرکز در هر نقطه بهشکل آسانتری تعریف کرد:
اکنون میتوان رأسها را ایجاد کرد و به تابع 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 برای استفاده در شکلهای دلخواه درنظر گرفته نشده است، بلکه بهطور خاص برای سادهسازی ایجاد چندضلعیهای گرد و پویانماییهای دگردیسی بین آنها طراحی شده است.
منابع بیشتر
برای اطلاعات بیشتر و نمونهها، منابع زیر را ببینید: