اشکال در نوشتن

با Compose می توانید اشکالی ایجاد کنید که از چند ضلعی ساخته شده اند. به عنوان مثال، می توانید انواع شکل های زیر را بسازید:

شش ضلعی آبی در مرکز منطقه طراحی
شکل 1 . نمونه هایی از اشکال مختلف که می توانید با کتابخانه اشکال گرافیکی بسازید

برای ایجاد یک چند ضلعی گرد سفارشی در 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()
)

شش ضلعی آبی در مرکز منطقه طراحی
شکل 2 . شش ضلعی آبی در مرکز منطقه طراحی.

در این مثال، کتابخانه یک RoundedPolygon ایجاد می کند که هندسه شکل درخواستی را نشان می دهد. برای ترسیم آن شکل در یک برنامه Compose، باید یک شی Path از آن دریافت کنید تا شکل را به شکلی تبدیل کنید که Compose چگونه ترسیم کند.

گوشه های یک چند ضلعی را گرد کنید

برای گرد کردن گوشه های یک چند ضلعی، از پارامتر CornerRounding استفاده کنید. این به دو پارامتر، radius و smoothing نیاز دارد. هر گوشه گرد از 1 تا 3 منحنی مکعبی تشکیل شده است که مرکز آن یک شکل قوس دایره ای دارد در حالی که منحنی های دو طرفه ("طرف") از لبه شکل به منحنی وسط منتقل می شوند.

شعاع

radius دایره ای است که برای گرد کردن یک راس استفاده می شود.

به عنوان مثال، مثلث گوشه گرد زیر به صورت زیر ساخته شده است:

مثلث با گوشه های گرد
شکل 3 . مثلث با گوشه های گرد.
شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند
شکل 4 . شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند.

صاف کردن

هموار شدن عاملی است که تعیین می کند چه مدت طول می کشد تا از قسمت گرد گوشه به لبه برسد. یک ضریب هموارسازی 0 (صاف نشده، مقدار پیش‌فرض برای CornerRounding ) منجر به گرد کردن گوشه کاملاً دایره‌ای می‌شود. یک ضریب هموارسازی غیر صفر (تا حداکثر 1.0) باعث می شود که گوشه توسط سه منحنی مجزا گرد شود.

یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی ایجاد می کند که از دایره ای دور گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
شکل 5 . یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی منفرد ایجاد می کند که از دایره ای در اطراف گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
یک ضریب هموارسازی غیر صفر، سه منحنی مکعبی برای گرد کردن راس ایجاد می‌کند: منحنی دایره‌ای داخلی (مانند قبل) به اضافه دو منحنی کناری که بین منحنی داخلی و لبه‌های چندضلعی انتقال می‌یابند.
شکل 6 . یک ضریب هموارسازی غیر صفر، سه منحنی مکعبی برای گرد کردن راس ایجاد می‌کند: منحنی دایره‌ای داخلی (مانند قبل) به اضافه دو منحنی کناری که بین منحنی داخلی و لبه‌های چندضلعی انتقال می‌یابند.

به عنوان مثال، قطعه زیر تفاوت ظریف در تنظیم هموارسازی 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)
)

دو مثلث سیاه که تفاوت پارامتر هموارسازی را نشان می دهد.
شکل 7 . دو مثلث سیاه که تفاوت پارامتر هموارسازی را نشان می دهد.

اندازه و موقعیت

به طور پیش فرض، شکلی با شعاع 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()
)

در مثال بالا، پیشرفت دقیقاً در نیمه راه بین دو شکل (مثلث گرد و مربع) است که نتیجه زیر را ایجاد می کند:

50% فاصله بین مثلث گرد و مربع
شکل 8 . 50% فاصله بین مثلث گرد و مربع.

در اکثر سناریوها، شکل‌گیری به‌عنوان بخشی از یک انیمیشن انجام می‌شود، و نه فقط یک رندر ثابت. برای متحرک سازی بین این دو، می توانید از 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()
)

شکل‌گیری بی‌نهایت بین یک مربع و یک مثلث گرد
شکل 9 . شکل‌گیری بی‌نهایت بین یک مربع و یک مثلث گرد.

از چند ضلعی به عنوان کلیپ استفاده کنید

استفاده از اصلاح کننده 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)
    )
}

این منجر به موارد زیر می شود:

شش ضلعی با متن «hello compose» در مرکز.
شکل 10 . شش گوش با متن "Hello Compose" در مرکز.

این ممکن است تفاوت چندانی با آنچه قبلاً رندر بود نداشته باشد، اما امکان استفاده از سایر ویژگی‌ها در 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)

    )
}

سگ در شش گوش با سایه اعمال شده در اطراف لبه ها
شکل 11 . شکل سفارشی به عنوان گیره اعمال می شود.

دکمه مورف با کلیک

می توانید از کتابخانه 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))
}

با ضربه زدن روی جعبه، انیمیشن زیر ایجاد می شود:

شکل به عنوان یک کلیک بین دو شکل اعمال می شود
شکل 12 . شکل به عنوان یک کلیک بین دو شکل اعمال می شود.

شکل را بی نهایت متحرک کنید

برای متحرک سازی بی پایان یک شکل مورف، از 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)
        )
    }
}

این کد نتیجه جالب زیر را می دهد:

شکل قلب
شکل 13 . تصویر نمایه ای که با یک شکل چرخشی بریده شده است.

چند ضلعی های سفارشی

اگر اشکال ایجاد شده از چند ضلعی های معمولی مورد استفاده شما را پوشش نمی دهند، می توانید یک شکل سفارشی تر با لیستی از رئوس ایجاد کنید. برای مثال، ممکن است بخواهید یک شکل قلب مانند زیر ایجاد کنید:

شکل قلب
شکل 14 . شکل قلب.

می‌توانید رئوس منفرد این شکل را با استفاده از اضافه بار RoundedPolygon که یک آرایه شناور از مختصات x، y می‌گیرد، مشخص کنید.

برای شکستن چند ضلعی قلب، توجه کنید که سیستم مختصات قطبی برای مشخص کردن نقاط، این کار را آسان‌تر از استفاده از سیستم مختصات دکارتی (x,y) می‌کند، جایی که از سمت راست شروع می‌شود و در جهت عقربه‌های ساعت، با 270° در موقعیت ساعت 12:

شکل قلب
شکل 15 . شکل قلب با مختصات.

اکنون می توان با تعیین زاویه (𝜭) و شعاع از مرکز در هر نقطه، شکل را به روشی ساده تر تعریف کرد:

شکل قلب
شکل 16 . شکل قلب با مختصات، بدون گرد.

اکنون می توان رئوس را ایجاد کرد و به تابع 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)
)

این باعث ایجاد قلب صورتی می شود:

شکل قلب
شکل 17 . نتیجه شکل قلب

اگر شکل‌های قبلی مورد استفاده شما را پوشش نمی‌دهند، از کلاس Path برای ترسیم یک شکل سفارشی یا بارگیری یک فایل ImageVector از دیسک استفاده کنید. کتابخانه graphics-shapes برای استفاده برای اشکال دلخواه در نظر گرفته نشده است، بلکه به طور خاص برای ساده سازی ایجاد چند ضلعی های گرد و انیمیشن های مورف بین آنها طراحی شده است.

منابع اضافی

برای اطلاعات بیشتر و نمونه ها به منابع زیر مراجعه کنید:

،

با Compose می توانید اشکالی ایجاد کنید که از چند ضلعی ساخته شده اند. به عنوان مثال، می توانید انواع شکل های زیر را بسازید:

شش ضلعی آبی در مرکز منطقه طراحی
شکل 1 . نمونه هایی از اشکال مختلف که می توانید با کتابخانه اشکال گرافیکی بسازید

برای ایجاد یک چند ضلعی گرد سفارشی در 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()
)

شش ضلعی آبی در مرکز منطقه طراحی
شکل 2 . شش ضلعی آبی در مرکز منطقه طراحی.

در این مثال، کتابخانه یک RoundedPolygon ایجاد می کند که هندسه شکل درخواستی را نشان می دهد. برای ترسیم آن شکل در یک برنامه Compose، باید یک شی Path از آن دریافت کنید تا شکل را به شکلی تبدیل کنید که Compose چگونه ترسیم کند.

گوشه های یک چند ضلعی را گرد کنید

برای گرد کردن گوشه های یک چند ضلعی، از پارامتر CornerRounding استفاده کنید. این به دو پارامتر، radius و smoothing نیاز دارد. هر گوشه گرد از 1 تا 3 منحنی مکعبی تشکیل شده است که مرکز آن یک شکل قوس دایره ای دارد در حالی که منحنی های دو طرفه ("طرف") از لبه شکل به منحنی وسط منتقل می شوند.

شعاع

radius دایره ای است که برای گرد کردن یک راس استفاده می شود.

به عنوان مثال، مثلث گوشه گرد زیر به صورت زیر ساخته شده است:

مثلث با گوشه های گرد
شکل 3 . مثلث با گوشه های گرد.
شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند
شکل 4 . شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند.

صاف کردن

هموار شدن عاملی است که تعیین می کند چه مدت طول می کشد تا از قسمت گرد گوشه به لبه برسد. یک ضریب هموارسازی 0 (صاف نشده، مقدار پیش‌فرض برای CornerRounding ) منجر به گرد کردن گوشه کاملاً دایره‌ای می‌شود. یک ضریب هموارسازی غیر صفر (تا حداکثر 1.0) باعث می شود که گوشه توسط سه منحنی مجزا گرد شود.

یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی ایجاد می کند که از دایره ای دور گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
شکل 5 . یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی منفرد ایجاد می کند که از دایره ای در اطراف گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
یک ضریب هموارسازی غیر صفر، سه منحنی مکعبی برای گرد کردن راس ایجاد می‌کند: منحنی دایره‌ای داخلی (مانند قبل) به اضافه دو منحنی کناری که بین منحنی داخلی و لبه‌های چندضلعی انتقال می‌یابند.
شکل 6 . یک ضریب هموارسازی غیر صفر، سه منحنی مکعبی برای گرد کردن راس ایجاد می‌کند: منحنی دایره‌ای داخلی (مانند قبل) به اضافه دو منحنی کناری که بین منحنی داخلی و لبه‌های چندضلعی انتقال می‌یابند.

به عنوان مثال، قطعه زیر تفاوت ظریف در تنظیم هموارسازی 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)
)

دو مثلث سیاه که تفاوت پارامتر هموارسازی را نشان می دهد.
شکل 7 . دو مثلث سیاه که تفاوت پارامتر هموارسازی را نشان می دهد.

اندازه و موقعیت

به طور پیش فرض، شکلی با شعاع 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()
)

در مثال بالا، پیشرفت دقیقاً در نیمه راه بین دو شکل (مثلث گرد و مربع) است که نتیجه زیر را ایجاد می کند:

50% فاصله بین مثلث گرد و مربع
شکل 8 . 50% فاصله بین مثلث گرد و مربع.

در اکثر سناریوها، شکل‌گیری به‌عنوان بخشی از یک انیمیشن انجام می‌شود، و نه فقط یک رندر ثابت. برای متحرک سازی بین این دو، می توانید از 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()
)

شکل‌گیری بی‌نهایت بین یک مربع و یک مثلث گرد
شکل 9 . شکل‌گیری بی‌نهایت بین یک مربع و یک مثلث گرد.

از چند ضلعی به عنوان کلیپ استفاده کنید

استفاده از اصلاح کننده 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)
    )
}

این منجر به موارد زیر می شود:

شش ضلعی با متن «hello compose» در مرکز.
شکل 10 . شش گوش با متن "Hello Compose" در مرکز.

این ممکن است تفاوت چندانی با آنچه قبلاً رندر بود نداشته باشد، اما امکان استفاده از سایر ویژگی‌ها در 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)

    )
}

سگ در شش گوش با سایه اعمال شده در اطراف لبه ها
شکل 11 . شکل سفارشی به عنوان گیره اعمال می شود.

دکمه مورف با کلیک

می توانید از کتابخانه 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))
}

با ضربه زدن روی جعبه، انیمیشن زیر ایجاد می شود:

شکل به عنوان یک کلیک بین دو شکل اعمال می شود
شکل 12 . شکل به عنوان یک کلیک بین دو شکل اعمال می شود.

شکل را بی نهایت متحرک کنید

برای متحرک سازی بی پایان یک شکل مورف، از 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)
        )
    }
}

این کد نتیجه جالب زیر را می دهد:

شکل قلب
شکل 13 . تصویر نمایه ای که با یک شکل چرخشی بریده شده است.

چند ضلعی های سفارشی

اگر اشکال ایجاد شده از چند ضلعی های معمولی مورد استفاده شما را پوشش نمی دهند، می توانید یک شکل سفارشی تر با لیستی از رئوس ایجاد کنید. برای مثال، ممکن است بخواهید یک شکل قلب مانند زیر ایجاد کنید:

شکل قلب
شکل 14 . شکل قلب.

می‌توانید رئوس منفرد این شکل را با استفاده از اضافه بار RoundedPolygon که یک آرایه شناور از مختصات x، y می‌گیرد، مشخص کنید.

برای شکستن چند ضلعی قلب، توجه کنید که سیستم مختصات قطبی برای مشخص کردن نقاط، این کار را آسان‌تر از استفاده از سیستم مختصات دکارتی (x,y) می‌کند، جایی که از سمت راست شروع می‌شود و در جهت عقربه‌های ساعت، با 270° در موقعیت ساعت 12:

شکل قلب
شکل 15 . شکل قلب با مختصات.

اکنون می توان با تعیین زاویه (𝜭) و شعاع از مرکز در هر نقطه، شکل را به روشی ساده تر تعریف کرد:

شکل قلب
شکل 16 . شکل قلب با مختصات، بدون گرد.

اکنون می توان رئوس را ایجاد کرد و به تابع 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)
)

این باعث ایجاد قلب صورتی می شود:

شکل قلب
شکل 17 . نتیجه شکل قلب

اگر شکل‌های قبلی مورد استفاده شما را پوشش نمی‌دهند، از کلاس Path برای ترسیم یک شکل سفارشی یا بارگیری یک فایل ImageVector از دیسک استفاده کنید. کتابخانه graphics-shapes برای استفاده برای اشکال دلخواه در نظر گرفته نشده است، بلکه به طور خاص برای ساده سازی ایجاد چند ضلعی های گرد و انیمیشن های مورف بین آنها طراحی شده است.

منابع اضافی

برای اطلاعات بیشتر و نمونه ها به منابع زیر مراجعه کنید:

،

با Compose می توانید اشکالی ایجاد کنید که از چند ضلعی ساخته شده اند. به عنوان مثال، می توانید انواع شکل های زیر را بسازید:

شش ضلعی آبی در مرکز منطقه طراحی
شکل 1 . نمونه هایی از اشکال مختلف که می توانید با کتابخانه اشکال گرافیکی بسازید

برای ایجاد یک چند ضلعی گرد سفارشی در 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()
)

شش ضلعی آبی در مرکز منطقه طراحی
شکل 2 . شش ضلعی آبی در مرکز منطقه طراحی.

در این مثال، کتابخانه یک RoundedPolygon ایجاد می کند که هندسه شکل درخواستی را نشان می دهد. برای ترسیم آن شکل در یک برنامه Compose، باید یک شی Path از آن دریافت کنید تا شکل را به شکلی تبدیل کنید که Compose چگونه ترسیم کند.

گوشه های یک چند ضلعی را گرد کنید

برای گرد کردن گوشه های یک چند ضلعی، از پارامتر CornerRounding استفاده کنید. این به دو پارامتر، radius و smoothing نیاز دارد. هر گوشه گرد از 1 تا 3 منحنی مکعبی تشکیل شده است که مرکز آن یک شکل قوس دایره ای دارد در حالی که منحنی های دو طرفه ("طرف") از لبه شکل به منحنی وسط منتقل می شوند.

شعاع

radius دایره ای است که برای گرد کردن یک راس استفاده می شود.

به عنوان مثال، مثلث گوشه گرد زیر به صورت زیر ساخته شده است:

مثلث با گوشه های گرد
شکل 3 . مثلث با گوشه های گرد.
شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند
شکل 4 . شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند.

صاف کردن

هموار شدن عاملی است که تعیین می کند چه مدت طول می کشد تا از قسمت گرد گوشه به لبه برسد. یک ضریب هموارسازی 0 (صاف نشده، مقدار پیش‌فرض برای CornerRounding ) منجر به گرد کردن گوشه کاملاً دایره‌ای می‌شود. یک ضریب هموارسازی غیر صفر (تا حداکثر 1.0) باعث می شود که گوشه توسط سه منحنی مجزا گرد شود.

یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی ایجاد می کند که از دایره ای دور گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
شکل 5 . یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی منفرد ایجاد می کند که از دایره ای در اطراف گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
یک ضریب هموارسازی غیر صفر، سه منحنی مکعبی برای گرد کردن راس ایجاد می‌کند: منحنی دایره‌ای داخلی (مانند قبل) به اضافه دو منحنی کناری که بین منحنی داخلی و لبه‌های چندضلعی انتقال می‌یابند.
شکل 6 . یک ضریب هموارسازی غیر صفر، سه منحنی مکعبی برای گرد کردن راس ایجاد می‌کند: منحنی دایره‌ای داخلی (مانند قبل) به اضافه دو منحنی کناری که بین منحنی داخلی و لبه‌های چندضلعی انتقال می‌یابند.

به عنوان مثال، قطعه زیر تفاوت ظریف در تنظیم هموارسازی 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)
)

دو مثلث سیاه که تفاوت پارامتر هموارسازی را نشان می دهد.
شکل 7 . دو مثلث سیاه که تفاوت پارامتر هموارسازی را نشان می دهد.

اندازه و موقعیت

به طور پیش فرض، شکلی با شعاع 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()
)

در مثال بالا، پیشرفت دقیقاً در نیمه راه بین دو شکل (مثلث گرد و مربع) است که نتیجه زیر را ایجاد می کند:

50% فاصله بین مثلث گرد و مربع
شکل 8 . 50% فاصله بین مثلث گرد و مربع.

در اکثر سناریوها، شکل‌گیری به‌عنوان بخشی از یک انیمیشن انجام می‌شود، و نه فقط یک رندر ثابت. برای متحرک سازی بین این دو، می توانید از 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()
)

شکل‌گیری بی‌نهایت بین یک مربع و یک مثلث گرد
شکل 9 . شکل‌گیری بی‌نهایت بین یک مربع و یک مثلث گرد.

از چند ضلعی به عنوان کلیپ استفاده کنید

استفاده از اصلاح کننده 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)
    )
}

این منجر به موارد زیر می شود:

شش ضلعی با متن «hello compose» در مرکز.
شکل 10 . شش گوش با متن "Hello Compose" در مرکز.

این ممکن است با آنچه قبلاً رندر شده بود متفاوت به نظر نرسد، اما امکان استفاده از ویژگی های دیگر در 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)

    )
}

سگ در شش گوش با سایه اعمال شده در اطراف لبه ها
شکل 11 . شکل سفارشی به عنوان گیره اعمال می شود.

دکمه مورف با کلیک

می توانید از کتابخانه 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))
}

با ضربه زدن روی جعبه، انیمیشن زیر ایجاد می شود:

شکل به عنوان یک کلیک بین دو شکل اعمال می شود
شکل 12 . شکل به عنوان یک کلیک بین دو شکل اعمال می شود.

شکل را بی نهایت متحرک کنید

برای متحرک سازی بی پایان یک شکل مورف، از 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)
        )
    }
}

این کد نتیجه جالب زیر را می دهد:

شکل قلب
شکل 13 . تصویر نمایه ای که با یک شکل چرخشی بریده شده است.

چند ضلعی های سفارشی

اگر اشکال ایجاد شده از چند ضلعی های معمولی مورد استفاده شما را پوشش نمی دهند، می توانید یک شکل سفارشی تر با لیستی از رئوس ایجاد کنید. برای مثال، ممکن است بخواهید یک شکل قلب مانند زیر ایجاد کنید:

شکل قلب
شکل 14 . شکل قلب.

می‌توانید رئوس منفرد این شکل را با استفاده از اضافه بار RoundedPolygon که یک آرایه شناور از مختصات x، y می‌گیرد، مشخص کنید.

برای شکستن چند ضلعی قلب، توجه کنید که سیستم مختصات قطبی برای مشخص کردن نقاط، این کار را آسان‌تر از استفاده از سیستم مختصات دکارتی (x,y) می‌کند، جایی که از سمت راست شروع می‌شود و در جهت عقربه‌های ساعت، با 270° در موقعیت ساعت 12:

شکل قلب
شکل 15 . شکل قلب با مختصات.

اکنون می توان با تعیین زاویه (𝜭) و شعاع از مرکز در هر نقطه، شکل را به روشی ساده تر تعریف کرد:

شکل قلب
شکل 16 . شکل قلب با مختصات، بدون گرد.

اکنون می توان رئوس را ایجاد کرد و به تابع 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)
)

این باعث ایجاد قلب صورتی می شود:

شکل قلب
شکل 17 . نتیجه شکل قلب

اگر شکل‌های قبلی مورد استفاده شما را پوشش نمی‌دهند، از کلاس Path برای ترسیم یک شکل سفارشی یا بارگیری یک فایل ImageVector از دیسک استفاده کنید. کتابخانه graphics-shapes برای استفاده برای اشکال دلخواه در نظر گرفته نشده است، بلکه به طور خاص برای ساده سازی ایجاد چند ضلعی های گرد و انیمیشن های مورف بین آنها طراحی شده است.

منابع اضافی

برای اطلاعات بیشتر و نمونه ها به منابع زیر مراجعه کنید:

،

با Compose می توانید اشکالی ایجاد کنید که از چند ضلعی ساخته شده اند. به عنوان مثال، می توانید انواع شکل های زیر را بسازید:

شش ضلعی آبی در مرکز منطقه طراحی
شکل 1 . نمونه هایی از اشکال مختلف که می توانید با کتابخانه اشکال گرافیکی بسازید

برای ایجاد یک چند ضلعی گرد سفارشی در 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()
)

شش ضلعی آبی در مرکز منطقه طراحی
شکل 2 . شش ضلعی آبی در مرکز منطقه طراحی.

در این مثال، کتابخانه یک RoundedPolygon ایجاد می کند که هندسه شکل درخواستی را نشان می دهد. برای ترسیم آن شکل در یک برنامه Compose، باید یک شی Path از آن دریافت کنید تا شکل را به شکلی تبدیل کنید که Compose چگونه ترسیم کند.

گوشه های یک چند ضلعی را گرد کنید

برای گرد کردن گوشه های یک چند ضلعی، از پارامتر CornerRounding استفاده کنید. این به دو پارامتر، radius و smoothing نیاز دارد. هر گوشه گرد از 1 تا 3 منحنی مکعبی تشکیل شده است که مرکز آن یک شکل قوس دایره ای دارد در حالی که منحنی های دو طرفه ("طرف") از لبه شکل به منحنی وسط منتقل می شوند.

شعاع

radius دایره ای است که برای گرد کردن یک راس استفاده می شود.

به عنوان مثال، مثلث گوشه گرد زیر به صورت زیر ساخته شده است:

مثلث با گوشه های گرد
شکل 3 . مثلث با گوشه های گرد.
شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند
شکل 4 . شعاع گرد کردن r اندازه گرد کردن گوشه های گرد را تعیین می کند.

صاف کردن

هموار شدن عاملی است که تعیین می کند چه مدت طول می کشد تا از قسمت گرد دایره ای گوشه به لبه برسد. یک ضریب هموارسازی 0 (صاف نشده، مقدار پیش‌فرض برای CornerRounding ) منجر به گرد کردن گوشه کاملاً دایره‌ای می‌شود. یک ضریب هموارسازی غیر صفر (تا حداکثر 1.0) باعث می شود که گوشه توسط سه منحنی مجزا گرد شود.

یک ضریب هموارسازی 0 (صاف نشده) یک منحنی مکعبی ایجاد می کند که از دایره ای دور گوشه با شعاع گرد مشخص شده پیروی می کند، مانند مثال قبلی.
شکل 5 . یک فاکتور صاف کننده 0 (بدون سیم) یک منحنی مکعب منفرد تولید می کند که مانند مثال قبلی ، یک دایره در اطراف گوشه را با شعاع گرد مشخص شده دنبال می کند.
یک فاکتور صاف کننده غیرزرو سه منحنی مکعب را برای دور کردن راس تولید می کند: منحنی دایره ای داخلی (مانند گذشته) به علاوه دو منحنی پهلو که بین منحنی داخلی و لبه های چند ضلعی انتقال می یابد.
شکل 6 . یک فاکتور صاف کننده غیرزرو سه منحنی مکعب را برای دور کردن راس تولید می کند: منحنی دایره ای داخلی (مانند گذشته) به علاوه دو منحنی پهلو که بین منحنی داخلی و لبه های چند ضلعی انتقال می یابد.

به عنوان مثال ، قطعه زیر تفاوت ظریف در تنظیم صاف کردن 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)
)

دو مثلث سیاه که تفاوت در پارامتر هموار سازی را نشان می دهد.
شکل 7 . دو مثلث سیاه که تفاوت در پارامتر هموار سازی را نشان می دهد.

اندازه و موقعیت

به طور پیش فرض ، یک شکل با شعاع 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()
)

در مثال بالا ، پیشرفت دقیقاً در نیمه راه بین دو شکل (مثلث گرد و یک مربع) است و نتیجه زیر را تولید می کند:

50 ٪ راه بین یک مثلث گرد و یک مربع
شکل 8 . 50 ٪ راه بین مثلث گرد و یک مربع.

در بیشتر سناریوها ، مورفین به عنوان بخشی از یک انیمیشن انجام می شود ، و نه فقط یک رندر استاتیک. برای جابجایی بین این دو ، می توانید از API های انیمیشن استاندارد در آهنگسازی استفاده کنید تا مقدار پیشرفت را با گذشت زمان تغییر دهید. به عنوان مثال ، شما می توانید بی نهایت مورف بین این دو شکل را به شرح زیر تحریک کنید:

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()
)

بی نهایت بین یک مربع و یک مثلث گرد
شکل 9 . بی نهایت بین یک مربع و یک مثلث گرد.

از چند ضلعی به عنوان کلیپ استفاده کنید

استفاده از اصلاح کننده 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)
    )
}

این منجر به موارد زیر می شود:

شش ضلعی با متن "سلام آهنگسازی" در مرکز.
شکل 10 . شش ضلعی با متن "سلام آهنگسازی" در مرکز.

این ممکن است متفاوت از آنچه قبلاً ارائه می شد ، باشد ، اما امکان استفاده از سایر ویژگی ها را در آهنگسازی فراهم می کند. به عنوان مثال ، از این تکنیک می توان برای کلیپ کردن یک تصویر و استفاده از سایه ای در اطراف منطقه قطع شده استفاده کرد:

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)

    )
}

سگ در شش ضلعی با سایه ای که در اطراف لبه ها اعمال می شود
شکل 11 . شکل سفارشی به عنوان کلیپ اعمال می شود.

دکمه MORPH روی کلیک

می توانید از کتابخانه 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))
}

این منجر به انیمیشن زیر می شود که جعبه ضربه خورده است:

مورف به عنوان کلیک بین دو شکل اعمال می شود
شکل 12 . مورف به عنوان کلیک بین دو شکل اعمال شد.

شکل دادن به شکل بی نهایت

برای بی پایان شکل دادن به شکل مورف ، از 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)
        )
    }
}

این کد نتیجه سرگرم کننده زیر را ارائه می دهد:

شکل قلب
شکل 13 . عکس پروفایل که توسط یک شکل چرخشی چرخان بریده شده است.

چند ضلعی های سفارشی

اگر اشکال ایجاد شده از چند ضلعی معمولی مورد استفاده شما را پوشش نمی دهد ، می توانید با لیستی از رئوس ها شکل سفارشی تری ایجاد کنید. به عنوان مثال ، شما ممکن است بخواهید شکل قلبی مانند این ایجاد کنید:

شکل قلب
شکل 14 . شکل قلب

شما می توانید با استفاده از اضافه بار RoundedPolygon که یک آرایه شناور از مختصات x ، y را می گیرد ، رئوس های این شکل را مشخص کنید.

برای تجزیه چند ضلعی قلب ، توجه داشته باشید که سیستم مختصات قطبی برای مشخص کردن نقاط ، این کار را آسانتر از استفاده از سیستم مختصات دکارتی (X ، Y) ، جایی که در سمت راست شروع می شود ، و در جهت عقربه های ساعت با 270° در حرکت می کند. موقعیت ساعت 12:

شکل قلب
شکل 15 شکل قلب با مختصات.

اکنون با مشخص کردن زاویه (𝜭) و شعاع از مرکز در هر نقطه می توان شکل را به روشی ساده تر تعریف کرد:

شکل قلب
شکل 16 . شکل قلب با مختصات ، بدون گرد.

اکنون رئوس ها می توانند ایجاد و به عملکرد 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)
)

این منجر به قلب صورتی می شود:

شکل قلب
شکل 17 نتیجه شکل قلب.

اگر اشکال قبلی مورد استفاده شما را پوشش نمی دهد ، استفاده از کلاس Path را برای ترسیم یک شکل سفارشی یا بارگیری یک فایل ImageVector از دیسک در نظر بگیرید. کتابخانه graphics-shapes برای استفاده برای اشکال دلخواه در نظر گرفته نشده است ، اما به طور خاص به منظور ساده سازی ایجاد چند ضلعی های گرد و انیمیشن های مورف بین آنها است.

منابع اضافی

برای اطلاعات بیشتر و مثال ، به منابع زیر مراجعه کنید: