From 9835516d965c92dd3ddcc497f2c6ba8eb89cba36 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Tue, 6 Feb 2024 17:31:29 +0000 Subject: [PATCH] Add snippets for graphics-shape (#177) * Adding graphics-shape snippets [WIP] * Adding graphics-shape snippets [WIP] * Apply Spotless * Adding graphics-shape snippets * Apply Spotless * Added custom polygon * Apply Spotless * Adding graphics-shape snippets * Adding graphics-shape snippets * Apply Spotless * Adding graphics-shape snippets * Apply Spotless --------- Co-authored-by: riggaroo --- compose/snippets/build.gradle.kts | 1 + .../compose/snippets/SnippetsActivity.kt | 2 + .../snippets/graphics/ShapesSnippets.kt | 593 ++++++++++++++++++ .../modifiers/CustomModifierSnippets.kt | 11 +- .../snippets/navigation/Destination.kt | 1 + gradle/libs.versions.toml | 1 + 6 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index 75e1fd51..37d55a7b 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -85,6 +85,7 @@ dependencies { implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.graphics.shapes) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.viewbinding) implementation(libs.androidx.paging.compose) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt index d2e129ca..31037647 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt @@ -38,6 +38,7 @@ import com.example.compose.snippets.components.ProgressIndicatorExamples import com.example.compose.snippets.components.ScaffoldExample import com.example.compose.snippets.components.SliderExamples import com.example.compose.snippets.components.SwitchExamples +import com.example.compose.snippets.graphics.ApplyPolygonAsClipImage import com.example.compose.snippets.graphics.BitmapFromComposableSnippet import com.example.compose.snippets.graphics.BrushExamplesScreen import com.example.compose.snippets.images.ImageExamplesScreen @@ -75,6 +76,7 @@ class SnippetsActivity : ComponentActivity() { it.route ) } + Destination.ShapesExamples -> ApplyPolygonAsClipImage() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt new file mode 100644 index 00000000..de0a5ee9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt @@ -0,0 +1,593 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import android.graphics.PointF +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.core.graphics.plus +import androidx.core.graphics.times +import androidx.graphics.shapes.CornerRounding +import androidx.graphics.shapes.Cubic +import androidx.graphics.shapes.Morph +import androidx.graphics.shapes.RoundedPolygon +import androidx.graphics.shapes.star +import com.example.compose.snippets.R +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +@Preview +@Composable +fun BasicShapeCanvas() { + // [START android_compose_graphics_basic_polygon] + Box( + modifier = Modifier + .drawWithCache { + val roundedPolygon = RoundedPolygon( + numVertices = 6, + radius = size.minDimension / 2, + centerX = size.width / 2, + centerY = size.height / 2 + ) + val roundedPolygonPath = roundedPolygon.cubics + .toPath() + onDrawBehind { + drawPath(roundedPolygonPath, color = Color.Blue) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_basic_polygon] +} + +@Preview +@Composable +private fun RoundedShapeExample() { + // [START android_compose_graphics_polygon_rounding] + 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 = 1f + ) + ) + val roundedPolygonPath = roundedPolygon.cubics + .toPath() + onDrawBehind { + drawPath(roundedPolygonPath, color = Color.Black) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_polygon_rounding] +} + +@Preview +@Composable +private fun RoundedShapeSmoothnessExample() { + // [START android_compose_graphics_polygon_rounding_smooth] + 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.cubics + .toPath() + onDrawBehind { + drawPath(roundedPolygonPath, color = Color.Black) + } + } + .size(100.dp) + ) + + // [END android_compose_graphics_polygon_rounding_smooth] +} + +@Preview +@Composable +private fun MorphExample() { + // [START android_compose_graphics_polygon_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 + .toComposePath(progress = 0.5f) + + onDrawBehind { + drawPath(morphPath, color = Color.Black) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_polygon_morph] +} + +@Preview +@Composable +private fun MorphExampleAnimation() { + // [START android_compose_graphics_polygon_morph_animation] + 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 + .toComposePath(progress = morphProgress.value) + + onDrawBehind { + drawPath(morphPath, color = Color.Black) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_polygon_morph_animation] +} + +/** + * Transforms the morph at a given progress into a [Path]. + * It can optionally be scaled, using the origin (0,0) as pivot point. + */ +fun Morph.toComposePath(progress: Float, scale: Float = 1f, path: Path = Path()): Path { + var first = true + path.rewind() + forEachCubic(progress) { bezier -> + if (first) { + path.moveTo(bezier.anchor0X * scale, bezier.anchor0Y * scale) + first = false + } + path.cubicTo( + bezier.control0X * scale, bezier.control0Y * scale, + bezier.control1X * scale, bezier.control1Y * scale, + bezier.anchor1X * scale, bezier.anchor1Y * scale + ) + } + path.close() + return path +} + +/** + * Function used to create a Path from a list of Cubics. + */ +fun List.toPath(path: Path = Path(), scale: Float = 1f): Path { + path.rewind() + firstOrNull()?.let { first -> + path.moveTo(first.anchor0X * scale, first.anchor0Y * scale) + } + for (bezier in this) { + path.cubicTo( + bezier.control0X * scale, bezier.control0Y * scale, + bezier.control1X * scale, bezier.control1Y * scale, + bezier.anchor1X * scale, bezier.anchor1Y * scale + ) + } + path.close() + return path +} + +// [START android_compose_morph_clip_shape] +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.toComposePath(progress = percentage) + path.transform(matrix) + return Outline.Generic(path) + } +} + +// [END android_compose_morph_clip_shape] +@Preview +@Composable +private fun MorphOnClick() { + // [START android_compose_graphics_morph_on_click] + 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)) + } + // [END android_compose_graphics_morph_on_click] +} + +// [START android_compose_shapes_polygon_compose_shape] +class RoundedPolygonShape( + private val polygon: RoundedPolygon +) : Shape { + private val matrix = Matrix() + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val path = polygon.cubics.toPath() + // 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) + path.transform(matrix) + + return Outline.Generic(path) + } +} +// [END android_compose_shapes_polygon_compose_shape] + +@Preview +@Composable +fun ApplyPolygonAsClipBasic() { + // [START android_compose_shapes_apply_as_clip] + 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) + ) + } + // [END android_compose_shapes_apply_as_clip] +} + +@Preview +@Composable +fun ApplyPolygonAsClipImage() { + // [START android_compose_shapes_apply_as_clip_advanced] + 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) + + ) + } + // [END android_compose_shapes_apply_as_clip_advanced] +} + +// [START android_compose_shapes_custom_rotating_morph_shape] +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.toComposePath(progress = percentage) + 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) + ) + } +} +// [END android_compose_shapes_custom_rotating_morph_shape] + +@Preview +@Composable +private fun CartesianPoints() { + // [START android_compose_shapes_custom_vertices] + 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, + ) + } + // [END android_compose_shapes_custom_vertices] + + // [START android_compose_shapes_custom_vertices_draw] + 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.cubics + .toPath() + 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) + ) + // [END android_compose_shapes_custom_vertices_draw] +} + +// [START android_compose_shapes_radial_to_cartesian] +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)) +// [END android_compose_shapes_radial_to_cartesian] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt index c07abf1d..7b778ef2 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -62,10 +63,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import kotlinx.coroutines.launch -@SuppressLint("ModifierFactoryUnreferencedReceiver") // graphics layer does the reference -// [START android_compose_custom_modifiers_1] -fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true) -// [END android_compose_custom_modifiers_1] +private object ClipModifierExample { + @SuppressLint("ModifierFactoryUnreferencedReceiver") // graphics layer does the reference + // [START android_compose_custom_modifiers_1] + fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true) + // [END android_compose_custom_modifiers_1] +} // [START android_compose_custom_modifiers_2] fun Modifier.myBackground(color: Color) = padding(16.dp) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt index b2c936c7..60c07901 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt @@ -22,6 +22,7 @@ enum class Destination(val route: String, val title: String) { AnimationQuickGuideExamples("animationExamples", "Animation Examples"), ComponentsExamples("topComponents", "Top Compose Components"), ScreenshotExample("screenshotExample", "Screenshot Examples"), + ShapesExamples("shapesExamples", "Shapes Examples"), } // Enum class for compose components navigation screen. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b312d852..cbdce495 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,6 +65,7 @@ androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } +androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.0-alpha04" androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" } androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }