شکل‌ها در Compose

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

شش‌ضلعی آبی در مرکز ناحیه طراحی
شکل ۱. مثال‌هایی از شکل‌های مختلفی که می‌توانید با کتابخانه graphics-shapes
بسازید

برای ایجاد چندضلعی گرد سفارشی در «نگارش»، وابستگی graphics-shapes را به app/build.gradle اضافه کنید:

implementation "androidx.graphics:graphics-shapes:1.0.1"

این کتابخانه به شما امکان می‌دهد شکل‌هایی بسازید که از چندضلعی‌ها ساخته شده‌اند. درحالی‌که شکل‌های چندضلعی فقط لبه‌های صاف و گوشه‌های تیز دارند، این شکل‌ها امکان داشتن گوشه‌های گرد اختیاری را فراهم می‌کنند. این ویژگی تغییر شکل بین دو شکل مختلف را ساده می‌کند. ریخت‌گردانی بین شکل‌های اختیاری دشوار است و معمولاً یک مشکل زمان طراحی است. اما این کتابخانه با تبدیل بین این اشکال با ساختارهای چندضلعی مشابه، این کار را ساده می‌کند.

ایجاد چندضلعی‌ها

تکه کد زیر یک شکل چندضلعی پایه با ۶ نقطه در مرکز ناحیه طراحی ایجاد می‌کند:

Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygon = RoundedPolygon(
                numVertices = 6,
                radius = size.minDimension / 2,
                centerX = size.width / 2,
                centerY = size.height / 2
            )
            val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
            onDrawBehind {
                drawPath(roundedPolygonPath, color = Color.Blue)
            }
        }
        .fillMaxSize()
)

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

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

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

برای گرد کردن گوشه‌های چندضلعی، از پارامتر CornerRounding استفاده کنید. این تابع دو پارامتر، radius و smoothing، می‌گیرد. هر گوشه گرد از ۱ تا ۳ منحنی مکعبی تشکیل شده است که مرکز آن شکل قوس دایره‌ای دارد و دو منحنی کناری («جانبی») از لبه شکل به منحنی مرکزی منتقل می‌شوند.

شعاع

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

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

مثلث با گوشه‌های گرد
شکل ۳. مثلث با گوشه‌های گرد.
شعاع گرد کردن r اندازه گرد کردن دایره‌ای گوشه‌های گرد را تعیین می‌کند
شکل ۴. شعاع گرد کردن r اندازه گرد کردن دایره‌ای گوشه‌های گرد را تعیین می‌کند.

صاف کردن

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

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

برای مثال، گلچین زیر تفاوت ظریف تنظیم صاف کردن روی ۰ درمقایسه با ۱ را نشان می‌دهد:

Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygon = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2,
                centerX = size.width / 2,
                centerY = size.height / 2,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
            onDrawBehind {
                drawPath(roundedPolygonPath, color = Color.Black)
            }
        }
        .size(100.dp)
)

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

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

به‌طور پیش‌فرض، شکلی با شعاع 1 در اطراف مرکز (0, 0) ایجاد می‌شود. این شعاع نشان‌دهنده فاصله بین مرکز و رأس‌های خارجی چندضلعی است که شکل براساس آن ایجاد شده است. توجه داشته باشید که گرد کردن گوشه‌ها منجر به شکل کوچک‌تری می‌شود زیرا گوشه‌های گردشده به مرکز نزدیک‌تر از رأس‌های گردشده خواهند بود. برای اندازه‌گیری چندضلعی، مقدار radius را تنظیم کنید. برای تنظیم موقعیت، centerX یا centerY چندضلعی را تغییر دهید. یا اینکه بااستفاده از توابع تبدیل استاندارد DrawScope مانند DrawScope#translate()، شیء را تبدیل کنید تا اندازه، موقعیت، و چرخش آن تغییر کند.

تغییر تدریجی شکل‌ها

شیء Morph شکل جدیدی است که پویانمایی بین دو شکل چندضلعی را نشان می‌دهد. برای تبدیل بین دو شکل، دو RoundedPolygons و یک Morph شیء که این دو شکل را می‌گیرد ایجاد کنید. برای محاسبه شکل بین شکل‌های شروع و پایان، مقدار progress بین صفر و یک ارائه دهید تا شکل آن بین شکل‌های شروع (۰) و پایان (۱) تعیین شود:

Box(
    modifier = Modifier
        .drawWithCache {
            val triangle = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val square = RoundedPolygon(
                numVertices = 4,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f
            )

            val morph = Morph(start = triangle, end = square)
            val morphPath = morph
                .toPath(progress = 0.5f).asComposePath()

            onDrawBehind {
                drawPath(morphPath, color = Color.Black)
            }
        }
        .fillMaxSize()
)

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

‫٪۵۰ از مسیر بین مثلث گرد و مربع
شکل ۸. ‫٪۵۰ از مسیر بین مثلث گرد و مربع.

در اکثر موارد، تغییر شکل به‌عنوان بخشی از یک پویانمایی انجام می‌شود، نه فقط یک پردازش ثابت. برای پویانمایی بین این دو، می‌توانید از میاناهای برنامه‌سازی کاربردی پویانمایی استاندارد در Compose برای تغییر مقدار پیشرفت درطول زمان استفاده کنید. برای مثال، می‌توانید تغییر شکل بین این دو شکل را به‌صورت بی‌نهایت پویانمایی کنید: به‌این صورت:

val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation")
val morphProgress = infiniteAnimation.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        tween(500),
        repeatMode = RepeatMode.Reverse
    ),
    label = "morph"
)
Box(
    modifier = Modifier
        .drawWithCache {
            val triangle = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val square = RoundedPolygon(
                numVertices = 4,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f
            )

            val morph = Morph(start = triangle, end = square)
            val morphPath = morph
                .toPath(progress = morphProgress.value)
                .asComposePath()

            onDrawBehind {
                drawPath(morphPath, color = Color.Black)
            }
        }
        .fillMaxSize()
)

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

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

استفاده از clip اصلاح‌کننده در «ترکیب» برای تغییر نحوه پرداز کردن عنصر ترکیبی و بهره‌گیری از سایه‌هایی که در اطراف ناحیه برش رسم می‌شوند رایج است:

fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
class RoundedPolygonShape(
    private val polygon: RoundedPolygon,
    private var matrix: Matrix = Matrix()
) : Shape {
    private var path = Path()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        path.rewind()
        path = polygon.toPath().asComposePath()
        matrix.reset()
        val bounds = polygon.getBounds()
        val maxDimension = max(bounds.width, bounds.height)
        matrix.scale(size.width / maxDimension, size.height / maxDimension)
        matrix.translate(-bounds.left, -bounds.top)

        path.transform(matrix)
        return Outline.Generic(path)
    }
}

سپس می‌توانید از چندضلعی به‌عنوان کلیپ استفاده کنید، همان‌طور که در گزیده زیر نشان داده شده است:

val hexagon = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val clip = remember(hexagon) {
    RoundedPolygonShape(polygon = hexagon)
}
Box(
    modifier = Modifier
        .clip(clip)
        .background(MaterialTheme.colorScheme.secondary)
        .size(200.dp)
) {
    Text(
        "Hello Compose",
        color = MaterialTheme.colorScheme.onSecondary,
        modifier = Modifier.align(Alignment.Center)
    )
}

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

شش‌ضلعی با نوشتار `hello compose` در مرکز.
شکل ۱۰. شش‌ضلعی با نوشتار «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)

    )
}

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

دکمه تغییر شکل با کلیک

می‌توانید از کتابخانه graphics-shape برای ایجاد دکمه‌ای استفاده کنید که با فشار دادن بین دو شکل تغییر می‌کند. ابتدا، MorphPolygonShape را ایجاد کنید که Shape گسترش می‌یابد، و آن را مقیاس‌بندی و ترجمه کنید تا به‌درستی جا بیفتد. توجه داشته باشید که پیشرفت به‌صورت زیر ارسال می‌شود تا شکل بتواند پویانمایی شود:

class MorphPolygonShape(
    private val morph: Morph,
    private val percentage: Float
) : Shape {

    private val matrix = Matrix()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
        // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
        matrix.scale(size.width / 2f, size.height / 2f)
        matrix.translate(1f, 1f)

        val path = morph.toPath(progress = percentage).asComposePath()
        path.transform(matrix)
        return Outline.Generic(path)
    }
}

برای استفاده از این شکل تغییرپذیر، دو چندضلعی، shapeA و shapeB، ایجاد کنید. ‫Morph را ایجاد و به‌خاطر بسپارید. سپس، تغییر شکل را به‌عنوان طرح کلی کلیپ به دکمه اعمال کنید، بااستفاده از interactionSource در فشار به‌عنوان نیروی محرکه پویانمایی:

val shapeA = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val shapeB = remember {
    RoundedPolygon.star(
        6,
        rounding = CornerRounding(0.1f)
    )
}
val morph = remember {
    Morph(shapeA, shapeB)
}
val interactionSource = remember {
    MutableInteractionSource()
}
val isPressed by interactionSource.collectIsPressedAsState()
val animatedProgress = animateFloatAsState(
    targetValue = if (isPressed) 1f else 0f,
    label = "progress",
    animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium)
)
Box(
    modifier = Modifier
        .size(200.dp)
        .padding(8.dp)
        .clip(MorphPolygonShape(morph, animatedProgress.value))
        .background(Color(0xFF80DEEA))
        .size(200.dp)
        .clickable(interactionSource = interactionSource, indication = null) {
        }
) {
    Text("Hello", modifier = Modifier.align(Alignment.Center))
}

این کار منجر به پویانمایی زیر هنگام ضربه زدن روی چارگوش می‌شود:

«ریخت‌گردانی» به‌عنوان کلیک بین دو شکل اعمال شد
شکل ۱۲. «ریخت‌گردانی» به‌عنوان کلیک بین دو شکل اعمال شد.

پویانمایی تغییر شکل بی‌نهایت

برای پویانمایی بی‌نهایت شکل تغییریافته، از rememberInfiniteTransition استفاده کنید. در زیر نمونه‌ای از عکس نمایه را می‌بینید که شکل آن (و چرخش آن) به‌طور نامحدود درطول زمان تغییر می‌کند. این رویکرد از اصلاح کوچکی برای MorphPolygonShape نشان‌داده‌شده در بالا استفاده می‌کند:

class CustomRotatingMorphShape(
    private val morph: Morph,
    private val percentage: Float,
    private val rotation: Float
) : Shape {

    private val matrix = Matrix()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
        // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
        matrix.scale(size.width / 2f, size.height / 2f)
        matrix.translate(1f, 1f)
        matrix.rotateZ(rotation)

        val path = morph.toPath(progress = percentage).asComposePath()
        path.transform(matrix)

        return Outline.Generic(path)
    }
}

@Preview
@Composable
private fun RotatingScallopedProfilePic() {
    val shapeA = remember {
        RoundedPolygon(
            12,
            rounding = CornerRounding(0.2f)
        )
    }
    val shapeB = remember {
        RoundedPolygon.star(
            12,
            rounding = CornerRounding(0.2f)
        )
    }
    val morph = remember {
        Morph(shapeA, shapeB)
    }
    val infiniteTransition = rememberInfiniteTransition("infinite outline movement")
    val animatedProgress = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "animatedMorphProgress"
    )
    val animatedRotation = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            tween(6000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "animatedMorphProgress"
    )
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Image(
            painter = painterResource(id = R.drawable.dog),
            contentDescription = "Dog",
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .clip(
                    CustomRotatingMorphShape(
                        morph,
                        animatedProgress.value,
                        animatedRotation.value
                    )
                )
                .size(200.dp)
        )
    }
}

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

شکل قلب
شکل ۱۳. عکس نمایه‌ای که با شکل دندانه‌دار چرخانی برش خورده است.

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

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

شکل قلب
شکل ۱۴. شکل قلب.

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

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

شکل قلب
شکل ۱۵. قلب با مختصات.

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

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

اکنون می‌توان رأس‌ها را ایجاد کرد و به تابع RoundedPolygon منتقل کرد:

val vertices = remember {
    val radius = 1f
    val radiusSides = 0.8f
    val innerRadius = .1f
    floatArrayOf(
        radialToCartesian(radiusSides, 0f.toRadians()).x,
        radialToCartesian(radiusSides, 0f.toRadians()).y,
        radialToCartesian(radius, 90f.toRadians()).x,
        radialToCartesian(radius, 90f.toRadians()).y,
        radialToCartesian(radiusSides, 180f.toRadians()).x,
        radialToCartesian(radiusSides, 180f.toRadians()).y,
        radialToCartesian(radius, 250f.toRadians()).x,
        radialToCartesian(radius, 250f.toRadians()).y,
        radialToCartesian(innerRadius, 270f.toRadians()).x,
        radialToCartesian(innerRadius, 270f.toRadians()).y,
        radialToCartesian(radius, 290f.toRadians()).x,
        radialToCartesian(radius, 290f.toRadians()).y,
    )
}

رأس‌ها باید بااستفاده از این تابع radialToCartesian به مختصات دکارتی ترجمه شوند:

internal fun Float.toRadians() = this * PI.toFloat() / 180f

internal val PointZero = PointF(0f, 0f)
internal fun radialToCartesian(
    radius: Float,
    angleRadians: Float,
    center: PointF = PointZero
) = directionVectorPointF(angleRadians) * radius + center

internal fun directionVectorPointF(angleRadians: Float) =
    PointF(cos(angleRadians), sin(angleRadians))

کد قبلی رئوس خام قلب را به شما می‌دهد، اما برای به‌دست آوردن شکل قلب انتخاب‌شده باید گوشه‌های خاصی را گرد کنید. گوشه‌های 90° و 270° گرد نیستند، اما گوشه‌های دیگر گرد هستند. برای گرد کردن سفارشی گوشه‌های جداگانه، از پارامتر perVertexRounding استفاده کنید:

val rounding = remember {
    val roundingNormal = 0.6f
    val roundingNone = 0f
    listOf(
        CornerRounding(roundingNormal),
        CornerRounding(roundingNone),
        CornerRounding(roundingNormal),
        CornerRounding(roundingNormal),
        CornerRounding(roundingNone),
        CornerRounding(roundingNormal),
    )
}

val polygon = remember(vertices, rounding) {
    RoundedPolygon(
        vertices = vertices,
        perVertexRounding = rounding
    )
}
Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygonPath = polygon.toPath().asComposePath()
            onDrawBehind {
                scale(size.width * 0.5f, size.width * 0.5f) {
                    translate(size.width * 0.5f, size.height * 0.5f) {
                        drawPath(roundedPolygonPath, color = Color(0xFFF15087))
                    }
                }
            }
        }
        .size(400.dp)
)

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

شکل قلب
شکل ۱۷. نتیجه به‌شکل قلب.

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

منابع بیشتر

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