package androidx.wear.compose.material import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateValue import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.material.ContentAlpha import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.wear.compose.material.ProgressIndicatorDefaults.BaseRotationAngle import androidx.wear.compose.material.ProgressIndicatorDefaults.CircularEasing import androidx.wear.compose.material.ProgressIndicatorDefaults.CircularIndicatorDiameter import androidx.wear.compose.material.ProgressIndicatorDefaults.HeadAndTailAnimationDuration import androidx.wear.compose.material.ProgressIndicatorDefaults.HeadAndTailDelayDuration import androidx.wear.compose.material.ProgressIndicatorDefaults.JumpRotationAngle import androidx.wear.compose.material.ProgressIndicatorDefaults.RotationAngleOffset import androidx.wear.compose.material.ProgressIndicatorDefaults.RotationDuration import androidx.wear.compose.material.ProgressIndicatorDefaults.RotationsPerCycle import kotlin.math.abs import kotlin.math.max import kotlin.math.min /** * Determinate Material Design circular progress indicator. * * Progress indicators express the proportion of completion of an ongoing task. * * [Progress Indicator doc](https://developer.android.com/training/wearables/components/progress-indicator) * ![Progress indicator image](https://developer.android.com/images/reference/androidx/compose/material/circular-progress-indicator.png) * * There is no animation between [progress] values by default, but progress can be animated * with the recommended [ProgressIndicatorDefaults.ProgressAnimationSpec], * as in the following example: * @sample androidx.wear.compose.material.samples.CircularProgressIndicatorWithAnimation * * [CircularProgressIndicator] supports a gap in the circular track between * [endAngle] and [startAngle], which leaves room for other content, * such as [TimeText] at the top of the screen. Example: * @sample androidx.wear.compose.material.samples.CircularProgressIndicatorFullscreenWithGap * * @param modifier Modifier to be applied to the CircularProgressIndicator * @param progress The progress of this progress indicator where 0.0 represents no progress and 1.0 * represents completion. Values outside of this range are coerced into the range 0..1. * @param startAngle The starting position of the progress arc, * measured clockwise in degrees (0 to 360) from the 3 o'clock position. For example, 0 and 360 * represent 3 o'clock, 90 and 180 represent 6 o'clock and 9 o'clock respectively. * Default is 270 degrees (top of the screen) * @param endAngle The ending position of the progress arc, * measured clockwise in degrees (0 to 360) from the 3 o'clock position. For example, 0 and 360 * represent 3 o'clock, 90 and 180 represent 6 o'clock and 9 o'clock respectively. * By default equal to [startAngle] * @param indicatorColor The color of the progress indicator bar. * @param trackColor The color of the background progress track. * @param strokeWidth The stroke width for the progress indicator. */ @Composable public fun CircularProgressIndicator( /* @FloatRange(fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0) */ progress: Float, modifier: Modifier = Modifier, startAngle: Float = 270f, endAngle: Float = startAngle, indicatorColor: Color = MaterialTheme.colors.primary, trackColor: Color = MaterialTheme.colors.onBackground .copy(alpha = ContentAlpha.disabled), strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth, ) { val stroke = with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) } Canvas( modifier .progressSemantics(progress) .size(CircularIndicatorDiameter) .focusable() ) { val backgroundSweep = 360f - ((startAngle - endAngle) % 360 + 360) % 360 val progressSweep = backgroundSweep * progress.coerceIn(0f..1f) // Draw a background drawCircularIndicator( startAngle, backgroundSweep, trackColor, stroke ) // Draw a progress drawCircularIndicator( startAngle, progressSweep, indicatorColor, stroke ) } } /** * Indeterminate Material Design circular progress indicator. * * Indeterminate progress indicator expresses an unspecified wait time and spins indefinitely. * * [Progress Indicator doc](https://developer.android.com/training/wearables/components/progress-indicator) * ![Progress indicator image](https://developer.android.com/images/reference/androidx/compose/material/circular-progress-indicator.png) * * Example of indeterminate progress indicator: * @sample androidx.wear.compose.material.samples.IndeterminateCircularProgressIndicator * * @param modifier Modifier to be applied to the CircularProgressIndicator * @param startAngle The starting position of the progress arc, * measured clockwise in degrees (0 to 360) from the 3 o'clock position. For example, 0 and 360 * represent 3 o'clock, 90 and 180 represent 6 o'clock and 9 o'clock respectively. * Default is 270 degrees (top of the screen) * @param indicatorColor The color of the progress indicator bar. * @param trackColor The color of the background progress track * @param strokeWidth The stroke width for the progress indicator. */ @Composable public fun CircularProgressIndicator( modifier: Modifier = Modifier, startAngle: Float = 270f, indicatorColor: Color = MaterialTheme.colors.primary, trackColor: Color = MaterialTheme.colors.onBackground .copy(alpha = ContentAlpha.disabled), strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth, ) { val stroke = with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) } val transition = rememberInfiniteTransition() // The current rotation around the circle, so we know where to start the rotation from val currentRotation by transition.animateValue( 0, RotationsPerCycle, Int.VectorConverter, infiniteRepeatable( animation = tween( durationMillis = RotationDuration * RotationsPerCycle, easing = LinearEasing ) ) ) // How far forward (degrees) the base point should be from the start point val baseRotation by transition.animateFloat( 0f, BaseRotationAngle, infiniteRepeatable( animation = tween( durationMillis = RotationDuration, easing = LinearEasing ) ) ) // How far forward (degrees) both the head and tail should be from the base point val endAngle by transition.animateFloat( 0f, JumpRotationAngle, infiniteRepeatable( animation = keyframes { durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration 0f at 0 with CircularEasing JumpRotationAngle at HeadAndTailAnimationDuration } ) ) val startProgressAngle by transition.animateFloat( 0f, JumpRotationAngle, infiniteRepeatable( animation = keyframes { durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration 0f at HeadAndTailDelayDuration with CircularEasing JumpRotationAngle at durationMillis } ) ) Canvas( modifier .progressSemantics() .size(CircularIndicatorDiameter) .focusable() ) { val currentRotationAngleOffset = (currentRotation * RotationAngleOffset) % 360f // How long a line to draw using the start angle as a reference point val sweep = abs(endAngle - startProgressAngle) // Offset by the constant offset and the per rotation offset val offset = (startAngle + currentRotationAngleOffset + baseRotation) % 360f drawCircularIndicator(0f, 360f, trackColor, stroke) drawIndeterminateCircularIndicator( startProgressAngle + offset, sweep, indicatorColor, stroke ) } } /** * Contains the default values used for [CircularProgressIndicator]. */ public object ProgressIndicatorDefaults { /** * Default stroke width for [CircularProgressIndicator] * * This can be customized with the `strokeWidth` parameter on [CircularProgressIndicator] */ public val StrokeWidth = 4.dp /** * The default [AnimationSpec] that should be used when animating between progress in a * determinate progress indicator. */ public val ProgressAnimationSpec = SpringSpec( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessVeryLow, // The default threshold is 0.01, or 1% of the overall progress range, which is quite // large and noticeable. visibilityThreshold = 1 / 1000f ) // CircularProgressIndicator Material specs // Diameter of the indicator circle internal val CircularIndicatorDiameter = 40.dp // The animation comprises of 5 rotations around the circle forming a 5 pointed star. // After the 5th rotation, we are back at the beginning of the circle. internal const val RotationsPerCycle = 5 // Each rotation is 1 and 1/3 seconds, but 1332ms divides more evenly internal const val RotationDuration = 1332 // How far the base point moves around the circle, in degrees internal const val BaseRotationAngle = 286f // How far the head and tail should jump forward during one rotation past the base point, // in degrees internal const val JumpRotationAngle = 290f // Each rotation we want to offset the start position by this much, so we continue where // the previous rotation ended. This is the maximum angle covered during one rotation. internal const val RotationAngleOffset = (BaseRotationAngle + JumpRotationAngle) % 360f // The head animates for the first half of a rotation, then is static for the second half // The tail is static for the first half and then animates for the second half internal const val HeadAndTailAnimationDuration = (RotationDuration * 0.5).toInt() internal const val HeadAndTailDelayDuration = HeadAndTailAnimationDuration // The easing for the head and tail jump internal val CircularEasing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f) } private fun DrawScope.drawCircularIndicator( startAngle: Float, sweep: Float, color: Color, stroke: Stroke ) { // To draw this circle we need a rect with edges that line up with the midpoint of the stroke. // To do this we need to remove half the stroke width from the total diameter for both sides. val diameter = min(size.width, size.height) val diameterOffset = stroke.width / 2 val arcDimen = diameter - 2 * diameterOffset drawArc( color = color, startAngle = startAngle, sweepAngle = sweep, useCenter = false, topLeft = Offset( diameterOffset + (size.width - diameter) / 2, diameterOffset + (size.height - diameter) / 2 ), size = Size(arcDimen, arcDimen), style = stroke ) } private fun DrawScope.drawIndeterminateCircularIndicator( startAngle: Float, sweep: Float, color: Color, stroke: Stroke ) { // When the start and end angles are in the same place, we still want to draw a small sweep, so // the stroke caps get added on both ends and we draw the correct minimum length arc val adjustedSweep = max(sweep, 0.1f) drawCircularIndicator(startAngle, adjustedSweep, color, stroke) } private operator fun Size.minus(offset: Float): Size = Size(this.width - offset, this.height - offset)