How to implement smooth scrolling Canvas animation in Compose? - android

I send a stream of shapes to the drawing pipeline. At any point in time, shapes contains the number of items that can fill the screen's width or less.
Here's the drawing widget:
#Composable
fun ShapeWidget(shapes: List<Shape>, size: Float, spacing: Float, screenWidth: Float) {
val color = MaterialTheme.colorScheme.tertiary
Canvas(
modifier = Modifier.fillMaxWidth(),
onDraw = {
shapes.forEachIndexed { i, s ->
val x = screenWidth - ((size + spacing) * i)
when (s.type) {
Type.RECT -> {
drawRect(
color = color,
size = Size(size, size),
topLeft = Offset(x, this.center.y - size / 2),
)
}
Type.CIRCLE -> {
drawCircle(
color = color,
radius = size / 2,
center = Offset(x + spacing * 2, center.y)
)
}
}
}
}
)
}
I want to achieve an effect similar to the marquee text effect in Android Views. The shapes start drawing from the right (of the screen) and scroll to the left as more shapes are drawn.
The code above does that, but the animation is janky; it doesn't give the illusion that the shapes are moving. It looks like the shapes are jumping, which is not what I want. This is what it looks like currently.
Please, do you have an idea how to fix this?

Related

How to transform Image Composable to match 3 touch points in Compose

I am currently playing with my old Instant Lab device and I am trying to recreate parts of the old app in jetpack compose.
A feature of the device is to detect 3 touch points on the screen in order to create the border of the image to display.
I was able to dectect the 3 touche points using jetpack compose and find the coordinate (x, y) of each touch points :
Now I would like to display my image between these touch points. I know that I need to use the Image Composable in order to display. But I do not know how to apply the right transformation in order to display this composable between these 3 points using rotation and absolute position (?).
Expected result:
Thank you in advance for your help.
Edit:
I tried using a custom shape I apply to a surface with the following composable :
#Composable
private fun Exposing(pointersCoordinates: PointersCoordinates)
{
val exposureShape = GenericShape { _, _ ->
moveTo(pointersCoordinates.xTopLeft(), pointersCoordinates.yTopLeft())
lineTo(pointersCoordinates.xTopRight(), pointersCoordinates.yTopRight())
lineTo(pointersCoordinates.xBottomRight(), pointersCoordinates.yBottomRight())
lineTo(pointersCoordinates.xBottomLeft(), pointersCoordinates.yBottomLeft())
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
Surface(
modifier = Modifier.fillMaxSize(),
shape = exposureShape,
color = Color.Yellow,
border = BorderStroke(1.dp, Color.Red)
) {
Image(
modifier = Modifier.fillMaxSize(),
bitmap = viewModel?.bitmap?.asImageBitmap() ?: ImageBitmap(0, 0),
contentDescription = "photo"
)
}
}
}
It's working correctly :) But is it the best way to do it?
Since you are able to get a Rect from touch points you can use Canvas or Modifier.drawWithContent{}.
Clip image and draw
If you wish to clip your image based on rectangle you can check out this answer. Whit BlendModes you can clip not only to rectangle or shapes that easy to create but shapes you get from web or image
How to clip or cut a Composable?
Another approach for clipping is using clip() function of DrawScope, this approach only clips to a Rect.
Also you can use Modifier.clip() with custom shape to clip it as required as in this answer
Draw without clippin
If you don't want to clip your image draw whole image insider rect you can do it with dstOffset with dstSize or translate with dstSize
#Composable
private fun DrawImageWithTouchSample() {
val rect = Rect(topLeft = Offset(100f, 100f), bottomRight = Offset(1000f, 1000f))
val modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
// Tap here to get points
}
}
val image = ImageBitmap.imageResource(id = R.drawable.landscape5)
Canvas(modifier = modifier) {
// Clip image
clipRect(
left = rect.left,
top = rect.top,
right = rect.right,
bottom = rect.bottom
){
drawImage(image = image)
}
// Not clipping image
// drawImage(
// image = image,
// dstOffset = IntOffset(x = rect.left.toInt(), y = rect.top.toInt()),
// dstSize = IntSize(width = rect.width.toInt(), height = rect.height.toInt())
// )
//
translate(
left = rect.left,
top = rect.top + 1000
){
drawImage(
image = image,
dstSize = IntSize(width = rect.width.toInt(), height = rect.height.toInt())
)
}
}
}
Image on top is clipped with clipRect while second one is scaled to fit inside rect because of dstSize
You don't need to use Image. A Box clipped to a circle and with a grey-ish background would do. Of course, you'll need an Image to display the actual image you'll be dragging the points on. Here's a sample implementation:
val x by remember { mutableStateOf (Offset(...)) } // Initial Offset
val y by ...
val z by ...
// The image is occupying all of this composable, and these will be positioned ABOVE the image composable using order of positioning.
Layout(
content = {
Box(/*Copy From Later*/)
Box(...)
Box(...)
}
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) } // Default Constraining.
layout(constraints.maxWidth, constraints.maxHeight){
measurables[0].place(x.x, x.y)
measurables[1].place(y.x, y.y)
measurables[2].place(z.x, z.y)
}
}
Layout the box like so,
Box(
modifier = Modifier
.clip(CircleShape)
.size(5.dp) // Change per need
.pointerInput(Unit) {
detectDragGestures { change, _ ->
x = change.position // similarly y for second, and z for the third box
}
}
)
This should track/update/position the points wherever you drag them. All the points are individually determined by their own state-holders. You'll need to add this pointerInput logic to every Box, but it would likely be better if you just created a single function to be invoked based on an index, unique to each Box, but that's not something required to be covered here.

Jetpack Compose: Vertical align drawText with other drawn elements

I'm drawing a custom progress bar, the idea is to have a vertical "line" of circles with space between them like this:
As you progress, the circles change color and size, and the text ("60" in the image I shared)is displayed alongside the top level circle. With the current text size it seems centered enough, but if I change the text size it is noticeable that is bottom aligned, like this:
Here is the code:
Box(
modifier
.aspectRatio(1f)
.drawWithCache {
onDrawBehind {
val height = size.height
val space = height / 10
for (i in 1..numberOfCircles) {
val onBrush =
if (i <= progressLevel) progressBrush(progressLevel) else Color.Gray.toBrush()
val circleSize =
if (i == progressLevel) circleRadius.value * 2.5F else if (i == progressLevel - 1) circleRadius.value * 1.5F else circleRadius.value
drawProgressCircle(
onBrush,
sizeArray[i - 1].value,
size.height - (space * i)
)
if (i == state.level) {
drawText(
onBrush,
circleSize,
size.height - (space * i),
(progressLevel * 10).toString()
)
}
}
}
},
contentAlignment = Alignment.Center
)
The draw circles function:
fun DrawScope.drawProgressCircle(
brush: Brush,
radius: Float,
place: Float
) {
drawCircle(
brush = brush,
radius = radius,
center = Offset(x = size.width/2, y = place),
)
}
The draw text function:
fun DrawScope.drawText(
brush: Brush,
radius: Float,
place: Float,
text: String
) {
drawContext.canvas.nativeCanvas.apply {
drawText(
text,
size.width/2.2F,
place + radius,
Paint().apply {
textSize = 20.sp.toPx()
color = Color.White.toArgb()
textAlign = Paint.Align.RIGHT
}
)
}
}
How can I keep the text vertically aligned at the last circle center with any text size?

Confusing behaviour by the 'rotate' transformation of Compose Canvas

I have a simple Composable Canvas that draws a shape multiple times in different positions, and I wish to apply rotation to each iteration of the shape as per a particular algorithm. However, I am unable to fully control the positioning of the shapes in the canvas since the rotate transformation seems to apply a translation trasformation of its own. Here's what I have
#Preview
#Composable
fun CanvasCollosum() {
val painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.tick))
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
Canvas(modifier = Modifier.height(200.dp).aspectRatio(1f)) {
with(painter) {
repeat(12) {
(it * 30f).let { angle ->
translate(
top = 0f, // Some translation is still applied by 'rotate'
left = 0f
) {
rotate(angle) {
draw(
Size(100f, 100f),
colorFilter = ColorFilter.tint(Color.White)
)
}
}
}
}
}
}
}
}
Hence, for some reason, the positionings of all of the shapes here (12 in total) assume the shape of a circle, which is not at all what I expected. I can assume it is something related to the 'pivot' of the rotation. However, it is set, by default, as the center of the Canvas, which seems fairly appropriate. The shape is being rendered far away from the pivot which seems to be causing the translation effect to occur as a side effect, so the question is -- How do I render all my shapes with a fixed rotation, and position them with specific (x, y) cords, given that I have an algorithm to position them relative to the center of the Canvas?
For reference, here's some outputs, displaying a single shape, with the only varying property being the rotation angle
0f Degrees
90f Degrees
180f Degrees
270f Degrees
360f is the same as 0f
This does not seem like something that should be happening here, but it is.
The Square is the Box Composable and the little white thing is the shape in concern.
It is not applying translation, but is rotating around a pivot, which is set to the center of the Box.
rotate has a parameter named pivot that will solve your problem:
#Composable
fun Rotation() {
val painter = rememberVectorPainter(image = Icons.Default.ThumbUp)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Canvas(
modifier = Modifier
.height(200.dp)
.background(color = Color.Red)
.aspectRatio(1f)
) {
with(painter) {
repeat(4) {
rotate(
degrees = (it * 90f),
pivot = Offset(
painter.intrinsicSize.width * 2,
painter.intrinsicSize.height * 2
)
) {
draw(
Size(100f, 100f),
colorFilter = ColorFilter.tint(Color.White)
)
}
}
}
}
}
}
The code above produces this result:
In order to change the point, around which the rotation is done, simple change the coordinates in pivot parameter.

How to create Angular gradient in Jetpack compose?

I want to create an angular gradient like this in Android Jetpack Compose.
The gradient is made in Figma and below is the android code in Figma Inspect.
I could only find resources for the linear gradient with only one angle.
How can I create this Angular gradient in Jetpack compose?
What you need is Brush.sweepGradient with colorStops and setting center correctly. Gradient stops start from 3'o clock, right center, so you need to add 0.25 to each stop and move the ones that pass 1 to start, i moved 2 colors from bottom to top at 0.01, and 0.14
#Composable
private fun SweepGradientExample() {
val colorStops = listOf(
0.01f to Color(0x8C1339FF),
0.14f to Color(0x8CFF13A1),
0.31f to Color(0x8C1380FF),
0.54f to Color(0x8CD013FF),
0.81f to Color(0x8C7B13FF),
).toTypedArray()
val density = LocalDensity.current
val centerX: Float
val centerY: Float
with(density) {
centerX = 161.dp.toPx() / 2
centerY = 97.dp.toPx() / 2
}
val brush = Brush.sweepGradient(
colorStops = colorStops,
center = Offset(centerX, centerY)
)
Box(modifier = Modifier
.size(width = 161.dp, height = 97.dp)
.background(brush)
)
}
Result

Arc/Semicircle Shape in Jetpack Compose

I'm looking to draw an arc type shape in Jetpack Compose but I'm really struggling to figure out the best way to do it. When I use drawArc, I can't seem to start the arc from the bottom left corner, it just starts from left middle. I've also tried drawRoundRect but I can't find a way to just round the top left and top right corners.
I'll attach a picture, I don't need the white handle within the shape. Desired result:
drawRoundRect attempt:
drawArc attempt:
You could make it by using a a canvas with your device width but try to increase arc limits beyond device width. With this code
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val formWidth = (canvasWidth * 2)
val xPos = canvasWidth / 2
drawArc(
Color.Black,
0f,
-180f,
useCenter = true,
size = Size(formWidth, 600f),
topLeft = Offset(x = -xPos, y = canvasHeight - 300)
)
}
Arc at bottom looks like this:
more info
You can draw it like this
Placed it to center for demonstration and border is to show that it doesn't overflow Canvas, if you don't pay attention, Canvas draws out of its borders unless use Modifier.clipbounds() is used on Canvas Modifier.
Trick for drawing half arc is to set two times of Canvas height, since arc is drawn inside a rectangle in principle. Changind height of Modifier of canvas changes height of arc.
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Canvas(
modifier = Modifier
.border(2.dp, Color.Red)
.fillMaxWidth()
.height(80.dp),
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawArc(
Color.Blue,
startAngle = 180f,
sweepAngle = 180f,
useCenter = true,
size = Size(canvasWidth, 2 * canvasHeight)
)
val handleWidth = 200f
val handleHeight = 30f
drawRoundRect(
Color.White,
size = Size(handleWidth, handleHeight),
cornerRadius = CornerRadius(5.dp.toPx(), 5.dp.toPx()),
topLeft = Offset((canvasWidth - handleWidth) / 2, (canvasHeight - handleHeight)/2)
)
}
}
Result
You can check this tutorial about Jetpack Compose which covers Canvas operations in detail.

Categories

Resources