Oluşturmada kullanılan şekiller

Oluştur işleviyle, çokgenlerden oluşan şekiller oluşturabilirsiniz. Örneğin, aşağıdaki türde şekiller oluşturabilirsiniz:

Çizim alanının ortasındaki mavi altıgen
Şekil 1. Grafik şekilli kitaplıkla yapabileceğiniz farklı şekil örnekleri

Oluşturma aracında özel yuvarlak bir çokgen oluşturmak için app/build.gradle öğenize graphics-shapes bağımlılığını ekleyin:

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

Bu kitaplık, çokgenlerden yapılmış şekiller oluşturmanıza olanak tanır. Poligonal şekillerin yalnızca düz kenarları ve keskin köşeleri olsa da bu şekiller isteğe bağlı olarak yuvarlatılmış köşelere olanak tanır. İki farklı şekil arasında geçiş yapmayı kolaylaştırır. Rastgele şekiller arasında dönüşüm işlemi yapmak zordur ve bu durum genellikle tasarım zamanıyla ilgili bir sorundur. Ancak bu kitaplık, benzer poligonal yapılara sahip bu şekiller arasında geçiş yaparak bunu basit hale getiriyor.

Poligonlar oluşturun

Aşağıdaki snippet, çizim alanının ortasında 6 noktası olan temel bir çokgen ş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. Bir Compose uygulamasında bu şekli çizmek için bundan bir Path nesnesi almanız ve şekli Compose'un nasıl çizileceğini bilen bir forma dönüştürmesini sağlamanız gerekir.

Çokgenin köşelerini yuvarlama

Bir poligonun köşelerini yuvarlamak için CornerRounding parametresini kullanın. Bunun için radius ve smoothing olmak üzere iki parametre kullanılır. Yuvarlatılmış her köşe, merkezi dairesel bir yay şekline sahip olan ve iki tarafı ("kenar") eğrileri, şeklin kenarından merkez eğrisine geçiş yapan 1-3 kübik eğriden oluşur.

Yarıçap

radius, bir tepe noktasını yuvarlamak için kullanılan dairenin yarıçapıdır.

Örneğin, aşağıdaki yuvarlatılmış köşe üçgeni şöyle yapılır:

Köşeleri yuvarlanmış üçgen
Şekil 3. Köşeleri yuvarlanmış üç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 doğru gitmenin ne kadar süreceğini belirleyen bir faktördür. Yumuşatma faktörü 0 (yumuşatılmamış, CornerRounding için varsayılan değer) tamamen yuvarlak köşelerin yuvarlanmasına neden olur. Sıfır olmayan bir düzgünleştirme faktörü (en fazla 1,0), köşenin üç ayrı eğriyle yuvarlanmasıyla sonuçlanır.

Yumuşatma faktörü 0 (düzgünleştirilmemiş), önceki örnekte olduğu gibi, köşesi belirtilen yuvarlama yarıçapıyla köşeyi takip eden bir daireyi takip eden tek bir kübik eğri oluşturur.
Şekil 5. Yumuşatma faktörü 0 (düzgünleştirilmemiş), önceki örnekte olduğu gibi, köşesi belirtilen yuvarlama yarıçapıyla köşeyi takip eden bir daireyi izleyen tek bir kübik eğri oluşturur.
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) artı 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 oluşturur: iç dairesel eğri (önceden olduğu gibi) artı iç eğri ile poligon kenarları arasında geçiş yapan iki yan eğri.

Örneğin, aşağıdaki snippet'te yumuşatma 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)
)

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, merkezden (0, 0) 1 yarıçapında bir şekil oluşturulur. Bu yarıçap, şeklin temel aldığı poligonun merkezi ile dış köşeleri arasındaki mesafeyi temsil eder. Yuvarlatılmış köşeler, köşelere yuvarlanan köşelere kıyasla daha yakın olacağından köşelerin yuvarlanmasıyla daha küçük bir şekil elde edileceğini unutmayın. Bir poligonu boyutlandırmak için radius değerini ayarlayın. Konumu ayarlamak için çokgenin centerX veya centerY değerini değiştirin. Alternatif olarak, DrawScope#translate() gibi standart DrawScope dönüştürme işlevlerini kullanarak nesneyi boyutunu, konumunu ve dönüşünü değiştirmek için dönüştürebilirsiniz.

Şekiller üzerinde değişiklik yapma

Morph nesnesi, iki poligon şekil arasındaki animasyonu temsil eden yeni bir şekildir. İki şekil arasında geçiş yapmak için bu iki şekli alan iki RoundedPolygons ve bir Morph nesnesi oluşturun. Başlangıç ve bitiş şekilleri arasındaki bir şekli hesaplamak için sıfır ile bir arasında bir 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 iki şeklin (yuvarlatılmış üçgen ve kare) tam ortasındadır ve şu sonucu verir:

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

Çoğu senaryoda, dönüştürme işlemi yalnızca statik bir oluşturma değil, bir animasyonun parçası olarak yapılır. Bu ikisi arasında animasyon oluşturmak için standart Oluşturma bölümündeki Animasyon API'lerini kullanarak ilerleme değerini zaman içinde değiştirebilirsiniz. Örneğin, bu iki şekil arasındaki şekli aşağıda gösterildiği 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 ve yuvarlak üçgen arasında sonsuz şekil değiştirme
Şekil 9. Kare ve yuvarlak bir üçgen arasında sonsuz şekil değiştirme.

Klip olarak poligonu kullan

Bir composable'ın oluşturulma şeklini değiştirmek ve kırpma alanının çevresini çizen gölgelerden yararlanmak için, Compose'da clip değiştiricisi 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 ön bilgide 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:

Ortasında "merhaba, oluştur" yazan altıgen.
Şekil 10. Ortasında "Hello Compose" yazan bir altıgen.

Bu işlem, önceki oluşturma işleminden ç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ından gölgelendirilmiş altıgen köpek
Şekil 11. Özel şekil klip olarak uygulandı.

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

Basıldığında iki şekil arasında geçiş yapan bir düğme oluşturmak için graphics-shape kitaplığını kullanabilirsiniz. Öncelikle Shape öğesini genişleten bir MorphPolygonShape oluşturun ve bunu uygun şekilde ölçekleyip çevirin. Şeklin canlandırılabilmesi için ilerlemenin devam ettiğine dikkat edin:

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 dönüşüm şeklini kullanmak için shapeA ve shapeB olmak üzere iki poligon oluşturun. Morph öğesini oluşturun ve unutmayın. Ardından, animasyonun arkasındaki itici güç olarak basıldığında interactionSource kullanarak dönüşümü klip taslağı olarak düğmeye 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 animasyonla sonuçlanır:

İki şekil arasına tıklama olarak uygulanan dönüşüm şekli
Şekil 12. Morph, iki şekil arasına tıklama olarak uygulandı.

Şeklin dönüşümünü sonsuza kadar canlandırma

Bir dönüşüm şeklini sonsuza kadar canlandırmak için rememberInfiniteTransition özelliğini 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 düzenleme kullanı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, şu eğlenceli sonucu verir:

Kalp şekli yapan
Şekil 13. Dönen ve yuvarlak bir şekille kırpılan profil resmi

Özel poligonlar

Normal poligonlardan oluşturulan şekiller kullanım alanınızı kapsamazsa köşe noktalarının listesiyle daha özel bir şekil oluşturabilirsiniz. Örneğin, şöyle bir kalp şekli oluşturabilirsiniz:

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

x ve y koordinatlarından oluşan bir kayan diziyi alan RoundedPolygon aşırı yükünü kullanarak, bu şeklin bağımsız köşelerini belirtebilirsiniz.

Kalp poligonunu ayırmak için kutupsal koordinat sisteminin, noktaların belirtilmesine yönelik kutupsal koordinat sisteminin bunu, sağ taraftan başlayıp saat yönünde 270° saat 12 konumunda devam ettiği kartezyen (x,y) koordinat sistemini kullanmaktan daha kolay hale getirdiğine dikkat edin:

Kalp şekli yapan
Şekil 15. Koordinatlı kalp şekli.

Şekil artık her bir noktanın merkezinden açıyı () ve yarıçapı belirterek daha kolay bir şekilde tanımlanabilir:

Kalp şekli yapan
Şekil 16. Koordinatlı, yuvarlanmamış 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 bu radialToCartesian işlevi kullanılarak kartezyen koordinatlarına 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))

Önceki kod, kalp şeklinin işlenmemiş köşelerini verir ancak seçilen kalp şeklini almak için belirli köşeleri yuvarlamanız gerekir. 90° ve 270° konumlarında yuvarlama yok, ancak diğer köşelerde yuvarlama var. Bağımsız köşelerde özel yuvarlama sağlamak 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)
)

Bunun sonucunda pembe kalp ortaya çıkar:

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

Yukarıdaki şekiller kullanım alanınızı kapsamıyorsa özel bir şekil çizmek için Path sınıfını kullanmayı veya diskten bir ImageVector dosyası yüklemeyi düşünün. graphics-shapes kitaplığı rastgele şekiller için kullanılmak üzere tasarlanmamıştır. Ancak özellikle yuvarlak poligonların ve bunlar arasında geçiş animasyonlarının oluşturulmasını kolaylaştırmak için tasarlanmıştır.

Ek kaynaklar

Daha fazla bilgi ve örnek için aşağıdaki kaynaklara bakın: