Kształty w oknie tworzenia wiadomości

Funkcja Utwórz umożliwia tworzenie kształtów wykonanych z wielokątów. Możesz na przykład tworzyć te rodzaje kształtów:

Niebieski sześciokąt na środku obszaru rysowania
Rysunek 1. Przykłady różnych kształtów, które możesz tworzyć za pomocą biblioteki kształtów graficznych

Aby utworzyć niestandardowy okrągły wielokąt w widoku tworzenia, dodaj zależność graphics-shapes od app/build.gradle:

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

Ta biblioteka umożliwia tworzenie kształtów złożonych z poligonów. Chociaż kształty wielokątne mają tylko proste krawędzie i ostre rogi, można w ich przypadku użyć opcjonalnych zaokrąglonych rogów. Dzięki temu możesz łatwo przekształcać jeden kształt w inny. Zmiana kształtów jest trudna i często na czas projektowania. Ta biblioteka upraszcza jednak ten proces, o podobnych wielokątach.

Utwórz wielokąty

Ten fragment tworzy prosty kształt wielokąta z sześcioma punktami pośrodku obszaru rysowania:

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()
)

Niebieski sześciokąt na środku obszaru rysowania
Rysunek 2. Niebieski sześciokąt na środku obszaru rysowania.

W tym przykładzie biblioteka tworzy obiekt RoundedPolygon, który zawiera geometrię reprezentującą żądany kształt. Aby narysować taki kształt w aplikacji Compose, należy uzyskać z niego obiekt Path, aby przekształcić kształt który umie rysować.

Zaokrąglanie narożników wielokąta

Aby zaokrąglić rogi wielokąta, użyj parametru CornerRounding. Ten przyjmuje 2 parametry: radius i smoothing. Każdy zaokrąglony róg składa się od 1 do 3 krzywych sześciennych, krzywe boczne („przesuwające się”) przechodzą od krawędzi kształtu do środkowej krzywej.

Promień

radius to promień okręgu służącego do zaokrąglenia wierzchołka.

Na przykład trójkąt z zaokrąglonymi rogami można utworzyć w ten sposób:

Trójkąt z zaokrąglonymi rogami
Rysunek 3. Trójkąt z zaokrąglonymi rogami.
Promień zaokrąglenia r określa rozmiar zaokrąglonych rogów.
Rysunek 4. Promień zaokrąglenia r określa wielkość zaokrąglonych rogów.

Złagodzenie

Wygładzanie to czynnik, który określa, ile czasu zajmuje przejście od zaokrąglonego narożnika do krawędzi. Wartość 0 (niewygładzona, domyślna wartość dla CornerRounding) powoduje zaokrąglenie narożników w postaci koła. Niezerowy współczynnik wygładzania (maksymalnie 1,0) powoduje narożnika zaokrąglonego przez trzy osobne krzywe.

Współczynnik wygładzania 0 (niewygładzony) tworzy jedną krzywą kubiczną, która biegnie po okręgu wokół narożnika o określonym promieniu zaokrąglenia, jak w poprzednim przykładzie.
Rysunek 5. Współczynnik wygładzania 0 (niewygładzony) tworzy jedną krzywą kubiczną, która biegnie po okręgu wokół narożnika z zadanym promieniem zaokrąglania, jak w poprzednim przykładzie.
.
Niezerowy współczynnik wygładzania powoduje powstanie 3 krzywych krzywoliniowych, które zaokrąglają wierzchołek: wewnętrzna krzywa kołowa (jak poprzednio) oraz 2 krzywe boczne, które przechodzą między krzywą wewnętrzną a krawędziami wielokąta.
Rysunek 6. Niezerowy współczynnik wygładzania powoduje utworzenie trzech krzywych sześciennych do zaokrąglenia wierzchołek: wewnętrzna krzywa kołowa (tak jak wcześniej) i dwie poprzeczne krzywe, między wewnętrznym krzywą a krawędziami wielokąta.

Na przykład poniższy fragment kodu pokazuje subtelną różnicę między ustawieniem wygładzania na 0 a 1:

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)
)

Dwa czarne trójkąty pokazujące różnicę w wygładzeniu
.
Rysunek 7. Dwa czarne trójkąty pokazujące różnicę w parametrze wygładzania.

Rozmiar i pozycja

Domyślnie tworzony jest kształt z promieniem 1 wokół środka (0, 0). Ten promień reprezentuje odległość między środkiem a wierzchołkami zewnętrznymi wielokąta, na którym oparty jest jego kształt. Pamiętaj, że zaokrąglanie rogów uzyskać mniejszy kształt, ponieważ zaokrąglone rogi będą bliżej niż zaokrąglone wierzchołki. Aby zmienić rozmiar wielokąta, dostosuj wartość radius. Aby dostosować położenie, zmień centerX lub centerY wielokąta. Możesz też zmienić rozmiar, położenie i obrót obiektu za pomocą standardowych funkcji przekształcenia DrawScope, takich jak DrawScope#translate().

Kształty Morph

Obiekt Morph to nowy kształt reprezentujący animację między dwoma wielokątami kształtów. Aby przekształcić jeden kształt w drugi, utwórz 2 obiekty RoundedPolygonsMorph, które przyjmują te 2 ksztalty. Aby obliczyć kształt między kształtem początkowym a końcowym, podaj wartość progress z zakresu od 0 do 1, aby określić jego formę między kształtem początkowym (0) a końcowym (1):

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()
)

W powyższym przykładzie postęp jest dokładnie w połowie drogi między dwoma kształtami (trójkąt zaokrąglony i kwadrat) daje taki wynik:

50% odległości między zaokrąglonym trójkątem a kwadratem
Rysunek 8. 50% odległości między zaokrąglonym trójkątem a kwadratem.

W większości przypadków przekształcanie jest wykonywane w ramach animacji, a nie tylko statycznych renderów. Aby przełączać się między nimi, możesz użyć standardowego Interfejsy API do animacji w Compose, które zostaną zmienione wartość postępu w czasie. Możesz na przykład bez końca animować przejście między tymi dwoma kształtami:

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()
)

Nieskończone przejście od kwadratu do zaokrąglonego trójkąta
Rysunek 9. Nieskończone przejście od kwadratu do zaokrąglonego trójkąta.

Używanie wielokąta jako klipu

Powszechnie służy funkcja clip w Compose zmienił sposób renderowania elementu kompozycyjnego. zalety cieni, które rysują się wokół obszaru przycięcia:

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)
    }
}

Następnie możesz użyć wielokąta jako klipu, jak pokazano w tym fragmencie kodu:

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)
    )
}

W efekcie:

Sześciokąt z tekstem „hello compose” pośrodku.
Rysunek 10. Sześciokąt z tekstem „Hello Compose” znajduje się pośrodku.

Może nie wyglądać tak samo jak to, co było wyświetlane wcześniej, ale pozwala który pozwala korzystać z innych funkcji narzędzia Compose. Możesz na przykład użyć tej techniki, aby wyciąć obraz i zastosować cień wokół wyciętego obszaru:

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)

    )
}

Pies w kształcie sześciokąta z cieniem dodanym wokół krawędzi
Rysunek 11. Kształt niestandardowy zastosowany jako klip.

Przycisk zmiany kształtu

Korzystając z biblioteki graphics-shape, możesz utworzyć przycisk, który zmienia się dwa kształty po naciśnięciu. Najpierw utwórz obiekt MorphPolygonShape, który rozszerza obiekt Shape, skalując i przekształcając go w odpowiednim zakresie. Zwróć uwagę na wprowadzenie tak aby można było animować kształt:

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)
    }
}

Aby użyć tego kształtu przejścia, utwórz 2 wieloboki: shapeAshapeB. Utwórz i zapamiętaj Morph. Następnie zastosuj przekształcenie do przycisku jako obrysu klipu, używając naciśnięcia przycisku interactionSource jako siły napędowej animacji:

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))
}

Po kliknięciu pola pojawia się ta animacja:

Przekształcenie zastosowane jako kliknięcie między dwoma kształtami
Rysunek 12. Przekształcenie zastosowane jako kliknięcie między dwoma kształtami.

Nieskończonie animuj kształt

Aby utworzyć nieskończoną animację przekształcania kształtu, użyj rememberInfiniteTransition. Poniżej znajduje się przykład zdjęcia profilowego, które zmienia kształt (i obraca się) bez końca. Ta metoda polega na niewielkim dostosowaniu Widoczna powyżej wartość MorphPolygonShape:

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)
        )
    }
}

Ten kod daje taki ciekawy wynik:

Dłonie złożone w kształt serca
Rysunek 13. Zdjęcie profilowe przycięte przez obracający się kształt z ząbkami.

Wielokąty niestandardowe

Jeśli kształty utworzone ze zwykłych wielokątów nie nadają się do zastosowania, możesz aby uzyskać niestandardowy kształt z listą wierzchołków. Możesz na przykład utworzyć kształt serca:

Dłonie złożone w kształt serca
Rysunek 14. Kształt serca.

Pojedyncze wierzchołki tego kształtu można określić za pomocą funkcji RoundedPolygon które wykorzystuje tablicę zmiennoprzecinkową współrzędnych x i y.

Aby rozbić wielokąt w kształcie serca, zwróć uwagę, że układ współrzędnych biegunowych dla argumentu określenie punktów jest łatwiejsze niż używanie współrzędnych kartezjańskich (x,y). w którym rozpoczyna się po prawej stronie i porusza się w prawo, 270° na godzinie 12:

Dłonie złożone w kształt serca
Rysunek 15. Kształt serca ze współrzędnymi.

Kształt można teraz łatwiej zdefiniować, określając kąt (Θ) i promień od środka w każdym punkcie:

Dłonie złożone w kształt serca
Rysunek 16. Kształt serca z współrzędnymi bez zaokrągleń.

Możesz teraz utworzyć wierzchołki i przekazać je do funkcji RoundedPolygon:

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,
    )
}

Wierzchołki należy przekształcić w współrzędne kartezjańskie za pomocą tej funkcji: radialToCartesian.

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))

Poprzedni kod zapewnia nieprzetworzone wierzchołki serca, ale musisz i zaokrąglanych rogów, aby uzyskać wybrany kształt serca. Narożniki: 90° i 270° nie ma zaokrąglenia, ale pozostałe rogi mają. Aby uzyskać niestandardowe zaokrąglanie dla poszczególnych narożników użyj parametru perVertexRounding:

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)
)

Tak wygląda różowe serce:

Dłonie złożone w kształt serca
Rysunek 17. Wynik w kształcie serca.

Jeśli poprzednie kształty nie mają zastosowania, rozważ użycie Path klasy do rysowania niestandardowej kształtu lub wczytywanie ImageVector plik z dysku. Biblioteka graphics-shapes nie jest przeznaczona do tworzenia dowolnych kształtów, ale ma na celu uproszczenie tworzenia zaokrąglonych wielokątów i animacji przejściowych między nimi.

Dodatkowe materiały

Więcej informacji i przykładów znajdziesz w tych materiałach: