implement a spinning activity indicator with Jetpack Compose - android

This is a share your knowledge, Q&A-style inspired by this question on Reddit and the one linked to stackoverflow
Result is

This is just a sample to show to build spinning progress indicator. Item count(8 or 12), animation duration, spinning item color or color of static items can be customized based on preference.
#Composable
private fun SpinningProgressBar(modifier: Modifier = Modifier) {
val count = 12
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = count.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(count * 80, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Canvas(modifier = modifier.size(48.dp)) {
val canvasWidth = size.width
val canvasHeight = size.height
val width = size.width * .3f
val height = size.height / 8
val cornerRadius = width.coerceAtMost(height) / 2
for (i in 0..360 step 360 / count) {
rotate(i.toFloat()) {
drawRoundRect(
color = Color.LightGray.copy(alpha = .7f),
topLeft = Offset(canvasWidth - width, (canvasHeight - height) / 2),
size = Size(width, height),
cornerRadius = CornerRadius(cornerRadius, cornerRadius)
)
}
}
val coefficient = 360f / count
for (i in 1..4) {
rotate((angle.toInt() + i) * coefficient) {
drawRoundRect(
color = Color.Gray.copy(alpha = (0.2f + 0.2f * i).coerceIn(0f, 1f)),
topLeft = Offset(canvasWidth - width, (canvasHeight - height) / 2),
size = Size(width, height),
cornerRadius = CornerRadius(cornerRadius, cornerRadius)
)
}
}
}
}
I created a library that contains other type of Spinners as can be seen in gif below is available here

Related

How to draw border around the LazyColumn items in Android Compose

There are items() {} sections inside LazyColumn. So I would like to draw a border with rounded corners around each section. Is there any method?
// need to draw a border around the items
LazyColumn {
items(10) {
Row {
// content
}
}
items(5) {
Row {
// content
}
}
}
If you want to add a border to single item just add in your item content a Composable with a border modifier:
items(10) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.border(width = 1.dp, color = Blue200, shape = RoundedCornerShape(8.dp))
.padding(2.dp)
){ /** ... */ }
}
If you want to add a border around all the items block you can create different border modifiers to apply to each items.
Something like:
//border
val strokeWidth: Dp = 2.dp
val strokeColor: Color = Blue500
val cornerRadius: Dp = 8.dp
//background shape
val topShape = RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius)
val bottomShape = RoundedCornerShape(bottomStart = cornerRadius, bottomEnd = cornerRadius)
LazyColumn {
val itemCount = 10
var shape : Shape
var borderModifier : Modifier
items(itemCount) { index ->
when (index) {
0 -> {
//First item. Only top border
shape = topShape
borderModifier = Modifier.topBorder(strokeWidth,strokeColor,cornerRadius)
}
itemCount -1 -> {
//last item. Only bottom border
shape = bottomShape
borderModifier = Modifier.bottomBorder(strokeWidth,strokeColor,cornerRadius)
}
else -> {
//Other items. Only side border
shape = RectangleShape
borderModifier = Modifier.sideBorder(strokeWidth,strokeColor,cornerRadius)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(shape)
.background(Teal200)
.then(borderModifier)
.padding(4.dp)
) {
Text(text = "Item: $index")
}
}
}
where:
fun Modifier.topBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = 0f, y = cornerRadiusPx),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 180f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset.Zero,
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = cornerRadiusPx, y = 0f),
end = Offset(x = width - cornerRadiusPx, y = 0f),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 270f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = width - cornerRadiusPx * 2, y = 0f),
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = width, y = height),
end = Offset(x = width, y = cornerRadiusPx),
strokeWidth = strokeWidthPx
)
}
}
)
fun Modifier.bottomBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = height-cornerRadiusPx),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 90f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = 0f, y = height - cornerRadiusPx * 2),
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = cornerRadiusPx, y = height),
end = Offset(x = width - cornerRadiusPx, y = height),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 0f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = width - cornerRadiusPx * 2, y = height - cornerRadiusPx * 2),
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = width, y = 0f),
end = Offset(x = width, y = height - cornerRadiusPx),
strokeWidth = strokeWidthPx
)
}
}
)
fun Modifier.sideBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = height),
strokeWidth = strokeWidthPx
)
drawLine(
color = color,
start = Offset(x = width, y = 0f),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)
You can draw a border around the whole list, using the modifier border and a RoundedCornerShape:
LazyColumn(modifier.border(width = 1.dp, color = Color.Red, shape = RoundedCornerShape(1.dp)))
Or around every item by applying the same to the rows:
Row(modifier.border(width = 1.dp, color = Color.Green, shape = RoundedCornerShape(1.dp)))

How to divide the stroke of a circle at equal intervals in Jetpack compose canvas?

How to achieve the following result using Compose Canvas.
You can draw these lines using simple trigonometry and drawing with Canvas, Modifier.drawWithContent or Modifier.drawWithCache.
#Composable
private fun CanvasSample() {
Canvas(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
val center = size.width / 2
val outerRadius = center * .8f
val innerRadius = outerRadius * .8f
for (i in 0..360 step 30) {
val xStart = center + (innerRadius * cos(i * DEG_TO_RAD)).toFloat()
val yStart = center + (innerRadius * sin(i * DEG_TO_RAD)).toFloat()
val xEnd = center + (outerRadius * cos(i * DEG_TO_RAD)).toFloat()
val yEnd = center + (outerRadius * sin(i * DEG_TO_RAD)).toFloat()
drawLine(
Color.Red,
start = Offset(xStart, yStart),
end = Offset(xEnd, yEnd),
strokeWidth = 3.dp.toPx(),
cap = StrokeCap.Join
)
}
}
}
const val DEG_TO_RAD = Math.PI / 180f
Image doesn't have round stroke cap, adding it will round line start and end.

How to get transparent while erasing the canvas in Jetpack Compose , now I'm getting white color?

how can I make some parts of canvas transparent? I want user to be able to erase parts of an photo like this link shows to be transparent. my canvas code:
Canvas(
modifier = modifier
.background(Color.Transparent)
) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawImage(
image = bitmap,
srcSize = IntSize(bitmap.width, bitmap.height),
dstSize = IntSize(canvasWidth, canvasHeight)
)
drawPath(
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear,
color = Color.Transparent,
)
restoreToCount(checkPoint)
}
}
What you get as Transparent is Color(0x00000000), white you get is the color of your background, even if you Canvas has transparent background, color of your root or parent Composable is white.
You need to draw checker layout or checker image first, inside Layer you should draw your image and path with BlendMode.Clear
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
val space = 20.dp.roundToPx()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstBitmap,
dstOffset = IntOffset(
space / 2,
space / 2
),
dstSize = IntSize(
canvasWidth - space, canvasHeight - space
)
)
// Source
drawPath(
color = Color.Transparent,
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
Full implementation
#Composable
private fun MyImageDrawer(modifier: Modifier) {
// This is the image to draw onto
val dstBitmap = ImageBitmap.imageResource(id = R.drawable.landscape1)
// Path used for erasing. In this example erasing is faked by drawing with canvas color
// above draw path.
val erasePath = remember { Path() }
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
// This is our motion event we get from touch motion
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
// This is previous motion event before next touch is saved into this current position
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
val drawModifier = modifier
.pointerMotionEvents(Unit,
onDown = { pointerInputChange ->
motionEvent = MotionEvent.Down
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onMove = { pointerInputChange ->
motionEvent = MotionEvent.Move
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onUp = { pointerInputChange ->
motionEvent = MotionEvent.Up
pointerInputChange.consume()
}
)
Canvas(modifier = drawModifier) {
val canvasWidth = size.width.roundToInt()
val canvasHeight = size.height.roundToInt()
// Draw or erase depending on erase mode is active or not
when (motionEvent) {
MotionEvent.Down -> {
erasePath.moveTo(currentPosition.x, currentPosition.y)
previousPosition = currentPosition
}
MotionEvent.Move -> {
erasePath.quadraticBezierTo(
previousPosition.x,
previousPosition.y,
(previousPosition.x + currentPosition.x) / 2,
(previousPosition.y + currentPosition.y) / 2
)
previousPosition = currentPosition
}
MotionEvent.Up -> {
erasePath.lineTo(currentPosition.x, currentPosition.y)
currentPosition = Offset.Unspecified
previousPosition = currentPosition
motionEvent = MotionEvent.Idle
}
else -> Unit
}
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
val space = 20.dp.roundToPx()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstBitmap,
dstOffset = IntOffset(
space / 2,
space / 2
),
dstSize = IntSize(
canvasWidth - space, canvasHeight - space
)
)
// Source
drawPath(
color = Color.Transparent,
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
}
}
Outcome

Jetpack Compose watermark or write on Bitmap with androidx.compose.ui.graphics.Canvas?

With androidx.compose.foundation.Canvas, default Canvas for Jetpack Compose, or Spacer with Modifier.drawBehind{} under the hood
#Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
Spacer(modifier.drawBehind(onDraw
correctly refreshes drawing on Canvas when mutableState Offset changes
var offset by remember {
mutableStateOf(Offset(bitmapWidth / 2f, bitmapHeight / 2f))
}
Canvas(modifier = canvasModifier.fillMaxSize()) {
val canvasWidth = size.width.roundToInt()
val canvasHeight = size.height.roundToInt()
drawImage(
image = dstBitmap,
srcSize = IntSize(dstBitmap.width, dstBitmap.height),
dstSize = IntSize(canvasWidth, canvasHeight)
)
drawCircle(
center = offset,
color = Color.Red,
radius = canvasHeight.coerceAtMost(canvasWidth) / 8f,
)
}
With androidx.compose.ui.graphics.Canvas, Canvas that takes an ImageBitmap as argument and draws to as in description of it
Create a new Canvas instance that targets its drawing commands to the
provided ImageBitmap
I add full implementation to test this out easily and much appreciated if you come up with a solution.
#Composable
fun NativeCanvasSample2(imageBitmap: ImageBitmap, modifier: Modifier) {
BoxWithConstraints(modifier) {
val imageWidth = constraints.maxWidth
val imageHeight = constraints.maxHeight
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
var offset by remember {
mutableStateOf(Offset(bitmapWidth / 2f, bitmapHeight / 2f))
}
val canvasModifier = Modifier.pointerMotionEvents(
Unit,
onDown = {
val position = it.position
val offsetX = position.x * bitmapWidth / imageWidth
val offsetY = position.y * bitmapHeight / imageHeight
offset = Offset(offsetX, offsetY)
it.consumeDownChange()
},
onMove = {
val position = it.position
val offsetX = position.x * bitmapWidth / imageWidth
val offsetY = position.y * bitmapHeight / imageHeight
offset = Offset(offsetX, offsetY)
it.consumePositionChange()
},
delayAfterDownInMillis = 20
)
val canvas: androidx.compose.ui.graphics.Canvas = Canvas(imageBitmap)
val paint1 = remember {
Paint().apply {
color = Color.Red
}
}
canvas.apply {
val nativeCanvas = this.nativeCanvas
val canvasWidth = nativeCanvas.width.toFloat()
val canvasHeight = nativeCanvas.height.toFloat()
drawCircle(
center = offset,
radius = canvasHeight.coerceAtMost(canvasWidth) / 8,
paint = paint1
)
}
Image(
modifier = canvasModifier,
bitmap = imageBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
Text(
"Offset: $offset",
modifier = Modifier.align(Alignment.BottomEnd),
color = Color.White,
fontSize = 16.sp
)
}
}
First issue it never refreshes Canvas without Text or something else reading Offset.
Second issue is as in the image below. It doesn't clear previous drawing on Image, i tried every possible solution in this question thread but none of them worked.
I tried drawing image with BlendMode, drawColor(Color.TRANSPARENT,Mode.Multiply) with native canvas and many combinations still not able to have the same result with Jetpack Compose Canvas.
val erasePaint = remember {
Paint().apply {
color = Color.Transparent
blendMode = BlendMode.Clear
}
}
with(canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawImage(imageBitmap, topLeftOffset = Offset.Zero, erasePaint)
drawCircle(
center = offset,
radius = canvasHeight.coerceAtMost(canvasWidth) / 8,
paint = paint1
)
restoreToCount(checkPoint)
}
I need to use androidx.compose.ui.graphics.Canvas as you can see operations on Canvas are reflected to Bitmap and using this i'm planning to create foundation for cropping Bitmap
I finally, after 6 months, figured out how it can be done and how you can modify Bitmap instance using androidx.compose.ui.graphics.Canvas
First create an empty mutable bitmap with same dimensions of original bitmap. This is what we will draw on. The trick here is not sending a real bitmap but an empty bitmap
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
val bmp: Bitmap = remember {
Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
}
Then since we draw nothing at the base we can use drawColor(android.graphics.Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
to clear on each draw then draw image and apply any blend mode using Paint
val paint = remember {
Paint()
}
val erasePaint = remember {
Paint().apply {
color = Color.Red
blendMode = BlendMode.SrcIn
}
}
canvas.apply {
val nativeCanvas = this.nativeCanvas
val canvasWidth = nativeCanvas.width.toFloat()
val canvasHeight = nativeCanvas.height.toFloat()
with(canvas.nativeCanvas) {
drawColor(android.graphics.Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawCircle(
center = offset,
radius = 400f,
paint = paint
)
drawImageRect(
image = imageBitmap,
dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),
paint = erasePaint
)
}
}
Finally draw bitmap we used in Canvas to Image Composable using
Image(
modifier = canvasModifier.border(2.dp, Color.Green),
bitmap = bmp.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
or you can save this modified ImageBitmap with watermark or any overlay you draw into canvas
Full implementation
#Composable
fun NativeCanvasSample2(imageBitmap: ImageBitmap, modifier: Modifier) {
BoxWithConstraints(modifier) {
val imageWidth = constraints.maxWidth
val imageHeight = constraints.maxHeight
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
var offset by remember {
mutableStateOf(Offset(bitmapWidth / 2f, bitmapHeight / 2f))
}
val bmp: Bitmap = remember {
Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
}
val canvas: Canvas = remember {
Canvas(bmp.asImageBitmap())
}
val paint = remember {
Paint()
}
val erasePaint = remember {
Paint().apply {
color = Color.Red
blendMode = BlendMode.SrcIn
}
}
canvas.apply {
val nativeCanvas = this.nativeCanvas
val canvasWidth = nativeCanvas.width.toFloat()
val canvasHeight = nativeCanvas.height.toFloat()
with(canvas.nativeCanvas) {
drawColor(android.graphics.Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawCircle(
center = offset,
radius = 400f,
paint = paint
)
drawImageRect(
image = imageBitmap,
dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),
paint = erasePaint
)
}
}
val canvasModifier = Modifier.pointerMotionEvents(
Unit,
onDown = {
val position = it.position
val offsetX = position.x * bitmapWidth / imageWidth
val offsetY = position.y * bitmapHeight / imageHeight
offset = Offset(offsetX, offsetY)
it.consume()
},
onMove = {
val position = it.position
val offsetX = position.x * bitmapWidth / imageWidth
val offsetY = position.y * bitmapHeight / imageHeight
offset = Offset(offsetX, offsetY)
it.consume()
},
delayAfterDownInMillis = 20
)
Image(
modifier = canvasModifier.border(2.dp, Color.Green),
bitmap = bmp.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
}
Result

Wavy box in Jetpack compose

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:

Categories

Resources