Oluşturmada kullanılan şekiller

Oluşturma aracıyla çokgenlerden oluşan şekiller oluşturabilirsiniz. Örneğin, aşağıdaki şekilleri oluşturabilirsiniz:

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

Compose'da özel yuvarlak poligon oluşturmak için app/build.gradle öğenize graphics-shapes bağımlılığını ekleyin:

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

Bu kitaplık, çokgenlerden oluşan şekiller oluşturmanıza olanak tanır. Çokgen şekillerin yalnızca düz kenarları ve keskin köşeleri olsa da bu şekillerde isteğe bağlı olarak yuvarlatılmış köşeler kullanılabilir. İki farklı şekil arasında kolayca geçiş yapabilirsiniz. İstediğiniz şekiller arasında dönüştürme yapmak zordur ve genellikle tasarım zamanında sorun yaşanır. Ancak bu kitaplık, benzer çokgen yapıları olan bu şekiller arasında dönüşüm yaparak işi kolaylaştırır.

Poligon oluşturma

Aşağıdaki snippet, çizim alanının ortasında 6 noktalı 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ında mavi altıgen
Şekil 2. Çizim alanının ortasında mavi altıgen.

Bu örnekte kitaplık, istenen şekli temsil eden geometriyi içeren bir RoundedPolygon oluşturur. Bu şekli bir Compose uygulamasında çizmek için, şekli Compose'un nasıl çizeceğini bildiği bir forma dönüştürmek üzere Path nesnesi almanız gerekir.

Çokgenin köşelerini yuvarlama

Bir poligonun köşelerini yuvarlamak için CornerRounding parametresini kullanın. Bu işlev iki parametre (radius ve smoothing) alır. Her yuvarlak köşe, 1-3 kübik eğriden oluşur. Bu eğrilerin ortası dairesel yay şeklindedir. İki yan eğri ise şeklin kenarından merkez eğriye geçiş yapar.

Yarıçap

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

Örneğin, aşağıdaki yuvarlak köşeli üçgen şu şekilde oluşturulur:

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

Yumuşatma

Yumuşatma, köşenin yuvarlak kısmından kenara ulaşmanın ne kadar sürdüğünü belirleyen bir faktördür. 0 düzleştirme faktörü (düzleştirilmemiş, CornerRounding için varsayılan değer) tamamen dairesel köşe yuvarlama ile sonuçlanır. Sıfırdan farklı bir düzleştirme faktörü (en fazla 1,0) köşenin üç ayrı eğriyle yuvarlatılmasına neden olur.

0 düzleştirme faktörü (düzleştirilmemiş), önceki örnekte olduğu gibi, belirtilen yuvarlama yarıçapıyla köşeyi çevreleyen bir daireyi takip eden tek bir kübik eğri oluşturur.
5. Şekil 0 (düzeltilmemiş) düzeltme faktörü, önceki örnekte olduğu gibi, belirtilen yuvarlama yarıçapıyla köşeyi çevreleyen bir çemberi takip eden tek bir kübik eğri oluşturur.
Sıfırdan farklı bir düzleştirme faktörü, köşeyi yuvarlamak 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ırdan farklı bir yumuşatma faktörü, köşeyi yuvarlamak için üç kübik eğri oluşturur: iç dairesel eğri (önceki gibi) ve iç eğri ile çokgen kenarları arasında geçiş yapan iki yan eğri.

Örneğin, aşağıdaki snippet'te düzeltme ayarının 0 ile 1 olarak ayarlanması arasındaki 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)
)

Düzleştirme parametresindeki farkı gösteren iki siyah üçgen.
7. Şekil. Düzleştirme parametresindeki farkı gösteren iki siyah üçgen.

Boyut ve konum

Varsayılan olarak, şekil merkez (0, 0) etrafında 1 yarıçapıyla oluşturulur. Bu yarıçap, şeklin temel alındığı poligonun merkezi ile dış köşeleri arasındaki mesafeyi temsil eder. Köşeleri yuvarlamanın, yuvarlatılan köşeler yuvarlatılan köşelerden daha merkeze yakın olacağından daha küçük bir şekil oluşturduğunu unutmayın. Bir poligonun boyutunu ayarlamak için radius değerini düzenleyin. Konumu ayarlamak için poligonun centerX veya centerY değerini değiştirin. Alternatif olarak, DrawScope#translate() gibi standart DrawScope dönüştürme işlevlerini kullanarak nesnenin boyutunu, konumunu ve dönüşünü değiştirmek için dönüştürme işlemini kullanın.

Şekilleri dönüştürme

Morph nesnesi, iki çokgen ş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ında bir şekil hesaplamak için başlangıç (0) ve bitiş (1) şekilleri arasındaki formu belirlemek üzere sıfır ile bir arasında bir progress değeri sağlayın:

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 durumu iki şeklin (yuvarlak üçgen ve kare) tam ortasında olduğundan aşağıdaki sonuç elde edilir:

Yuvarlatılmış bir üçgen ile kare arasında% 50
Şekil 8. Yuvarlatılmış bir üçgen ile kare arasındaki mesafenin% 50'si.

Çoğu durumda, dönüştürme işlemi yalnızca statik bir oluşturma olarak değil, animasyonun bir parçası olarak yapılır. Bu ikisi arasında animasyon yapmak için Compose'daki standart animasyon API'lerini kullanarak zaman içinde ilerleme değerini değiştirebilirsiniz. Örneğin, bu iki şekil arasındaki dönüşümü aşağıdaki gibi sonsuza kadar canlandırabilirsiniz:

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 yuvarlak üçgen arasında sonsuz dönüşüm
Şekil 9. Kare ile yuvarlak üçgen arasında sonsuz dönüşüm.

Klip olarak poligon kullanma

Bir composable'ın nasıl oluşturulduğunu değiştirmek ve kırpma alanının etrafında çizilen gölgelerden yararlanmak için Compose'da clip değiştiricisini kullanmak yaygındı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, aşağıdaki snippet'te gösterildiği gibi poligonu 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:

Ortasında "hello compose" metninin bulunduğu altıgen.
10. Şekil. Ortasında "Hello Compose" (Merhaba Oluştur) yazan altıgen.

Bu, daha önce oluşturulanlardan çok farklı görünmeyebilir ancak Compose'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çindeki köpek
Şekil 11. Özel şekil, klip olarak uygulandı.

Tıklamada dönüştürme düğmesi

graphics-shape kitaplığını kullanarak basıldığında iki şekil arasında dönüşen bir düğme oluşturabilirsiniz. Öncelikle, Shape öğesini genişleten, uygun şekilde ölçeklendiren ve çeviren bir MorphPolygonShape oluşturun. Şeklin animasyonlu hale getirilebilmesi için ilerleme durumunun 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üştürme özelliğini kullanmak için shapeA ve shapeB olmak üzere iki çokgen oluşturun. Morph oluşturun ve hatırlayın. Ardından, animasyonun itici gücü olarak interactionSource on press'i kullanarak düğmeye klip ana hattı olarak dönüştürme efektini 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 işlem, kutuya dokunulduğunda aşağıdaki animasyonun gösterilmesine neden olur:

İki şekil arasında tıklama olarak uygulanan dönüştürme
Şekil 12. Dönüşüm, iki şekil arasında tıklama olarak uygulanır.

Şekil dönüştürme animasyonunu sonsuz kez oynatma

Dönüşüm şeklini sonsuza kadar canlandırmak için rememberInfiniteTransition simgesini kullanın. Aşağıda, zaman içinde şekli (ve yönü) sonsuza kadar değişen 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 bir tarak şekliyle kırpılmış profil resmi.

Özel poligonlar

Düzenli çokgenlerden oluşturulan şekiller kullanım alanınızı kapsamıyorsa köşe listesiyle 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.

Bu şeklin ayrı ayrı köşelerini, x ve y koordinatlarından oluşan bir kayan nokta dizisi alan RoundedPolygon aşırı yüklemesini kullanarak belirtebilirsiniz.

Kalp poligonunu parçalamak için noktaları belirtmeye yönelik kutupsal koordinat sisteminin, sağ tarafta başlayıp saat yönünde ilerleyerek 12 konumunda 270° ile devam eden kartezyen (x,y) koordinat sistemini kullanmaktan daha kolay olduğunu unutmayın:

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

Şekil artık her noktada merkezden olan açı (𝜭) ve yarıçap belirtilerek daha kolay bir şekilde tanımlanabilir:

Kalp şekli yapan
Şekil 16. Yuvarlama olmadan koordinatlarla kalp şekli.

Köşeler artık oluşturulabilir ve 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öşelerin, şu radialToCartesian işlevi kullanılarak Kartezyen koordinatlara dönüştürülmesi 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))

Yukarıdaki kod, kalp için ham köşe noktalarını verir ancak seçilen kalp şeklini elde etmek için belirli köşeleri yuvarlamanız gerekir. 90° ve 270° konumlarındaki köşeler yuvarlatılmamış, diğer köşeler ise yuvarlatılmıştır. Köşeleri ayrı ayrı yuvarlatmak için 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 durumda pembe kalp gösterilir:

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

Yukarıdaki şekiller kullanım alanınızı kapsamıyorsa Path sınıfını kullanarak özel bir şekil çizmeyi veya diskten bir ImageVector dosyası yüklemeyi deneyin. graphics-shapes kitaplığı, rastgele şekiller için kullanılmak üzere tasarlanmamıştır. Özellikle yuvarlatılmış çokgenlerin oluşturulmasını ve bunlar arasında şekil değiştirme animasyonlarının yapılmasını kolaylaştırmak için tasarlanmıştır.

Ek kaynaklar

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