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