Guest User

Untitled

a guest
May 16th, 2026
181
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 7.91 KB | None | 0 0
  1. import androidx.compose.foundation.Canvas
  2. import androidx.compose.foundation.background
  3. import androidx.compose.foundation.layout.Box
  4. import androidx.compose.foundation.layout.BoxWithConstraints
  5. import androidx.compose.foundation.layout.aspectRatio
  6. import androidx.compose.foundation.layout.fillMaxSize
  7. import androidx.compose.foundation.layout.height
  8. import androidx.compose.foundation.layout.padding
  9. import androidx.compose.foundation.layout.size
  10. import androidx.compose.foundation.shape.RoundedCornerShape
  11. import androidx.compose.material3.MaterialTheme
  12. import androidx.compose.material3.Text
  13. import androidx.compose.runtime.Composable
  14. import androidx.compose.runtime.getValue
  15. import androidx.compose.runtime.mutableFloatStateOf
  16. import androidx.compose.runtime.remember
  17. import androidx.compose.runtime.setValue
  18. import androidx.compose.ui.Alignment
  19. import androidx.compose.ui.Modifier
  20. import androidx.compose.ui.geometry.Offset
  21. import androidx.compose.ui.geometry.Size
  22. import androidx.compose.ui.graphics.Brush
  23. import androidx.compose.ui.graphics.Color
  24. import androidx.compose.ui.graphics.SolidColor
  25. import androidx.compose.ui.graphics.StrokeCap
  26. import androidx.compose.ui.graphics.drawscope.Stroke
  27. import androidx.compose.ui.graphics.graphicsLayer
  28. import androidx.compose.ui.layout.onSizeChanged
  29. import androidx.compose.ui.platform.LocalDensity
  30. import androidx.compose.ui.tooling.preview.Preview
  31. import androidx.compose.ui.unit.Dp
  32. import androidx.compose.ui.unit.dp
  33. import androidx.compose.ui.unit.sp
  34. import androidx.compose.ui.util.fastCoerceIn
  35. import kotlin.math.asin
  36. import kotlin.math.cos
  37. import kotlin.math.max
  38. import kotlin.math.sin
  39.  
  40. /**
  41. * Circular progress indicator with an optional composable anchored to the arc endpoint.
  42. *
  43. * @param progress Current progress in [0.0, 1.0]; values outside this range are coerced.
  44. * @param modifier Modifier applied to this indicator.
  45. * @param strokeWidth Width of the progress arc.
  46. * @param strokeBrush Brush for the progress arc.
  47. * @param trackBrush Brush for the background track circle.
  48. * @param trackWidth Width of the background track; defaults to [strokeWidth].
  49. * @param strokeCap Cap style for the arc ends.
  50. * @param startAngle Arc start angle in degrees (0 = 12 o'clock position).
  51. * @param direction Sweep direction of the arc.
  52. * @param rotateContent If true, [content] is rotated to align with the arc tangent at the endpoint.
  53. * @param keepContentUpsideUp When [rotateContent] is true, prevents [content] from appearing upside-down.
  54. * @param contentAlignment Aligns [content]'s edge to the LEADING, TRAILING, or CENTER of the arc endpoint.
  55. * @param content Composable placed at the arc endpoint. Receives the arc radius and the current
  56. * rotation angle in degrees so callers can size or orient child elements accordingly.
  57. */
  58. @Composable
  59. fun AdvancedCircularProgressIndicator(
  60. progress: () -> Float,
  61. modifier: Modifier = Modifier,
  62. strokeWidth: Dp = 8.dp,
  63. strokeBrush: Brush = SolidColor(MaterialTheme.colorScheme.primary),
  64. trackBrush: Brush = SolidColor(MaterialTheme.colorScheme.onSurface),
  65. trackWidth: Dp = strokeWidth,
  66. strokeCap: StrokeCap = StrokeCap.Round,
  67. startAngle: Float = 180f,
  68. direction: RotationDirection = RotationDirection.CLOCKWISE,
  69. rotateContent: Boolean = true,
  70. keepContentUpsideUp: Boolean = true,
  71. contentAlignment: RadialAlignment = RadialAlignment.TRAILING,
  72. content: (@Composable (radius: Float, angle: Float) -> Unit)? = null
  73. ) {
  74. val coercedProgress = progress().fastCoerceIn(0f, 1f)
  75. val arcEndDeg = (startAngle + 360f * coercedProgress * direction.multiplier) % 360f
  76.  
  77. BoxWithConstraints(
  78. modifier = modifier.aspectRatio(1f),
  79. contentAlignment = Alignment.Center
  80. ) {
  81. Canvas(modifier.fillMaxSize()) {
  82. val trackWidthPx = trackWidth.toPx()
  83. val strokeWidthPx = strokeWidth.toPx()
  84. val radiusOffset = max(trackWidthPx, strokeWidthPx) / 2f
  85. val arcSize = size.minDimension - radiusOffset * 2f
  86. drawCircle(
  87. brush = trackBrush,
  88. radius = size.minDimension / 2f - radiusOffset,
  89. style = Stroke(width = trackWidthPx, cap = strokeCap)
  90. )
  91. drawArc(
  92. brush = strokeBrush,
  93. startAngle = startAngle - 90f,
  94. sweepAngle = 360f * coercedProgress * direction.multiplier,
  95. useCenter = false,
  96. topLeft = Offset(radiusOffset, radiusOffset),
  97. size = Size(arcSize, arcSize),
  98. style = Stroke(width = strokeWidthPx, cap = strokeCap)
  99. )
  100. }
  101.  
  102. if (content != null) {
  103. val density = LocalDensity.current
  104.  
  105. val strokeWidthPx = remember(strokeWidth, density) { with(density) { strokeWidth.toPx() } }
  106. val trackWidthPx = remember(trackWidth, density) { with(density) { trackWidth.toPx() } }
  107. val radius = remember(constraints.maxWidth, strokeWidthPx, trackWidthPx) {
  108. constraints.maxWidth / 2f - max(strokeWidthPx, trackWidthPx) / 2f
  109. }
  110. val capAngleOffset = remember(strokeWidthPx, radius) {
  111. Math.toDegrees(asin((strokeWidthPx / 2.0) / radius)).toFloat()
  112. }
  113. var contentWidthPx by remember { mutableFloatStateOf(0f) }
  114. val alignmentDelta = remember(contentWidthPx, radius, contentAlignment) {
  115. val ratio = (contentWidthPx / 2f / radius).coerceIn(-1f, 1f)
  116. val halfAngle = Math.toDegrees(asin(ratio.toDouble())).toFloat()
  117. when (contentAlignment) {
  118. RadialAlignment.LEADING -> +halfAngle
  119. RadialAlignment.TRAILING -> -halfAngle
  120. RadialAlignment.CENTER -> 0f
  121. }
  122. }
  123.  
  124. val alignedAngle = arcEndDeg + capAngleOffset * direction.multiplier + alignmentDelta
  125. val visualRotation = if (keepContentUpsideUp) keepUpsideRotation(alignedAngle) else alignedAngle
  126. val contentOffset = pointOnCircle(radius.toInt(), userAngleToCanvas(alignedAngle))
  127.  
  128. Box(
  129. Modifier
  130. .onSizeChanged { contentWidthPx = it.width.toFloat() }
  131. .graphicsLayer {
  132. if (rotateContent) rotationZ = visualRotation
  133. translationX = contentOffset.x
  134. translationY = contentOffset.y
  135. }
  136. ) {
  137. content(radius, visualRotation)
  138. }
  139. }
  140. }
  141. }
  142.  
  143. private fun userAngleToCanvas(angle: Float): Float = angle - 90f
  144.  
  145. private fun keepUpsideRotation(degrees: Float): Float {
  146. val normalized = ((degrees % 360f) + 360f) % 360f
  147. return if (normalized in 90f..270f) normalized - 180f else normalized
  148. }
  149.  
  150. private fun pointOnCircle(radius: Int, angleDegrees: Float): Offset {
  151. val angleRad = Math.toRadians(angleDegrees.toDouble())
  152. return Offset(
  153. x = (radius * cos(angleRad)).toFloat(),
  154. y = (radius * sin(angleRad)).toFloat()
  155. )
  156. }
  157.  
  158. enum class RotationDirection(val multiplier: Int) {
  159. CLOCKWISE(1),
  160. COUNTER_CLOCKWISE(-1)
  161. }
  162.  
  163. enum class RadialAlignment {
  164. LEADING,
  165. CENTER,
  166. TRAILING
  167. }
  168.  
  169. @Preview
  170. @Composable
  171. fun AdvancedCircularProgressIndicatorPreview() {
  172. AdvancedCircularProgressIndicator(
  173. { 0.6f },
  174. Modifier.size(200.dp),
  175. strokeWidth = 28.dp,
  176. ) { radius, angle ->
  177. Box(
  178. modifier = Modifier
  179. .padding(end = 3.dp)
  180. .height(22.dp)
  181. .background(Color.White.copy(alpha = 0.2f), RoundedCornerShape(100))
  182. .padding(start = 8.dp, end = 4.dp),
  183. contentAlignment = Alignment.Center
  184. ) {
  185. Text(
  186. text = "$radius",
  187. fontSize = 12.sp
  188. )
  189. }
  190. }
  191. }
Advertisement
Add Comment
Please, Sign In to add comment