Oluştur işleviyle, çokgenlerden oluşan şekiller oluşturabilirsiniz. Örneğin, aşağıdaki türde şekiller oluşturabilirsiniz:
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() )
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:
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.
Ö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) )
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:
Ç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() )
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:
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) ) }
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:
Ş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:
Ö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:
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, 0°
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:
Şekil artık her bir noktanın merkezinden açıyı () ve yarıçapı belirterek daha kolay bir şekilde tanımlanabilir:
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:
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:
- Blog: Karşılaşılacak Şeylerin Şekli - Şekiller
- Blog: Android'de şekil değiştirme
- Şekiller GitHub gösterimi