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

با 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 برای استفاده برای اشکال دلخواه در نظر گرفته نشده است، بلکه به طور خاص برای ساده سازی ایجاد چند ضلعی های گرد و انیمیشن های مورف بین آنها طراحی شده است.

منابع اضافی

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