Oluşturmada kullanılan şekiller

Oluştur özelliğini kullanarak poligonlardan şekiller oluşturabilirsiniz. Örneğin, aşağıdaki şekilleri oluşturabilirsiniz:

Çizim alanının ortasındaki mavi altıgen
Şekil 1. graphics-shapes kitaplığıyla oluşturabileceğiniz farklı şekillere örnekler

Oluşturma'da özel yuvarlatılmış bir poligon oluşturmak için app/build.gradle dosyanıza graphics-shapes bağımlılığını ekleyin:

implementation "androidx.graphics:graphics-shapes:1.0.0-rc01"

Bu kitaplık, poligonlardan oluşan şekiller oluşturmanıza olanak tanır. Poligonal şekiller yalnızca düz kenarlara ve keskin köşelere sahip olsa da bu şekiller isteğe bağlı yuvarlatılmış köşelere izin verir. İki farklı şekil arasında geçiş yapmayı kolaylaştırır. İsteğe bağlı şekiller arasında dönüşüm yapmak zordur ve tasarım aşamasında bir sorun olarak ortaya çıkar. Ancak bu kitaplık, benzer poligonal yapılara sahip bu şekiller arasında geçiş yaparak işlemi basitleştirir.

Poligon oluşturma

Aşağıdaki snippet, çizim alanının ortasında 6 nokta bulunan temel bir poligon şekli oluşturur:

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

Çizim alanının ortasındaki mavi altıgen
Şekil 2. Çizim alanının ortasındaki mavi altıgen.

Bu örnekte kitaplık, istenen şekli temsil eden geometriyi barındıran bir RoundedPolygon oluşturur. Bu şekli bir Oluştur uygulamasında çizmek için, şekli Oluştur'un çizebileceği bir forma dönüştürmek üzere Path nesnesi almanız gerekir.

Bir poligonun köşelerini yuvarlama

Bir poligonun köşelerini yuvarlamak için CornerRounding parametresini kullanın. Bu işlem radius ve smoothing olmak üzere iki parametre alır. Her yuvarlatılmış köşe, 1-3 kübik eğriden oluşur. Bu eğrilerin ortasında dairesel bir yay şekli bulunurken iki yan ("yan yana") eğri, şeklin kenarından merkez eğriye geçiş yapar.

Yarıçap

radius, bir köşeyi yuvarlatmak için kullanılan dairenin yarıçapıdır.

Örneğin, aşağıdaki yuvarlatılmış köşeli üçgen aşağıdaki gibi oluşturulur:

Yuvarlatılmış köşeli üçgen
Şekil 3. Yuvarlatılmış köşeli üçgen.
Yuvarlama yarıçapı r, yuvarlatılmış köşelerin dairesel yuvarlama boyutunu belirler.
Şekil 4. Yuvarlama yarıçapı r, yuvarlatılmış köşelerin dairesel yuvarlama boyutunu belirler.

Yumuşatma

Yumuşatma, köşenin dairesel yuvarlama kısmından kenara geçişin ne kadar süreceğini belirleyen bir faktördür. 0 olan yumuşatma faktörü (yumuşatılmamış, CornerRounding için varsayılan değer), köşelerin tamamen dairesel yuvarlanmasına neden olur. Sıfır olmayan bir yumuşatma faktörü (maksimum 1,0'a kadar), köşenin üç ayrı eğriyle yuvarlanmasıyla sonuçlanır.

0 olan bir yumuşatma faktörü (yumuşatılmamış), önceki örnekte olduğu gibi köşe etrafında belirtilen yuvarlama yarıçapıyla bir daireyi izleyen tek bir kübik eğri oluşturur.
Şekil 5. 0 olan bir yumuşatma faktörü (yumuşatılmamış), önceki örnekte olduğu gibi köşe etrafında belirtilen yuvarlama yarıçapıyla bir çemberi izleyen tek bir kübik eğri oluşturur.
Sıfır olmayan bir yumuşatma faktörü, köşeyi yuvarlatmak için üç kübik eğri oluşturur: iç dairesel eğri (önceki gibi) ve iç eğri ile poligon kenarları arasında geçiş yapan iki yan eğri.
Şekil 6. Sıfır olmayan bir yumuşatma faktörü, tepe noktasını yuvarlamak için üç kübik eğri üretir: iç dairesel eğri (önceki gibi) ve iç eğri ile poligon kenarları arasında geçiş yapan iki bitişik eğri.

Örneğin, aşağıdaki snippet'te pürüzsüzleştirmenin 0 ve 1 olarak ayarlanmasıyla ilgili küçük fark gösterilmektedir:

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

Yumuşatma parametresindeki farkı gösteren iki siyah üçgen.
Şekil 7. Yumuşatma parametresindeki farkı gösteren iki siyah üçgen.

Boyut ve konum

Varsayılan olarak, merkezin (0, 0) çevresinde 1 yarıçapında bir şekil oluşturulur. Bu yarıçap, şeklin temel aldığı poligonun merkez ile dış köşeleri arasındaki mesafeyi temsil eder. Yuvarlatılmış köşeler merkeze, yuvarlatılan köşelerden daha yakın olacağından köşeler yuvarlatıldığında daha küçük bir şekil elde edildiğini unutmayın. Bir poligonun boyutunu ayarlamak için radius değerini değiştirin. Konumu ayarlamak için poligonun centerX veya centerY değerini değiştirin. Alternatif olarak, DrawScope#translate() gibi standart DrawScope dönüşüm işlevlerini kullanarak nesnenin boyutunu, konumunu ve dönüşünü değiştirmek için nesneyi dönüştürebilirsiniz.

Şekilleri dönüştürme

Morph nesnesi, iki poligonal şekil arasındaki animasyonu temsil eden yeni bir şekildir. İki şekil arasında geçiş yapmak için iki RoundedPolygons ve bu iki şekli alan bir Morph nesnesi oluşturun. Başlangıç ve bitiş şekilleri arasındaki bir şekli hesaplamak için sıfır ile bir arasında progress değeri sağlayarak başlangıç (0) ve bitiş (1) şekilleri arasındaki biçimini belirleyin:

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

Yukarıdaki örnekte, ilerleme tam olarak iki şeklin (yuvarlatılmış üçgen ve kare) ortasındadır ve aşağıdaki sonucu verir:

Yuvarlak bir üçgen ile kare arasındaki yolun% 50'si
Şekil 8. Yuvarlak bir üçgen ile kare arasındaki yolun% 50'si.

Çoğu durumda, şekil değiştirme işlemi yalnızca statik bir oluşturma işlemi değil, animasyon kapsamında yapılır. Bu iki değer arasında animasyon oluşturmak için ilerleme değerini zaman içinde değiştirmek üzere standart Compose'daki animasyon API'lerini kullanabilirsiniz. Örneğin, bu iki şekil arasında dönüşümü aşağıdaki gibi sonsuz olarak animasyonlu hale getirebilirsiniz:

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

Kare ile yuvarlatılmış bir üçgen arasında sonsuz şekilde şekillenme
Şekil 9. Kare ile yuvarlak üçgen arasında sonsuz şekil değiştirme

Poligonu klip olarak kullanma

Bir composable'ın oluşturulma şeklini değiştirmek ve kırpma alanının etrafında çizilen gölgelerden yararlanmak için Compose'da clip değiştirici yaygın olarak kullanılır:

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

Ardından, poligonu aşağıdaki snippet'te gösterildiği gibi klip olarak kullanabilirsiniz:

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

Bu durum aşağıdakilerle sonuçlanır:

Merkezinde "hello compose" metninin yer aldığı altıgen.
Şekil 10. Ortasında "Merhaba Compose" yazan altıgen.

Bu, daha önce oluşturulanlardan çok farklı görünmeyebilir ancak Oluştur'daki diğer özelliklerden yararlanmanıza olanak tanır. Örneğin, bu teknik bir resmi kırpmak ve kırpılan bölgenin etrafına gölge uygulamak için kullanılabilir:

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)

    )
}

Kenarlarına gölge uygulanmış altıgen içinde köpek
Şekil 11. Özel şekil, klip olarak uygulandı.

Tıklandığında dönüşüm düğmesi

Basıldığında iki şekil arasında dönüşüm yapan bir düğme oluşturmak için graphics-shape kitaplığını kullanabilirsiniz. Öncelikle, Shape'u genişleten bir MorphPolygonShape oluşturun, uygun şekilde sığdıracak şekilde ölçeklendirin ve çevirin. Şeklin animasyonlu olabilmesi için ilerlemenin iletildiğini unutmayın:

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

Bu şekil dönüşümünü kullanmak için shapeA ve shapeB olmak üzere iki poligon oluşturun. Morph oluşturun ve hatırlayın. Ardından, animasyon için itici güç olarak interactionSource düğmesini kullanarak, düğmeye klip ana hattı olarak dönüşümü uygulayın:

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

Bu durumda, kutuya dokunulduğunda aşağıdaki animasyon gösterilir:

İki şekil arasında tıklama olarak uygulanan dönüşüm
Şekil 12. İki şekil arasında tıklama olarak uygulanan Morph.

Şekil dönüşümünü sonsuz olarak canlandırma

Bir dönüşüm şeklini sonsuz olarak canlandırmak için rememberInfiniteTransition simgesini kullanın. Aşağıda, zaman içinde şekli sonsuza kadar değişen (ve dönen) bir profil resmi örneği verilmiştir. Bu yaklaşımda, yukarıda gösterilen MorphPolygonShape için küçük bir ayarlama yapılır:

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

Bu kod aşağıdaki eğlenceli sonucu verir:

Kalp şekli yapan
Şekil 13. Dönen eğimli bir şekille kırpılan profil resmi.

Özel poligonlar

Normal poligonlardan oluşturulan şekiller kullanım alanınızı kapsamıyorsa köşe listesi içeren daha özel bir şekil oluşturabilirsiniz. Örneğin, aşağıdaki gibi bir kalp şekli oluşturmak isteyebilirsiniz:

Kalp şekli yapan
Şekil 14. Kalp şekli.

x, y koordinatlarından oluşan bir kayan nokta dizisi alan RoundedPolygon aşırı yüklemeyi kullanarak bu şeklin köşelerini tek tek belirtebilirsiniz.

Kalp poligonunu parçalara ayırmak için noktaları belirtme amaçlı kutupsal koordinat sisteminin, bu işlemi Kartezyen (x, y) koordinat sistemini kullanmaktan daha kolay hale getirdiğini unutmayın. Bu sistemde sağ taraftan başlar ve saat 12 konumunda 270° ile saat yönünde ilerler:

Kalp şekli yapan
Şekil 15. Koordinat içeren kalp şekli.

Artık şekil, her noktadaki açıyı (𝜭) ve merkeze olan yarıçapı belirterek daha kolay bir şekilde tanımlanabilir:

Kalp şekli yapan
Şekil 16. Yuvarlama olmadan koordinatlı kalp şekli.

Köşe noktaları artık oluşturulup RoundedPolygon işlevine aktarılabilir:

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,
    )
}

Köşe noktalarının, bu radialToCartesian işlevi kullanılarak Kartezyen koordinatlara çevrilmesi gerekir:

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

Önceki kodda kalbin ham köşeleri verilmiştir ancak seçilen kalp şeklini elde etmek için belirli köşeleri yuvarlamanız gerekir. 90° ve 270° köşelerinde yuvarlama yoktur ancak diğer köşelerde yuvarlama vardır. Ayrı köşeler için özel yuvarlama elde etmek üzere perVertexRounding parametresini kullanın:

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

Bu işlem sonucunda pembe kalp elde edilir:

Kalp şekli yapan
Şekil 17. Kalp şekli sonucu.

Önceki şekiller kullanım alanınızı kapsamıyorsa Path sınıfını kullanarak özel şekil çizebilir veya diskten bir ImageVector dosyası yükleyebilirsiniz. graphics-shapes kitaplığı, rastgele şekiller için kullanılmak üzere tasarlanmamıştır. Bunun yerine, yuvarlatılmış poligonların ve bunlar arasındaki dönüşüm animasyonlarının oluşturulmasını basitleştirmek için tasarlanmıştır.

Ek kaynaklar

Daha fazla bilgi ve örnek için aşağıdaki kaynakları inceleyin: