Skip to content

Commit

Permalink
Rework MainActivitiy App to generic Shape Editor
Browse files Browse the repository at this point in the history
Relnote: Rework MainActivitiy App to generic Shape Editor
Test: None, only UI changes
Bug: 374043103, 374043103
Change-Id: I1ac13e43844a89d4fd9c3f59d8ce93fd3c8c45d2
  • Loading branch information
bergsiek committed Nov 21, 2024
1 parent ce68ec7 commit 652bf0c
Show file tree
Hide file tree
Showing 16 changed files with 2,494 additions and 946 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ internal fun featureDistSquared(f1: Feature, f2: Feature): Float {
return (featureRepresentativePoint(f1) - featureRepresentativePoint(f2)).getDistanceSquared()
}

// TODO: b/378441547 - Move to explicit parameter / expose?
internal fun featureRepresentativePoint(feature: Feature): Point {
val x = (feature.cubics.first().anchor0X + feature.cubics.last().anchor1X) / 2f
val y = (feature.cubics.first().anchor0Y + feature.cubics.last().anchor1Y) / 2f
Expand Down
6 changes: 5 additions & 1 deletion graphics/integration-tests/testapp-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ dependencies {
implementation("androidx.compose.ui:ui-graphics:1.5.4")
implementation("androidx.compose.foundation:foundation:1.5.4")
implementation("androidx.compose.foundation:foundation-layout:1.5.4")
implementation("androidx.compose.material3:material3:1.1.2")
implementation("androidx.compose.runtime:runtime:1.5.4")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.fragment:fragment:1.6.1")
implementation(project(":compose:material3:material3"))
implementation("androidx.compose.material:material-icons-core:1.7.0")
implementation("androidx.compose.material:material-icons-extended:1.6.8")
}

android {
compileSdk 35

namespace "androidx.graphics.shapes.testcompose"
// TODO(b/313699418): need to update compose.runtime version to 1.6.0+
experimentalProperties["android.lint.useK2Uast"] = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".SimpleMorph"
android:name=".ShapeEditor"
android:exported="true"
android:label="Simple Morph"
android:label="Shape Editor"
android:taskAffinity=".morph"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:name=".SimpleMorph"
android:exported="true"
android:label="Graphics Shapes Test - Compose"
android:taskAffinity=".main"
android:label="Simple Morph"
android:taskAffinity=".morph"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.graphics.shapes.Cubic
import androidx.graphics.shapes.Feature
import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.TransformResult
import kotlin.math.max
import kotlin.math.min

/**
* Utility functions providing more idiomatic ways of transforming RoundedPolygons and transforming
Expand Down Expand Up @@ -76,12 +75,24 @@ fun Morph.toPath(progress: Float, path: Path = Path()): Path {
return path
}

/**
* Gets a [Path] representation for a [Feature] shape. This [Path] can be used to draw the feature.
*
* @param path an optional [Path] object which, if supplied, will avoid the function having to
* create a new [Path] object
*/
@JvmOverloads
fun Feature.toPath(path: Path = Path()): Path {
pathFromCubics(path, cubics, false)
return path
}

/**
* Returns the geometry of the given [cubics] in the given [path] object. This is used internally by
* the toPath functions, but we could consider exposing it as public API in case anyone was dealing
* directly with the cubics we create for our shapes.
*/
private fun pathFromCubics(path: Path, cubics: List<Cubic>) {
private fun pathFromCubics(path: Path, cubics: List<Cubic>, closePath: Boolean = true) {
var first = true
path.rewind()
for (i in 0 until cubics.size) {
Expand All @@ -99,7 +110,9 @@ private fun pathFromCubics(path: Path, cubics: List<Cubic>) {
cubic.anchor1Y
)
}
path.close()
if (closePath) {
path.close()
}
}

/** Transforms a [RoundedPolygon] with the given [Matrix] */
Expand Down Expand Up @@ -188,13 +201,12 @@ class MorphShape(
*/
fun fitToViewport(path: Path, bounds: Rect, viewport: Size, matrix: Matrix = Matrix()) {
matrix.reset()
val maxDimension = max(bounds.width, bounds.height)
val maxDimension = bounds.maxDimension
if (maxDimension > 0f) {
val viewportMin = min(viewport.width, viewport.height)
val scaleFactor = viewportMin / maxDimension
val scaleFactor = viewport.minDimension / maxDimension
val pathCenterX = bounds.left + bounds.width / 2
val pathCenterY = bounds.top + bounds.height / 2
matrix.translate(viewportMin / 2, viewportMin / 2)
matrix.translate(viewport.minDimension / 2, viewport.minDimension / 2)
matrix.scale(scaleFactor, scaleFactor)
matrix.translate(-pathCenterX, -pathCenterY)
path.transform(matrix)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,141 @@

package androidx.graphics.shapes.testcompose

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.graphics.shapes.Cubic
import androidx.graphics.shapes.Feature
import kotlin.math.roundToInt

internal fun Cubic.pointOnCurve(t: Float): Offset {
val u = 1 - t
return Offset(
anchor0X * (u * u * u) +
control0X * (3 * t * u * u) +
control1X * (3 * t * t * u) +
anchor1X * (t * t * t),
anchor0Y * (u * u * u) +
control0Y * (3 * t * u * u) +
control1Y * (3 * t * t * u) +
anchor1Y * (t * t * t)
)
}

internal fun DrawScope.debugDraw(bezier: Cubic) {
internal fun DrawScope.debugDrawCubic(bezier: Cubic, scheme: ColorScheme) {
// Draw red circles for start and end.
drawCircle(Color.Red, radius = 6f, center = bezier.anchor0(), style = Stroke(2f))
drawCircle(Color.Magenta, radius = 8f, center = bezier.anchor1(), style = Stroke(2f))
drawCircle(scheme.inverseSurface, radius = 6f, center = bezier.anchor0(), style = Stroke(2f))
drawCircle(scheme.inverseSurface, radius = 8f, center = bezier.anchor1(), style = Stroke(2f))

// Draw a circle for the first control point, and a line from start to it.
// The curve will start in this direction
drawLine(Color.Yellow, bezier.anchor0(), bezier.control0(), strokeWidth = 0f)
drawCircle(Color.Yellow, radius = 4f, center = bezier.control0(), style = Stroke(2f))
drawLine(scheme.scrim, bezier.anchor0(), bezier.control0(), strokeWidth = 0f)
drawCircle(scheme.scrim, radius = 4f, center = bezier.control0(), style = Stroke(2f))

// Draw a circle for the second control point, and a line from it to the end.
// The curve will end in this direction
drawLine(Color.Yellow, bezier.control1(), bezier.anchor1(), strokeWidth = 0f)
drawCircle(Color.Yellow, radius = 4f, center = bezier.control1(), style = Stroke(2f))
drawLine(scheme.scrim, bezier.control1(), bezier.anchor1(), strokeWidth = 0f)
drawCircle(scheme.scrim, radius = 4f, center = bezier.control1(), style = Stroke(2f))

// Draw dots along each curve
var t = .1f
while (t < 1f) {
drawCircle(Color.White, radius = 2f, center = bezier.pointOnCurve(t), style = Stroke(2f))
drawCircle(scheme.primary, radius = 2f, center = bezier.pointOnCurve(t), style = Stroke(2f))
t += .1f
}
}

private fun Cubic.anchor0() = Offset(anchor0X, anchor0Y)
internal fun DrawScope.debugDrawFeature(
feature: Feature,
colorScheme: FeatureColorScheme,
backgroundColor: Color,
radius: Float
) {
val color = featureToColor(feature, colorScheme)
val representativePoint = featureRepresentativePoint(feature)

// Draw a clickable circle for the representative Point
drawCircle(color, radius = radius, center = representativePoint, style = Fill)

// With a bit of a background to suggest tapping is possible
drawCircle(
color.copy(0.2f),
radius = radius + (radius * 0.6f),
center = representativePoint,
style = Fill
)

// Finally add a border around the representative point
drawCircle(
backgroundColor,
radius = radius,
center = representativePoint,
style = Stroke(radius / 4)
)
}

@Composable
internal fun FeatureRepresentativePoint(
modifier: Modifier = Modifier,
feature: Feature,
colorScheme: FeatureColorScheme,
backgroundColor: Color,
model: PanZoomRotateBoxState = PanZoomRotateBoxState(),
pointSize: Dp = 15.dp,
onClick: () -> Unit,
) {
val radius = with(LocalDensity.current) { (pointSize / 2).roundToPx() }
val position = model.mapOut(featureRepresentativePoint(feature))

Box(
modifier
.offset {
IntOffset(
(position.x).roundToInt(),
(position.y).roundToInt(),
)
}
.drawWithContent {
drawContent()
debugDrawFeature(feature, colorScheme, backgroundColor, radius.toFloat())
}
)

Box(
modifier
.offset {
IntOffset(
(position.x - CLICKABLE_SCALE * radius).roundToInt(),
(position.y - CLICKABLE_SCALE * radius).roundToInt(),
)
}
.size(pointSize * CLICKABLE_SCALE)
.clip(CircleShape)
.clickable(onClick = onClick)
)
}

internal fun featureToColor(feature: Feature, scheme: FeatureColorScheme): Color =
if (feature.isEdge) {
scheme.edgeColor
} else if (feature.isConvexCorner) {
scheme.convexColor
} else {
scheme.concaveColor
}

// TODO: b/378441547 - Remove if explicit / exposed by default
internal fun featureRepresentativePoint(feature: Feature): Offset =
(feature.cubics.first().anchor0() + feature.cubics.last().anchor1()) / 2f

internal fun Cubic.anchor0() = Offset(anchor0X, anchor0Y)

internal fun Cubic.control0() = Offset(control0X, control0Y)

private fun Cubic.control0() = Offset(control0X, control0Y)
internal fun Cubic.control1() = Offset(control1X, control1Y)

private fun Cubic.control1() = Offset(control1X, control1Y)
internal fun Cubic.anchor1() = Offset(anchor1X, anchor1Y)

private fun Cubic.anchor1() = Offset(anchor1X, anchor1Y)
internal const val CLICKABLE_SCALE = 1.8f
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2024 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
*
* http://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 androidx.graphics.shapes.testcompose

import androidx.compose.ui.geometry.Offset
import androidx.graphics.shapes.Cubic
import androidx.graphics.shapes.Feature

/**
* Copied code from internal Measurer to measure cubics. Reason for copy: We split cubics depending
* on length threshold, therefore we need a way to know their lengths.
*/
internal class LengthMeasurer() {
fun measureCubic(cubic: Cubic): Float {
var total = 0f
var prev = Offset(cubic.anchor0X, cubic.anchor0Y)

for (i in 1..3) {
val progress = i.toFloat() / 3
val point = cubic.pointOnCurve(progress)
val segment = (point - prev).getDistance()

total += segment
prev = point
}

return total
}

fun measureFeature(feature: Feature): Float = feature.cubics.map { measureCubic(it) }.sum()
}
Loading

0 comments on commit 652bf0c

Please sign in to comment.