Oluştur özelliğini kullanarak poligonlardan şekiller oluşturabilirsiniz. Örneğin, aşağıdaki şekilleri oluşturabilirsiniz:
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() )
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:
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.
Ö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) )
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:
Ç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() )
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:
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) ) }
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:
Ş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:
Ö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:
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 0°
sağ taraftan başlar ve saat 12 konumunda 270°
ile saat yönünde ilerler:
Artık şekil, her noktadaki açıyı (𝜭) ve merkeze olan yarıçapı belirterek daha kolay bir şekilde tanımlanabilir:
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:
Ö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:
- Blog: The Shape of Things to Come - Shapes
- Blog: Android'de şekil değiştirme
- Şekiller GitHub gösterimi