I am trying to draw Squircle Image using the Android Jetpack Compose.
I found simple code set on Github: https://github.com/Size0f/android.compose.squircle/blob/master/squircle/src/main/java/com/sizeof/libraries/compose/squircle/SquircleShape.kt
Here is SquuircleShape compose:
class SquircleShape : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
) = Outline.Generic(
path = createSquirclePath(size, SMOOTHING)
)
private fun createSquirclePath(size: Size, smoothing: Double): androidx.compose.ui.graphics.Path {
return Path().apply {
val oversize = size.width * OVERSAMPLING_MULTIPLIER
val squircleRadius = (oversize / 2F).toInt()
// power radius before for optimization
val poweredRadius = squircleRadius
.toDouble()
.pow(smoothing)
// generate Y coordinates for path
val yCoordinates = (-squircleRadius..squircleRadius).map { x ->
x.toFloat() to evalSquircleFun(x, poweredRadius, smoothing)
}
// generate Y coordinates for mirror half of squircle shape
val yMirroredCoordinates = yCoordinates.map { (x, y) -> Pair(x, -y) }
var currentX = 0F
var currentY = 0F
// set path by using quadraticBezier
(yCoordinates + yMirroredCoordinates).forEach { (x, y) ->
quadTo(currentX, currentY, x, y)
currentX = x
currentY = y
}
close()
// scale down to original size - for better corners without anti-alias
transform(
scaleMatrix(
sx = 1 / OVERSAMPLING_MULTIPLIER,
sy = 1 / OVERSAMPLING_MULTIPLIER
)
)
// translate path to center
transform(
translationMatrix(
tx = size.width / 2,
ty = size.height / 2
)
)
}.asComposePath()
}
// squircle formula: | (r^smoothing) - |x|^5 | ^ (1 / smoothing)
private fun evalSquircleFun(x: Int, poweredRadius: Double, smoothing: Double) =
(poweredRadius - abs(x.toDouble().pow(smoothing))).pow(1 / smoothing).toFloat()
companion object {
private const val SMOOTHING = 3.0
private const val OVERSAMPLING_MULTIPLIER = 4F
}
}
Here is SquircleImage compose:
#Composable
fun SquircleImage(
modifier: Modifier = Modifier,
imageRequest: ImageRequest,
size: Dp,
backgroundColor: Color = Gray10,
borderColor: Color = Gray20,
borderSize: Dp = 0.5.dp,
radius: Dp = 24.dp
) {
Box(
modifier = modifier
.size(size)
.clip(SquircleShape())
.background(borderColor),
contentAlignment = Alignment.Center
) {
Image(
painter = rememberAsyncImagePainter(
model = imageRequest,
contentScale = ContentScale.Crop
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(SquircleShape())
.background(backgroundColor)
.size(size - borderSize - borderSize),
)
}
}
In general, it works fine like:
But if I navigate other pages again and again repeatedly, the Squircle images are gone!:
There is no exception or meaningful logs.
It doesn't happen always.
Occurrence frequency maybe 1/10?
And It occurs on Android 10.
Somebody help me, please?
I am currently developing a Sokoban game in Kotlin for an Android app and at this point I have an algorithm that generates a matrix of strings where each string refers to a specific game element, e.g., "#" - Wall, "0" - Floor, "#" - Player, "$" - Box, etc...
Based on that matrix, I am using Jetpack Compose to draw in a canvas rectangles with different colors so I can distinguish the elements (wall, floor, ...) of the Sokoban game.
At the moment, this is the result:
Elements represented by rectangles
However, my final goal is to replace that colored rectangles, with images (.png files). Example:
Expected result
How can I do that?
Here's my code:
class SokobanActivity : AppCompatActivity() {
private lateinit var mGameMatrix: Array<Array<String>>
...
#Composable
fun GameCanvas() {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawRect(color = Color.Black)
val squareDim = 64.dp.value
(mGameMatrix.indices).forEach { i ->
(mGameMatrix[i].indices).forEach { j ->
val element = mGameMatrix[i][j]
drawGameElement(
squareColor = when (element) {
//Wall
"#" -> Color.Red
//Floor
"0" -> Color.Cyan
//Box
"$" -> Color.Green
//Box-objective
"." -> Color.Yellow
//Player
"#" -> Color.Magenta
else -> Color.Blue
},
Offset(
(i * squareDim) + ((size.width - (mGameMatrix.size * squareDim)) / 2),
(j * squareDim) + ((size.height - (mGameMatrix.size * squareDim)) / 2)
),
size = Size(
width = squareDim,
height = squareDim
)
)
}
}
}
}
private fun DrawScope.drawGameElement(
squareColor: Color,
offset: Offset,
size: Size,
) {
drawRect(
color = squareColor,
topLeft = offset,
size = size
)
}
...
}
You can just draw your vector, png etc. resources on Canvas, like that:
val image = ImageVector.vectorResource(id = iconRes)
val painter = rememberVectorPainter(image = image)
translate(
left = size.width / 2 - painter.intrinsicSize.width / 2,
top = size.height / 2 - painter.intrinsicSize.height / 2
) {
with(painter) {
draw(painter.intrinsicSize)
}
}
Is there a way to make a box with a wavy top with Canvas?
I would like to know if this effect can be achieved directly with a Canvas, it is not necessary to have a scrolling animation.
It's not quite clear why you're talking about Canvas. To crop a view like this, you can use a custom Shape and apply it to your view with Modifier.clip. Here's a shape you can use:
class WavyShape(
private val period: Dp,
private val amplitude: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
val wavyPath = Path().apply {
val halfPeriod = with(density) { period.toPx() } / 2
val amplitude = with(density) { amplitude.toPx() }
moveTo(x = -halfPeriod / 2, y = amplitude)
repeat(ceil(size.width / halfPeriod + 1).toInt()) { i ->
relativeQuadraticBezierTo(
dx1 = halfPeriod / 2,
dy1 = 2 * amplitude * (if (i % 2 == 0) 1 else -1),
dx2 = halfPeriod,
dy2 = 0f,
)
}
lineTo(size.width, size.height)
lineTo(0f, size.height)
}
val boundsPath = Path().apply {
addRect(Rect(offset = Offset.Zero, size = size))
}
op(wavyPath, boundsPath, PathOperation.Intersect)
})
}
If you really need to use this inside Canvas for some reason, you can pass the same Path that I create inside WavyShape to DrawScope.clipPath, so that the contents of the clipPath block will be clipped.
Apply custom shape to your Image or any other view:
Image(
painter = painterResource(id = R.drawable.my_image_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clip(WavyShape(period = 100.dp, amplitude = 50.dp))
)
Result:
In the past, a kind of animation could be included in the text, in which if the text exceeded the limits, it would automatically scroll horizontally. This was done by including: android:ellipsize="marquee", and the result was something similar to the one shown here:
The problem is that in Jetpack Compose I don't see a way to include that option inside the Composable Text, there is the TextOverflow that includes the Clip, Ellipsis or Visible options, but I don't know if there is a way to include or use the "Marquee" option in Jetpack Compose. Is there any way to do it?
Modifier.basicMarquee was introduced in 1.4.0-alpha04. It's gonna animate position of your content if it doesn't fit the container width. Here's usage example:
Text(
LoremIpsum().values.first().take(10),
maxLines = 1,
modifier = Modifier
.width(50.dp)
.basicMarquee()
)
It's not gonna add gradient edges, as XML attribute does. The tricky part is that this modifier doesn't has any state for you to know, if content fits the bounds or not, so you can't optionally draw the gradient. Maybe they'll add it later.
Below solution would work prior Compose 1.4 and also would only add gradient in case content doesn't fit the container.
You will need TargetBasedAnimation, which will update the text offset, and SubcomposeLayout, which lies under most collections. Inside you can define the size of the text, and also place the second similar Text, which will appear from the right edge.
#Composable
fun MarqueeText(
text: String,
modifier: Modifier = Modifier,
textModifier: Modifier = Modifier,
gradientEdgeColor: Color = Color.White,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
val createText = #Composable { localModifier: Modifier ->
Text(
text,
textAlign = textAlign,
modifier = localModifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = 1,
onTextLayout = onTextLayout,
style = style,
)
}
var offset by remember { mutableStateOf(0) }
val textLayoutInfoState = remember { mutableStateOf<TextLayoutInfo?>(null) }
LaunchedEffect(textLayoutInfoState.value) {
val textLayoutInfo = textLayoutInfoState.value ?: return#LaunchedEffect
if (textLayoutInfo.textWidth <= textLayoutInfo.containerWidth) return#LaunchedEffect
val duration = 7500 * textLayoutInfo.textWidth / textLayoutInfo.containerWidth
val delay = 1000L
do {
val animation = TargetBasedAnimation(
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = duration,
delayMillis = 1000,
easing = LinearEasing,
),
repeatMode = RepeatMode.Restart
),
typeConverter = Int.VectorConverter,
initialValue = 0,
targetValue = -textLayoutInfo.textWidth
)
val startTime = withFrameNanos { it }
do {
val playTime = withFrameNanos { it } - startTime
offset = (animation.getValueFromNanos(playTime))
} while (!animation.isFinishedFromNanos(playTime))
delay(delay)
} while (true)
}
SubcomposeLayout(
modifier = modifier.clipToBounds()
) { constraints ->
val infiniteWidthConstraints = constraints.copy(maxWidth = Int.MAX_VALUE)
var mainText = subcompose(MarqueeLayers.MainText) {
createText(textModifier)
}.first().measure(infiniteWidthConstraints)
var gradient: Placeable? = null
var secondPlaceableWithOffset: Pair<Placeable, Int>? = null
if (mainText.width <= constraints.maxWidth) {
mainText = subcompose(MarqueeLayers.SecondaryText) {
createText(textModifier.fillMaxWidth())
}.first().measure(constraints)
textLayoutInfoState.value = null
} else {
val spacing = constraints.maxWidth * 2 / 3
textLayoutInfoState.value = TextLayoutInfo(
textWidth = mainText.width + spacing,
containerWidth = constraints.maxWidth
)
val secondTextOffset = mainText.width + offset + spacing
val secondTextSpace = constraints.maxWidth - secondTextOffset
if (secondTextSpace > 0) {
secondPlaceableWithOffset = subcompose(MarqueeLayers.SecondaryText) {
createText(textModifier)
}.first().measure(infiniteWidthConstraints) to secondTextOffset
}
gradient = subcompose(MarqueeLayers.EdgesGradient) {
Row {
GradientEdge(gradientEdgeColor, Color.Transparent)
Spacer(Modifier.weight(1f))
GradientEdge(Color.Transparent, gradientEdgeColor)
}
}.first().measure(constraints.copy(maxHeight = mainText.height))
}
layout(
width = constraints.maxWidth,
height = mainText.height
) {
mainText.place(offset, 0)
secondPlaceableWithOffset?.let {
it.first.place(it.second, 0)
}
gradient?.place(0, 0)
}
}
}
#Composable
private fun GradientEdge(
startColor: Color, endColor: Color,
) {
Box(
modifier = Modifier
.width(10.dp)
.fillMaxHeight()
.background(
brush = Brush.horizontalGradient(
0f to startColor, 1f to endColor,
)
)
)
}
private enum class MarqueeLayers { MainText, SecondaryText, EdgesGradient }
private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int)
Usage:
MarqueeText(LoremIpsum().values.first().take(90))
Result:
Simple solution yet not perfect
val scrollState = rememberScrollState()
var shouldAnimate by remember {
mutableStateOf(true)
}
LaunchedEffect(key1 = shouldAnimated){
scrollState.animateScrollTo(
scrollState.maxValue,
animationSpec = tween(10000, 200, easing = CubicBezierEasing(0f,0f,0f,0f))
)
scrollState.scrollTo(0)
shouldAnimated = !shouldAnimated
}
Text(
text = value,
color = Color.White,
fontSize = 10.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.horizontalScroll(scrollState, false)
)
Starting from 1.4.0-alpha04 you can use the basicMarquee() modifier:
// Marquee only animates when the content doesn't fit in the max width.
Column(Modifier.width(30.dp)) {
Text("hello world hello world hello",
Modifier.basicMarquee())
}
If you want to add a fade effect at the edges you can use:
Text(
"the quick brown fox jumped over the lazy dogs",
Modifier
.widthIn(max = edgeWidth * 4)
// Rendering to an offscreen buffer is required to get the faded edges' alpha to be
// applied only to the text, and not whatever is drawn below this composable (e.g. the
// window).
.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
.drawWithContent {
drawContent()
drawFadedEdge(leftEdge = true)
drawFadedEdge(leftEdge = false)
}
.basicMarquee(
// Animate forever.
iterations = Int.MAX_VALUE,
spacing = MarqueeSpacing(0.dp)
)
.padding(start = edgeWidth)
)
with:
val edgeWidth = 32.dp
fun ContentDrawScope.drawFadedEdge(leftEdge: Boolean) {
val edgeWidthPx = edgeWidth.toPx()
drawRect(
topLeft = Offset(if (leftEdge) 0f else size.width - edgeWidthPx, 0f),
size = Size(edgeWidthPx, size.height),
brush = Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.Black),
startX = if (leftEdge) 0f else size.width,
endX = if (leftEdge) edgeWidthPx else size.width - edgeWidthPx
),
blendMode = BlendMode.DstIn
)
}
You can refer to https://issuetracker.google.com/issues/139321650 for this. The code is available at : https://android-review.googlesource.com/c/platform/frameworks/support/+/2334291/20/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
It adds an extension function for Modifier.basicMarquee(parameters)
The code works perfectly fine for Text composable.
EDIT: You can refer here as well->
const val DefaultMarqueeIterations: Int = 3
const val DefaultMarqueeDelayMillis: Int = 1_200
val DefaultMarqueeSpacing: MarqueeSpacing = MarqueeSpacing.fractionOfContainer(1f / 3f)
val DefaultMarqueeVelocity: Dp = 30.dp
/**
* Applies an animated marquee effect to the modified content if it's too wide to fit in the
* available space. This modifier has no effect if the content fits in the max constraints. The
* content will be measured with unbounded width.
*
* When the animation is running, it will restart from the initial state any time:
* - any of the parameters to this modifier change, or
* - the content or container size change.
*
* The animation only affects the drawing of the content, not its position. The offset returned by
* the [LayoutCoordinates] of anything inside the marquee is undefined relative to anything outside
* the marquee, and may not match its drawn position on screen. This modifier also does not
* currently support content that accepts position-based input such as pointer events.
*
*
* #param iterations The number of times to repeat the animation. `Int.MAX_VALUE` will repeat
* forever, and 0 will disable animation.
* #param animationMode Whether the marquee should start animating [Immediately] or only
* [WhileFocused].
* #param delayMillis The duration to wait before starting each subsequent iteration, in millis.
* #param initialDelayMillis The duration to wait before starting the first iteration of the
* animation, in millis. By default, there will be no initial delay if [animationMode] is
* [Immediately], otherwise the initial delay will be [delayMillis].
* #param spacing A [MarqueeSpacing] that specifies how much space to leave at the end of the
* content before showing the beginning again.
* #param velocity The speed of the animation in dps / second.
*/
fun Modifier.basicMarquee(
iterations: Int = DefaultMarqueeIterations,
animationMode: MarqueeAnimationMode = Immediately,
delayMillis: Int = DefaultMarqueeDelayMillis,
initialDelayMillis: Int = if (animationMode == Immediately) delayMillis else 0,
spacing: MarqueeSpacing = DefaultMarqueeSpacing,
velocity: Dp = DefaultMarqueeVelocity
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "basicMarquee"
properties["iterations"] = iterations
properties["animationMode"] = animationMode
properties["delayMillis"] = delayMillis
properties["initialDelayMillis"] = initialDelayMillis
properties["spacing"] = spacing
properties["velocity"] = velocity
}
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val modifier = remember(
iterations,
delayMillis,
initialDelayMillis,
velocity,
spacing,
animationMode,
density,
layoutDirection,
) {
MarqueeModifier(
iterations = iterations,
delayMillis = delayMillis,
initialDelayMillis = initialDelayMillis,
velocity = velocity * if (layoutDirection == Ltr) 1f else -1f,
spacing = spacing,
animationMode = animationMode,
density = density
)
}
LaunchedEffect(modifier) {
modifier.runAnimation()
}
return#composed modifier
}
private class MarqueeModifier(
private val iterations: Int,
private val delayMillis: Int,
private val initialDelayMillis: Int,
private val velocity: Dp,
private val spacing: MarqueeSpacing,
private val animationMode: MarqueeAnimationMode,
private val density: Density,
) : Modifier.Element, LayoutModifier, DrawModifier, FocusEventModifier {
private var contentWidth by mutableStateOf(0)
private var containerWidth by mutableStateOf(0)
private var hasFocus by mutableStateOf(false)
private val offset = Animatable(0f)
private val direction = sign(velocity.value)
private val spacingPx by derivedStateOf {
with(spacing) {
density.calculateSpacing(contentWidth, containerWidth)
}
}
private val firstCopyVisible by derivedStateOf {
when (direction) {
1f -> offset.value < contentWidth
else -> offset.value < containerWidth
}
}
private val secondCopyVisible by derivedStateOf {
when (direction) {
1f -> offset.value > (contentWidth + spacingPx) - containerWidth
else -> offset.value > spacingPx
}
}
private val secondCopyOffset: Float by derivedStateOf {
when (direction) {
1f -> contentWidth + spacingPx
else -> -contentWidth - spacingPx
}.toFloat()
}
private val contentWidthPlusSpacing: Float?
get() {
// Don't animate if content fits. (Because coroutines, the int will get boxed anyway.)
if (contentWidth <= containerWidth) return null
if (animationMode == WhileFocused && !hasFocus) return null
return (contentWidth + spacingPx).toFloat()
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val childConstraints = constraints.copy(maxWidth = Constraints.Infinity)
val placeable = measurable.measure(childConstraints)
containerWidth = constraints.constrainWidth(placeable.width)
contentWidth = placeable.width
return layout(containerWidth, placeable.height) {
// Placing the marquee content in a layer means we don't invalidate the parent draw
// scope on every animation frame.
placeable.placeWithLayer(x = (-offset.value * direction).roundToInt(), y = 0)
}
}
override fun ContentDrawScope.draw() {
val clipOffset = offset.value * direction
clipRect(left = clipOffset, right = clipOffset + containerWidth) {
if (firstCopyVisible) {
this#draw.drawContent()
}
if (secondCopyVisible) {
translate(left = secondCopyOffset) {
this#draw.drawContent()
}
}
}
}
override fun onFocusEvent(focusState: FocusState) {
hasFocus = focusState.hasFocus
}
suspend fun runAnimation() {
if (iterations <= 0) {
// No animation.
return
}
snapshotFlow { contentWidthPlusSpacing }.collectLatest { contentWithSpacingWidth ->
// Don't animate when the content fits.
if (contentWithSpacingWidth == null) return#collectLatest
val spec = createMarqueeAnimationSpec(
iterations,
contentWithSpacingWidth,
initialDelayMillis,
delayMillis,
velocity,
density
)
offset.snapTo(0f)
try {
offset.animateTo(contentWithSpacingWidth, spec)
} finally {
offset.snapTo(0f)
}
}
}
}
private fun createMarqueeAnimationSpec(
iterations: Int,
targetValue: Float,
initialDelayMillis: Int,
delayMillis: Int,
velocity: Dp,
density: Density
): AnimationSpec<Float> {
val pxPerSec = with(density) { velocity.toPx() }
val singleSpec = velocityBasedTween(
velocity = pxPerSec.absoluteValue,
targetValue = targetValue,
delayMillis = delayMillis
)
// Need to cancel out the non-initial delay.
val startOffset = StartOffset(-delayMillis + initialDelayMillis)
return if (iterations == Int.MAX_VALUE) {
infiniteRepeatable(singleSpec, initialStartOffset = startOffset)
} else {
repeatable(iterations, singleSpec, initialStartOffset = startOffset)
}
}
/**
* Calculates a float [TweenSpec] that moves at a constant [velocity] for an animation from 0 to
* [targetValue].
*
* #param velocity Speed of animation in px / sec.
*/
private fun velocityBasedTween(
velocity: Float,
targetValue: Float,
delayMillis: Int
): TweenSpec<Float> {
val pxPerMilli = velocity / 1000f
return tween(
durationMillis = ceil(targetValue / pxPerMilli).toInt(),
easing = LinearEasing,
delayMillis = delayMillis
)
}
/** Specifies when the [basicMarquee] animation runs. */
#JvmInline
value class MarqueeAnimationMode private constructor(private val value: Int) {
override fun toString(): String = when (this) {
Immediately -> "Immediately"
WhileFocused -> "WhileFocused"
else -> error("invalid value: $value")
}
companion object {
/** Starts animating immediately, irrespective of focus state. */
#Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
val Immediately = MarqueeAnimationMode(0)
/**
* Only animates while the marquee has focus. This includes when a focusable child in the
* marquee's content is focused.
*/
#Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
val WhileFocused = MarqueeAnimationMode(1)
}
}
/**
* A [MarqueeSpacing] with a fixed size.
*/
fun MarqueeSpacing(spacing: Dp): MarqueeSpacing = MarqueeSpacing { _, _ -> spacing.roundToPx() }
/**
* Defines a [calculateSpacing] method that determines the space after the end of [basicMarquee]
* content before drawing the content again.
*/
fun interface MarqueeSpacing {
/**
* Calculates the space after the end of [basicMarquee] content before drawing the content
* again.
*
* This is a restartable method: any state used to calculate the result will cause the spacing
* to be re-calculated when it changes.
*
* #param contentWidth The width of the content inside the marquee, in pixels. Will always be
* larger than [containerWidth].
* #param containerWidth The width of the marquee itself, in pixels. Will always be smaller than
* [contentWidth].
* #return The space in pixels between the end of the content and the beginning of the content
* when wrapping.
*/
fun Density.calculateSpacing(
contentWidth: Int,
containerWidth: Int
): Int
companion object {
/**
* A [MarqueeSpacing] that is a fraction of the container's width.
*/
fun fractionOfContainer(fraction: Float): MarqueeSpacing = MarqueeSpacing { _, width ->
(fraction * width).roundToInt()
}
}
}
and you can use it like this:
Text(text = "marquee text",modifier = Modifier.basicMarquee())
My application needs a ProgressBar, and I am trying to implement it with Jetpack Compose, so either I need a builtin ProgressBar support (I didn't find it) or there should be a mechanism to display plain Android Widgets with Compose. Is anything of this possible?
Ofcourse, we have Progress Bars in Jetpack Compose:
CircularProgressIndicator: Displays progress bar as Circle. It is indeterminate. Themed to Primary color set in styles. Another variant is determinate that takes progress in argument as Float (0.0f - 1.0f)
Example:
// Indeterminate
CircularProgressIndicator()
// Determinate
CircularProgressIndicator(progress = 0.5f)
LinearProgressIndicator: Displays progress bar as line. It is indeterminate. Themed to Primary color set in styles. Another variant is determinate that takes progress in argument as Float (0.0f - 1.0f)
Example:
// Indeterminate
LinearProgressIndicator()
// Determinate
LinearProgressIndicator(progress = 0.5f)
With 1.0.x you can use the LinearProgressIndicator or CircularProgressIndicator
// Indeterminate
CircularProgressIndicator()
LinearProgressIndicator()
// Determinate
CircularProgressIndicator(progress = ..)
LinearProgressIndicator(progress = ..)
Example:
var progress by remember { mutableStateOf(0.1f) }
LinearProgressIndicator(
backgroundColor = Color.White,
progress = progress,
color = blue700
)
To update the value you can use something like:
// { if (progress < 1f) progress += 0.1f }
For rounded corners we can use this code (It's the same like LinearProgress but with one small correction - in drawLine we use param StrokeCap.Round for rounding)
#Composable
fun LinearRoundedProgressIndicator(
/*#FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
backgroundColor: Color = color.copy(alpha = ProgressIndicatorDefaults.IndicatorBackgroundOpacity)
) {
val linearIndicatorHeight = ProgressIndicatorDefaults.StrokeWidth
val linearIndicatorWidth = 240.dp
Canvas(
modifier
.progressSemantics(progress)
.size(linearIndicatorWidth, linearIndicatorHeight)
.focusable()
) {
val strokeWidth = size.height
drawRoundedLinearIndicatorBackground(backgroundColor, strokeWidth)
drawRoundedLinearIndicator(0f, progress, color, strokeWidth)
}
}
private fun DrawScope.drawRoundedLinearIndicatorBackground(
color: Color,
strokeWidth: Float
) = drawRoundedLinearIndicator(0f, 1f, color, strokeWidth)
private fun DrawScope.drawRoundedLinearIndicator(
startFraction: Float,
endFraction: Float,
color: Color,
strokeWidth: Float
) {
val width = size.width
val height = size.height
// Start drawing from the vertical center of the stroke
val yOffset = height / 2
val isLtr = layoutDirection == LayoutDirection.Ltr
val barStart = (if (isLtr) startFraction else 1f - endFraction) * width
val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width
// Progress line
drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth, StrokeCap.Round)
}
custom linear progress indicator
#Composable
fun CustomLinearProgressIndicator(
modifier: Modifier = Modifier,
progress: Float,
progressColor: Color = orangeColor,
backgroundColor: Color = orangeColor.copy(0.24f),
clipShape: Shape = RoundedCornerShape(16.dp)
) {
Box(
modifier = modifier
.clip(clipShape)
.background(backgroundColor)
.height(8.dp)
) {
Box(
modifier = Modifier
.background(progressColor)
.fillMaxHeight()
.fillMaxWidth(progress)
)
}
}
you can use this library which supports thumb, round corners, and animations.
https://github.com/KevinnZou/compose-progressIndicator
For someone who is looking for a rounded corner linear progress bar,
you can use the Modifier's clip function.
LinearProgressIndicator(
progress = 0.5f,
modifier = Modifier.height(10.dp).clip(RoundedCornerShape(10.dp))
)