Merge "Add circular progree indicator fallback implementation with older renderer" into androidx-main
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index 2aa2b52..9887718 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -27,6 +27,7 @@
import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
import androidx.wear.protolayout.material3.AppCardStyle.Companion.largeAppCardStyle
import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
import androidx.wear.protolayout.material3.ButtonDefaults.filledTonalButtonColors
@@ -76,6 +77,8 @@
.setScreenDensity(displayMetrics.density)
.setFontScale(1f)
.setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ // testing with the latest renderer version
+ .setRendererSchemaVersion(VersionInfo.Builder().setMajor(99).setMinor(999).build())
.build()
val clickable = clickable(id = "action_id")
val testCases: HashMap<String, LayoutElementBuilders.LayoutElement> = HashMap()
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt
index c8a3022..c029f51 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt
@@ -23,13 +23,14 @@
import androidx.wear.protolayout.DimensionBuilders.AngularLayoutConstraint
import androidx.wear.protolayout.DimensionBuilders.ContainerDimension
import androidx.wear.protolayout.DimensionBuilders.DegreesProp
+import androidx.wear.protolayout.DimensionBuilders.DpProp
import androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp
import androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp
import androidx.wear.protolayout.DimensionBuilders.degrees
-import androidx.wear.protolayout.DimensionBuilders.dp
import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Arc
+import androidx.wear.protolayout.LayoutElementBuilders.ArcLine
import androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer
import androidx.wear.protolayout.LayoutElementBuilders.Box
import androidx.wear.protolayout.LayoutElementBuilders.DashedArcLine
@@ -38,6 +39,8 @@
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
+import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.CPI_DEFAULT_DP_SIZE
import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.INDICATOR_STROKE_WIDTH_INCREMENT_PX
import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.LARGE_STROKE_WIDTH
import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.METADATA_TAG
@@ -49,11 +52,17 @@
import androidx.wear.protolayout.modifiers.padding
import androidx.wear.protolayout.modifiers.tag
import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.dp
import kotlin.math.min
/**
* Protolayout Material3 design circular progress indicator.
*
+ * Note that, the proper implementation of this component requires a ProtoLayout renderer with
+ * version equal to or above 1.403. When the renderer is lower than 1.403, this component will
+ * automatically fallback to an implementation with reduced features, without support for expandable
+ * size, overflow, and start/end transition.
+ *
* @param staticProgress The static progress of this progress indicator where 0 represent no
* progress and 1 represents completion. Progress above 1 is also allowed. If [dynamicProgress] is
* also set, this static value will only be used when the dynamic value cannot be evaluated. By
@@ -91,8 +100,8 @@
modifier: LayoutModifier = LayoutModifier,
startAngleDegrees: Float = 0F,
endAngleDegrees: Float = startAngleDegrees + 360F,
- @Dimension(unit = DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
- @Dimension(unit = DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
+ @Dimension(DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
+ @Dimension(DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
colors: ProgressIndicatorColors = filledProgressIndicatorColors(),
size: ContainerDimension = expand(),
): LayoutElement {
@@ -100,19 +109,39 @@
verifySize(size)
val modifiers = (LayoutModifier.tag(METADATA_TAG) then modifier).toProtoLayoutModifiers()
+ val hasDashedArcLineSupport =
+ deviceConfiguration.rendererSchemaVersion.hasDashedArcLineSupport()
+ // With the fallback implementation, expandable size is not supported, fallback to dp size.
+ val containerSize =
+ if (hasDashedArcLineSupport || size is DpProp) size else CPI_DEFAULT_DP_SIZE.dp
+ val boxBuilder =
+ if (hasDashedArcLineSupport)
+ singleSegmentImpl(
+ startAngleDegrees = startAngleDegrees,
+ endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+ staticProgress = staticProgress,
+ dynamicProgress = dynamicProgress,
+ strokeWidth = strokeWidth,
+ gapSize = gapSize,
+ colors = colors
+ )
+ else
+ circularProgressIndicatorFallbackImpl(
+ // Without DashedArcLine support, container size fell back to dp size.
+ arcContainerSize = (containerSize as DpProp).value,
+ startAngleDegrees = startAngleDegrees,
+ endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+ staticProgress = staticProgress,
+ dynamicProgress = dynamicProgress,
+ strokeWidth = strokeWidth,
+ gapSize = gapSize,
+ colors = colors
+ )
- return singleSegmentImpl(
- startAngleDegrees = startAngleDegrees,
- endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
- staticProgress = staticProgress,
- dynamicProgress = dynamicProgress,
- strokeWidth = strokeWidth,
- gapSize = gapSize,
- colors = colors
- )
+ return boxBuilder
.setModifiers(modifiers)
- .setWidth(size)
- .setHeight(size)
+ .setWidth(containerSize)
+ .setHeight(containerSize)
.build()
}
@@ -121,6 +150,11 @@
*
* A segmented variant of [circularProgressIndicator] that is divided into equally sized segments.
*
+ * Note that, the proper implementation of this component requires a ProtoLayout renderer with
+ * version equal to or above 1.403. When the renderer is lower than 1.403, this component will
+ * automatically fallback to an implementation with reduced features, without support for multiple
+ * segments, expandable size, overflow, and start/end transition.
+ *
* @param segmentCount Number of equal segments that the progress indicator should be divided into.
* Has to be a number greater than or equal to 1.
* @param staticProgress The static progress of this progress indicator where 0 represent no
@@ -161,8 +195,8 @@
modifier: LayoutModifier = LayoutModifier,
startAngleDegrees: Float = 0F,
endAngleDegrees: Float = startAngleDegrees + 360F,
- @Dimension(unit = DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
- @Dimension(unit = DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
+ @Dimension(DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
+ @Dimension(DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
colors: ProgressIndicatorColors = filledProgressIndicatorColors(),
size: ContainerDimension = expand(),
): LayoutElement {
@@ -170,20 +204,40 @@
verifySize(size)
val modifiers = (LayoutModifier.tag(METADATA_TAG) then modifier).toProtoLayoutModifiers()
+ val hasDashedArcLineSupport =
+ deviceConfiguration.rendererSchemaVersion.hasDashedArcLineSupport()
+ // Without using DashedArcLine, expandable size is not supported, fallback to dp size.
+ val containerSize =
+ if (hasDashedArcLineSupport || size is DpProp) size else CPI_DEFAULT_DP_SIZE.dp
+ val boxBuilder =
+ if (hasDashedArcLineSupport)
+ multipleSegmentsImpl(
+ segmentCount = segmentCount,
+ startAngleDegrees = startAngleDegrees,
+ endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+ staticProgress = staticProgress,
+ dynamicProgress = dynamicProgress,
+ strokeWidth = strokeWidth,
+ gapSize = gapSize,
+ colors = colors
+ )
+ else
+ circularProgressIndicatorFallbackImpl(
+ // Without DashedArcLine support, container size fell back to dp size.
+ arcContainerSize = (containerSize as DpProp).value,
+ startAngleDegrees = startAngleDegrees,
+ endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+ staticProgress = staticProgress,
+ dynamicProgress = dynamicProgress,
+ strokeWidth = strokeWidth,
+ gapSize = gapSize,
+ colors = colors
+ )
- return multipleSegmentsImpl(
- segmentCount = segmentCount,
- startAngleDegrees = startAngleDegrees,
- endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
- staticProgress = staticProgress,
- dynamicProgress = dynamicProgress,
- strokeWidth = strokeWidth,
- gapSize = gapSize,
- colors = colors
- )
+ return boxBuilder
.setModifiers(modifiers)
- .setWidth(size)
- .setHeight(size)
+ .setWidth(containerSize)
+ .setHeight(containerSize)
.build()
}
@@ -197,8 +251,8 @@
endAngleDegrees: Float,
staticProgress: Float,
dynamicProgress: DynamicFloat?,
- @Dimension(unit = DP) strokeWidth: Float,
- @Dimension(unit = DP) gapSize: Float,
+ @Dimension(DP) strokeWidth: Float,
+ @Dimension(DP) gapSize: Float,
colors: ProgressIndicatorColors
): Box.Builder {
val sweepAngle = endAngleDegrees - startAngleDegrees
@@ -225,7 +279,7 @@
.setGapLocations(0f)
.build()
val spacer =
- ArcSpacer.Builder().setAngularLength(dp(gapSize / 2)).setThickness(dp(strokeWidth)).build()
+ ArcSpacer.Builder().setAngularLength((gapSize / 2F).dp).setThickness(strokeWidth.dp).build()
return Box.Builder()
.addContent( // the track
createArc(
@@ -266,8 +320,8 @@
endAngleDegrees: Float,
staticProgress: Float,
dynamicProgress: DynamicFloat?,
- @Dimension(unit = DP) strokeWidth: Float,
- @Dimension(unit = DP) gapSize: Float,
+ @Dimension(DP) strokeWidth: Float,
+ @Dimension(DP) gapSize: Float,
colors: ProgressIndicatorColors
): Box.Builder {
val sweepAngle = endAngleDegrees - startAngleDegrees
@@ -333,6 +387,12 @@
}
}
+/**
+ * For renderer with version lower than 1.403, there is no [DashedArcLine] support. In this case,
+ * the progress indicator will fallback to use [ArcLine]
+ */
+private fun VersionInfo.hasDashedArcLineSupport() = major > 1 || (major == 1 && minor >= 403)
+
/*
* Check the endAngle is valid with the provided startAngle, and adjust the sweep angle to be 360
* maximum.
@@ -394,11 +454,11 @@
anchorType: Int,
arcLength: DegreesProp,
arcColor: ColorProp,
- @Dimension(unit = DP) strokeWidth: Float,
+ @Dimension(DP) strokeWidth: Float,
linePattern: DashedLinePattern,
arcDirection: Int
-): Arc.Builder {
- return Arc.Builder()
+): Arc.Builder =
+ Arc.Builder()
.setAnchorAngle(anchorAngle)
.setAnchorType(anchorType)
.setArcDirection(arcDirection)
@@ -415,4 +475,3 @@
)
.build()
)
-}
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt
index 7ca7208..bd9c49c 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt
@@ -84,18 +84,18 @@
.build()
/** Large stroke width for circular progress indicator. */
- @Dimension(unit = DP) public const val LARGE_STROKE_WIDTH: Float = 8F
+ @Dimension(DP) public const val LARGE_STROKE_WIDTH: Float = 8F
/** Small stroke width for circular progress indicator. */
- @Dimension(unit = DP) public const val SMALL_STROKE_WIDTH: Float = 4F
+ @Dimension(DP) public const val SMALL_STROKE_WIDTH: Float = 4F
/**
* Returns recommended size of the gap based on [strokeWidth].
*
* The absolute value can be customized with `gapSize` parameter on [circularProgressIndicator].
*/
- @Dimension(unit = DP)
- public fun calculateRecommendedGapSize(@Dimension(unit = DP) strokeWidth: Float): Float =
+ @Dimension(DP)
+ public fun calculateRecommendedGapSize(@Dimension(DP) strokeWidth: Float): Float =
strokeWidth / 3F
internal const val METADATA_TAG: String = "M3CPI"
@@ -110,4 +110,7 @@
* top of the track arc where there are multiple segments.
*/
internal const val INDICATOR_STROKE_WIDTH_INCREMENT_PX: Float = 1.5f
+
+ /** Default size for the fallback implementation. */
+ @Dimension(DP) internal const val CPI_DEFAULT_DP_SIZE: Float = 52F
}
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorFallback.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorFallback.kt
new file mode 100644
index 0000000..00dc0e2
--- /dev/null
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorFallback.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2025 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.wear.protolayout.material3
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ColorBuilders.ColorProp
+import androidx.wear.protolayout.DimensionBuilders.AngularLayoutConstraint
+import androidx.wear.protolayout.DimensionBuilders.DegreesProp
+import androidx.wear.protolayout.DimensionBuilders.degrees
+import androidx.wear.protolayout.LayoutElementBuilders
+import androidx.wear.protolayout.LayoutElementBuilders.Arc
+import androidx.wear.protolayout.LayoutElementBuilders.ArcLine
+import androidx.wear.protolayout.LayoutElementBuilders.Box
+import androidx.wear.protolayout.LayoutElementBuilders.DashedArcLine
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.types.dp
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * This method provides the fallback content layout for [circularProgressIndicator] and
+ * [segmentedCircularProgressIndicator] using [ArcLine] when the renderer version is lower than
+ * 1.403 where [DashedArcLine] is not available.
+ *
+ * Note that we require valid start and end angles for calling this method.
+ */
+internal fun MaterialScope.circularProgressIndicatorFallbackImpl(
+ @Dimension(unit = DP) arcContainerSize: Float,
+ startAngleDegrees: Float,
+ endAngleDegrees: Float,
+ staticProgress: Float,
+ dynamicProgress: DynamicFloat?,
+ @Dimension(unit = DP) strokeWidth: Float,
+ @Dimension(unit = DP) gapSize: Float,
+ colors: ProgressIndicatorColors
+): Box.Builder {
+ // Offset the anchor to make space for the cap and half gap.
+ val anchorOffsetDegrees =
+ (strokeWidth + gapSize).dpToDegree(radius = (arcContainerSize - strokeWidth) / 2.0) / 2
+
+ // Visually, the progress arcline and track arcline never overlaps. But from the layout point
+ // of view, their layout size are both from start angle to end angle, and overlaps completely.
+ // Thus, we put each arcline in an arc container, and stacks these two arc containers inside a
+ // box.
+ return Box.Builder()
+ .addContent(
+ createArc(
+ anchorDegree = endAngleDegrees - anchorOffsetDegrees,
+ anchorType = LayoutElementBuilders.ARC_ANCHOR_END,
+ arcLength =
+ trackInDegrees(
+ sweepAngle = endAngleDegrees - startAngleDegrees,
+ staticProgress = staticProgress,
+ dynamicProgress = dynamicProgress,
+ lengthAdjustment = anchorOffsetDegrees * 2F
+ ),
+ color = colors.trackColor.prop,
+ strokeWidth = strokeWidth
+ )
+ )
+ .addContent(
+ createArc(
+ anchorDegree = startAngleDegrees + anchorOffsetDegrees,
+ anchorType = LayoutElementBuilders.ARC_ANCHOR_START,
+ arcLength =
+ progressInDegrees(
+ sweepAngle = endAngleDegrees - startAngleDegrees,
+ staticProgress = staticProgress,
+ dynamicProgress = dynamicProgress,
+ lengthAdjustment = anchorOffsetDegrees * 2F
+ ),
+ color = colors.indicatorColor.prop,
+ strokeWidth = strokeWidth
+ )
+ )
+}
+
+/**
+ * A small offset to make the progress dot remaining when the progress is 0, and the track dot
+ * remaining when the progress is 1.
+ */
+private const val ARC_OFFSET_IN_DEGREES = 0.05f
+
+private fun createArc(
+ anchorDegree: Float,
+ anchorType: Int,
+ arcLength: DegreesProp,
+ color: ColorProp,
+ @Dimension(unit = DP) strokeWidth: Float
+): Arc =
+ Arc.Builder()
+ .setAnchorAngle(degrees(anchorDegree))
+ .setAnchorType(anchorType)
+ .setArcDirection(LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE)
+ .addContent(
+ ArcLine.Builder()
+ .setColor(color)
+ .setArcDirection(LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE)
+ .setLength(arcLength)
+ .setLayoutConstraintsForDynamicLength(
+ // We use one Arc container to put one arcline, so it is fine to put 360 here
+ // as layout constraint.
+ AngularLayoutConstraint.Builder(360F).setAngularAlignment(anchorType).build()
+ )
+ .setThickness(strokeWidth.dp)
+ .build()
+ )
+ .build()
+
+private fun Float.dpToDegree(radius: Double): Float =
+ // radianAngle = arcLength / radius
+ Math.toDegrees(this / radius).toFloat()
+
+private fun progressInDegrees(
+ sweepAngle: Float,
+ staticProgress: Float,
+ dynamicProgress: DynamicFloat?,
+ lengthAdjustment: Float
+): DegreesProp =
+ arcInDegrees(
+ sweepAngle = sweepAngle,
+ staticRatio = staticProgress,
+ dynamicRatio = dynamicProgress,
+ lengthAdjustment = lengthAdjustment
+ )
+
+private fun trackInDegrees(
+ sweepAngle: Float,
+ staticProgress: Float,
+ dynamicProgress: DynamicFloat?,
+ lengthAdjustment: Float
+): DegreesProp =
+ arcInDegrees(
+ sweepAngle = sweepAngle,
+ staticRatio = 1F - staticProgress,
+ dynamicRatio =
+ if (dynamicProgress == null) null else DynamicFloat.constant(1F).minus(dynamicProgress),
+ lengthAdjustment = lengthAdjustment
+ )
+
+private fun arcInDegrees(
+ sweepAngle: Float,
+ staticRatio: Float,
+ dynamicRatio: DynamicFloat?,
+ lengthAdjustment: Float
+): DegreesProp {
+ val staticValue =
+ getCorrectStaticArcLength(
+ sweepAngle = sweepAngle,
+ ratio = staticRatio,
+ lengthAdjustment = lengthAdjustment
+ )
+
+ if (dynamicRatio == null) { // static value
+ return degrees(staticValue)
+ }
+
+ return DegreesProp.Builder(staticValue)
+ .setDynamicValue(
+ getApproximateDynamicArcLength(
+ sweepAngle = sweepAngle,
+ ratio = dynamicRatio,
+ lengthAdjustment = lengthAdjustment
+ )
+ )
+ .build()
+}
+
+/**
+ * When drawing the progress arc line, we need to adjust its Length to make space for two caps and
+ * gap. Note that, event the progress arc is 0, we still leave the above space, for a good
+ * transition to a non-zero progress. Similar for track arc line. The arc length calculation is as
+ * follows:
+ * ```
+ * sweepAngle = endAngle-startAngle
+ * perArcLengthAdjustment = cap * 2 - gap
+ * maxTotalLength = endAngle - startAngle- 2 * perArcLengthAdjustment
+ * ProgressArcLength = clamp(sweepAngle * progress - perArcLengthAdjustment, 0, maxTotalLength)
+ * trackArcLength = clamp(sweepAngle*(1-progress) - perArcLengthAdjustment, 0, maxTotalLength)
+ * ```
+ */
+private fun getCorrectStaticArcLength(
+ sweepAngle: Float,
+ ratio: Float,
+ lengthAdjustment: Float
+): Float {
+ val length = (sweepAngle) * ratio - lengthAdjustment
+ val maxLength = sweepAngle - 2F * lengthAdjustment
+ return (max(min(length, maxLength), 0F) + ARC_OFFSET_IN_DEGREES)
+}
+
+/**
+ * When the progress is static, we calculate the arc lengths as {@link #getCorrectStaticArcLength}.
+ * However, the clamp operation adds two extra animation quota per arc with animated dynamic
+ * progress which is not acceptable. We thus use an approximation calculation for dynamic values,
+ * Which is not very precise, but a good approximation. As follows:
+ * ```
+ * sweepAngle = endAngle-startAngle
+ * perArcLengthAdjustment = cap * 2 - gap
+ * maxTotalLength = endAngle-startAngle - 2 * perArcLengthAdjustment
+ * progressArcLength = maxTotalLength * progress
+ * trackArcLength = maxTotalLength * (1-progress)
+ * ```
+ */
+private fun getApproximateDynamicArcLength(
+ sweepAngle: Float,
+ ratio: DynamicFloat,
+ lengthAdjustment: Float
+) = ratio.times(sweepAngle - lengthAdjustment * 2).plus(ARC_OFFSET_IN_DEGREES)
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt
index 07973b83..6d7e763 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt
@@ -21,6 +21,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
import androidx.wear.protolayout.testing.hasHeight
import androidx.wear.protolayout.testing.hasWidth
@@ -61,6 +62,7 @@
DeviceParametersBuilders.DeviceParameters.Builder()
.setScreenWidthDp(192)
.setScreenHeightDp(192)
+ .setRendererSchemaVersion(VersionInfo.Builder().setMajor(1).setMinor(403).build())
.build()
private const val STATIC_PROGRESS = 0.5F
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
index ef30cd7..cd0f098 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
@@ -16,6 +16,8 @@
package androidx.wear.protolayout.types
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.wear.protolayout.DimensionBuilders.DpProp
import androidx.wear.protolayout.DimensionBuilders.EmProp
import androidx.wear.protolayout.DimensionBuilders.SpProp
@@ -32,7 +34,8 @@
internal val Boolean.prop: BoolProp
get() = BoolProp.Builder(this).build()
-internal val Float.dp: DpProp
+@get:RestrictTo(Scope.LIBRARY_GROUP)
+val Float.dp: DpProp
get() = DpProp.Builder(this).build()
@RequiresSchemaVersion(major = 1, minor = 400)
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
index f3f97b9..183279b 100644
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
+++ b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
@@ -17,6 +17,8 @@
package androidx.wear.tiles.samples.tile
import android.content.Context
+import androidx.wear.protolayout.DeviceParametersBuilders
+import androidx.wear.protolayout.DimensionBuilders.dp
import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.DimensionBuilders.weight
import androidx.wear.protolayout.LayoutElementBuilders
@@ -25,6 +27,7 @@
import androidx.wear.protolayout.ResourceBuilders.ImageResource
import androidx.wear.protolayout.TimelineBuilders
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
import androidx.wear.protolayout.material3.ButtonDefaults.filledVariantButtonColors
import androidx.wear.protolayout.material3.CardColors
import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
@@ -250,6 +253,43 @@
}
)
+private fun MaterialScope.graphicDataCardSampleWithFallbackProgressIndicator(context: Context) =
+ graphicDataCard(
+ onClick = clickable(),
+ modifier = LayoutModifier.contentDescription("Graphic Data Card"),
+ height = expand(),
+ horizontalAlignment = LayoutElementBuilders.HORIZONTAL_ALIGN_END,
+ title = {
+ text(
+ "1,234!".layoutString,
+ )
+ },
+ content = {
+ text(
+ "steps".layoutString,
+ )
+ },
+ graphic = {
+ materialScope(
+ context = context,
+ deviceConfiguration =
+ DeviceParametersBuilders.DeviceParameters.Builder()
+ .setRendererSchemaVersion(
+ VersionInfo.Builder().setMajor(1).setMinor(402).build()
+ )
+ .build()
+ ) {
+ segmentedCircularProgressIndicator(
+ segmentCount = 6,
+ startAngleDegrees = 200F,
+ endAngleDegrees = 520F,
+ dynamicProgress = DynamicFloat.animate(0.0F, 1.0F, recommendedAnimationSpec),
+ size = dp(75F)
+ )
+ }
+ }
+ )
+
private fun MaterialScope.dataCards() = buttonGroup {
buttonGroupItem {
textDataCard(