Compose ile çokgenlerden oluşan şekiller oluşturabilirsiniz. Örneğin, aşağıdaki şekilleri oluşturabilirsiniz:
Compose'da özel yuvarlak poligon oluşturmak için app/build.gradle dosyanıza 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 şekillerde yalnızca düz kenarlar ve keskin köşeler varken bu şekillerde isteğe bağlı olarak yuvarlatılmış köşeler kullanılabilir. İki farklı şekil arasında geçiş yapmayı kolaylaştırır. Rastgele şekiller arasında geçiş yapmak zordur ve genellikle tasarım aşamasında sorunlara yol açar. Ancak bu kitaplık, benzer çokgen yapılara sahip bu şekiller arasında geçiş yaparak işi kolaylaştırır.
Poligon oluşturma
Aşağıdaki snippet, çizim alanının ortasında 6 noktadan oluşan 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() )
Bu örnekte kitaplık, istenen şekli temsil eden geometriyi içeren bir RoundedPolygon oluşturur. Bu şekli bir Compose uygulamasında çizmek için ondan bir Path nesnesi almanız gerekir. Böylece şekli, Compose'un nasıl çizileceğini bildiği bir forma sokabilirsiniz.
Çokgenin köşelerini yuvarlatma
Bir poligonun köşelerini yuvarlamak için CornerRounding parametresini kullanın. Bu işlev, radius ve smoothing olmak üzere iki parametre alır. Her yuvarlak köşe, 1-3 kübik eğriden oluşur. Bu eğrilerin ortası dairesel yay şeklindedir, iki yan ("yan") eğri ise şeklin kenarından orta 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 yuvarlatılmış köşeli üçgen şu şekilde oluşturulur:
r, yuvarlatılmış köşelerin dairesel yuvarlama boyutunu belirler.Düzleştirici
Yumuşatma, köşenin dairesel yuvarlama kısmından kenara ulaşmanın ne kadar süreceğini belirleyen bir faktördür. 0 değerinde bir yumuşatma faktörü (yumuşatılmamış, CornerRounding için varsayılan değer) tamamen dairesel köşe yuvarlamasıyla sonuçlanır. Sıfır olmayan bir yumuşatma faktörü (maksimum 1,0'a kadar), köşenin üç ayrı eğriyle yuvarlatılmasına neden olur.
Örneğin, aşağıdaki snippet'te düzeltme değerini 0 ve 1 olarak ayarlama 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) )
Boyut ve konum
Varsayılan olarak, merkez (0, 0) etrafında 1 yarıçaplı bir şekil oluşturulur.
Bu yarıçap, merkezin şeklin temel aldığı çokgenin dış köşelerine olan uzaklığını temsil eder. Köşeleri yuvarlamanın, şekli küçülteceğini unutmayın. Bunun nedeni, yuvarlatılmış köşelerin merkeze, yuvarlatılan köşelerden daha yakın olmasıdır. Bir çokgeni 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üşüm işlevlerini kullanarak nesneyi dönüştürüp boyutunu, konumunu ve yönünü değiştirebilirsiniz.
Ş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 biçimini belirlemek üzere sıfır ile bir arasında bir progress değeri girin:
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 (yuvarlak üçgen ve kare) tam ortasındadır ve aşağıdaki sonuç elde edilir:
Çoğu senaryoda, şekil değiştirme yalnızca statik bir oluşturma olarak değil, bir animasyonun parçası olarak yapılır. Bu ikisi arasında animasyon oluşturmak için Compose'daki standart Animation API'leri kullanarak ilerleme değerini zaman içinde değiştirebilirsiniz. Örneğin, bu iki şekil arasındaki dönüşümü aşağıdaki gibi sonsuz şekilde 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() )
Poligonu klip olarak kullanma
Oluşturma'da, bir composable'ın oluşturulma şeklini değiştirmek ve kırpma alanının etrafına çizilen gölgelerden yararlanmak için 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 durumda:
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) ) }
Tıklandığında düğmeyi dönüştür
Basıldığında iki şekil arasında dönüşen bir düğme oluşturmak için graphics-shape kitaplığını kullanabilirsiniz. Öncelikle Shape sınıfını genişleten bir MorphPolygonShape oluşturun,
uygun şekilde sığması için ölçeklendirin ve çevirin. Şeklin animasyonlu hale getirilebilmesi için ilerleme durumunun iletildiğ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 şekil değiştirme biçimini kullanmak için shapeA ve shapeB olmak üzere iki poligon oluşturun. Morph oluşturun ve hatırlayın. Ardından, animasyonun arkasındaki itici güç olarak interactionSource on press'i kullanarak şekil değiştirmeyi düğmeye klip konturu olarak 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, kutuya dokunulduğunda aşağıdaki animasyonun gösterilmesini sağlar:
Şekil dönüşümünü sonsuz şekilde canlandırma
Şekil değiştirme şekline sonsuz animasyon uygulamak için rememberInfiniteTransition simgesini kullanın.
Aşağıda, zaman içinde şekli değişen (ve dönen) bir profil resmi örneği verilmiştir. Bu yaklaşımda, yukarıda gösterilen MorphPolygonShape üzerinde 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:
Özel poligonlar
Normal çokgenlerden oluşturulan şekiller kullanım alanınızı kapsamıyorsa köşe listesiyle daha özel bir şekil oluşturabilirsiniz. Örneğin, şöyle bir kalp şekli oluşturmak isteyebilirsiniz:
Bu şeklin her bir köşesini, x ve y koordinatlarının kayan nokta dizisini alan RoundedPolygon aşırı yüklemesini kullanarak belirtebilirsiniz.
Kalp poligonunu parçalara ayırmak için noktaları belirlemede kutupsal koordinat sisteminin, kartezyen (x,y) koordinat sistemine göre daha kolay olduğunu fark edeceksiniz. Burada 0° sağ taraftan başlar ve saat yönünde ilerler, 270° ise saat 12 konumundadır:
Şekil artık her noktada merkezden açı (𝜭) ve yarıçap belirtilerek daha kolay bir şekilde tanımlanabilir:
Artık köşeler oluşturulabilir ve RoundedPolygon işlevine iletilebilir:
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, ş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))
Önceki kod, kalp için ham köşeleri 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ış ancak diğer köşeler yuvarlatılmıştır. Tek tek köşeler için özel yuvarlatma uygulamak istiyorsanız 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:
Yukarıdaki şekiller kullanım alanınızı kapsamıyorsa özel bir şekil çizmek için Path sınıfını kullanabilir veya diskten bir ImageVector dosyası yükleyebilirsiniz. graphics-shapes kitaplığı, rastgele şekiller için kullanılmak üzere tasarlanmamıştır. Özellikle yuvarlak poligonların ve bunlar arasındaki şekil değiştirme 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 kaynaklara bakın: