How to add an icon to canvas in Jetpack Compose? - android

I am trying to add and icon from resource in Canvas DrawScope.
The nearest solution I found is drawImage(), but it doesn't work for my case. Also I cannot use the normal Icon() composable inside the DrawScope. So is there any work around to display an icon inside canvas, similarly to the way we do it with composable:
import androidx.compose.material.Icon
Icon(Icons.Rounded.Menu, contentDescription = "Localized description")

Icons.Rounded.Menu is a VectorImage and you can wrap it into a VectorPainter.
You can use something like:
val painter = rememberVectorPainter(Icons.Rounded.Menu)
Canvas(modifier = Modifier.fillMaxSize()) {
with(painter) {
draw(painter.intrinsicSize)
}
}

Drawscope extension for abstract class Painter has size, alpha and colorFilter params. You can change these params. If you wish to change draw position of Icon, it' drawn top left (0,0) by default you can use translate function or other functions such as rotate or scale for further operations
val painter = rememberVectorPainter(Icons.Rounded.Menu)
Canvas(modifier = Modifier.fillMaxSize()) {
translate(left = 0f, top = 0f) {
with(painter) {
// draw(size = painter.intrinsicSize)
draw(
size = Size(40.dp.toPx(), 40.dp.toPx()),
alpha = 1f,
colorFilter = ColorFilter.tint(Color.Red)
)
}
}
}

Related

compose modifiy icon set part of the color

I have an icon to indicate sequence.
My requirement is that when the order is reversed, the lower half of the icon turns blue; when the order is turned, the upper half of the icon turns blue.
I found a related question, but it conflicts with my needs in two points. First, I don't know how to write such code in compose. Second, I prefer to use code to control the color transformation.
Using BlendModes you can manipulate any pixel using another shape, png file, drawing or Path. You can refer these answers for more details
Jetpack Compose Applying PorterDuffMode to Image
How to clip or cut a Composable?
Result
Implementation
#Preview
#Composable
private fun Test() {
Image(
modifier = Modifier
.size(100.dp)
.drawWithContent {
val height = size.height
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawContent()
// Source
drawRect(
Color.Red,
topLeft = Offset(0f, height / 2),
size = Size(size.width, size.height / 2),
blendMode = BlendMode.SrcIn
)
}
},
painter = painterResource(id = R.drawable.arrows),
contentDescription = null
)
}

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.

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.

When to initialize resources to be used inside Compose Canvas?

I am using Jetpack Compose and I want to create a circle with custom shadow/gradient effects. As far as I know there is no way to create that with composable objects inside DrawScope and I have to use NativeCanvas instead. That works fine for my case, but as I remember when we use View and we write something in the onDraw() method, we SHOULD NOT INITIALIZE NEW OBJECTS there. Since the method is called on each 30/60fps when using animation and creating new objects for each call will lead to poor performance.
Where is the proper place to define those object BlurMaskFilter, RadialGradient, Paint so they could be re-initialized only when the size of the composable is changes?
I was wondering if I should define them as lateinit var outside the function and then use SideEffect, to initialize them?
I forgot to mention that I am using InfiniteTransition, and then using the state to change shapes that are drawn inside the NativeCanvas!
Box(
modifier = Modifier
.size(widthDp, widthDp)
.drawBehind {
drawIntoCanvas { canvas ->
canvas.nativeCanvas.apply {
val blurMask = BlurMaskFilter(
15f,
BlurMaskFilter.Blur.NORMAL
)
val radialGradient = android.graphics.RadialGradient(
100f, 100f, 50f,
intArrayOf(android.graphics.Color.WHITE, android.graphics.Color.BLACK),
floatArrayOf(0f, 0.9f), android.graphics.Shader.TileMode.CLAMP
)
val paint = Paint().asFrameworkPaint().apply {
shader = radialGradient
maskFilter = blurMask
color = android.graphics.Color.WHITE
}
drawCircle(100f, 100f, 50f, paint)
}
}
}
) {
}
There are two ways to keep some objects between recompositions in Compose - using remember or representation models. For this particular case remember is a better fit.
If you have a static size given by Modifier.size(widthDp, widthDp), it is easy to calculate everything in advance:
val density = LocalDensity.current
val paint = remember(widthDp) {
// in case you need to use width in your calculations
val widthPx = with(density) {
widthDp.toPx()
}
val blurMask = BlurMaskFilter(
15f,
BlurMaskFilter.Blur.NORMAL
)
val radialGradient = android.graphics.RadialGradient(
100f, 100f, 50f,
intArrayOf(android.graphics.Color.WHITE, android.graphics.Color.BLACK),
floatArrayOf(0f, 0.9f), android.graphics.Shader.TileMode.CLAMP
)
Paint().asFrameworkPaint().apply {
shader = radialGradient
maskFilter = blurMask
color = android.graphics.Color.WHITE
}
}
If you don't have a static size, for example you want to use Modifier.fillMaxSize, you can use Modifier.onSizeChanged to get the real size and update your Paint - that's why I pass size as key in the remember call - it will recalculate the value when the key changes.
val (size, updateSize) = remember { mutableStateOf<IntSize?>(null) }
val paint = remember(size) {
if (size == null) {
Paint()
} else {
Paint().apply {
// your code
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.onSizeChanged(updateSize)
.drawBehind {
// ...
}
)
While the accepted answer is correct, there is a more nice approach.
Just use modifier drawWithCache. In your case:
Modifier
.drawWithCache {
// setup calculations and paints
onDrawBehind {
// draw
}
}
Don't forget to read the documentation of the drawWithCache to be sure your code matches the conditions to reuse the cache (seems like it does).

How can I draw a shadow for a path in canvas in jetpack compose

I am drawing a custom shape for a topbar in jetpack compose. I want to draw a shadow for the path.
val topBarShapePath = Path().apply {
moveTo(dpToPixels(leftPadding), 0f)
lineTo(dpToPixels(leftPadding), dpToPixels(dpValue = 110.dp))
arcTo(
Rect(
dpToPixels(leftPadding),
dpToPixels(dpValue = 110.dp),
dpToPixels(dpValue = 32.dp),
dpToPixels(dpValue = 135.dp)
), -180f, -90f, true)
lineTo(
dpToPixels(dpValue = triangleStartX),
dpToPixels(dpValue = rectHeight))
lineTo(
dpToPixels(dpValue = screenWidth),
dpToPixels(dpValue = triangleEndY)
)
lineTo(dpToPixels(dpValue = screenWidth), 0f)
lineTo(dpToPixels(dpValue = leftPadding), 0f)
}
Column(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.drawBehind {
val finalWidth = 40.dp.toPx()
drawPath(
topBarShapePath,
color = topbarcolor)
drawOutline(
outline = Outline.Generic(
topBarShapePath),
brush = Brush.horizontalGradient(),
style = Stroke(
width = 1.dp.toPx(),
)
)
}
)
This is the code I am using to draw the shape, the "drawOutline" was to try and draw a shadow for the path, but I can't figure out how to blur the line.
Any help appreciated.
Here is a screenshot of the result I am looking for:
It's impossible to draw shadow in Canvas at the moment, but you can do it with Modifier.shadow, specifying the needed custom shape, like this:
class TopBarShape(/*some parameters*/): Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
// your path code
})
}
Modifier.shadow(elevation = 10.dp, shape = TopBarShape(/*your parameters*/))
Sadly this modifier doesn't allow much modifications, it's one of the most starred Compose issues, so hopefully it'll change in future, but as it's not in the latest 1.1-beta I wouldn't expect it at least until 1.2.
If you still think that drawing shadow manually is a needed feature, you can create a feature request.

Categories

Resources