Scrollable Custom View Compose (Canvas) - android

Can't resolve the problem with scrolling a Custom View in Compose.
I have a Canvas nested in a Column with fillMaxSize() modifier.
In Canvas I also use fillMaxSize() modifier (or size() with fixed value, doesn't matter)
I draw a vertical line that can be higher than screen size. And want, if this vertical line is higher, make canvas scrollable, but I can't. Any methods like verticalScroll(), scrollable() has no effects. I'm trying to make height of Canvas higher and use scrollable modifier, but it does no effect also (when I check "size" variable of DrawScope, it's always 1630 on Pixel 4).
Please help.
My code (Simplified and this method will be nested in Column):
#Composable
fun DrawTimeLine(list: List<Unit>){
Canvas(modifier = Modifier
.fillMaxSize()
.scrollable(rememberScrollState(), Orientation.Vertical)
.padding(
top = Dimens.DEFAULT_MARGIN,
bottom = Dimens.BOTTOM_BAR_SIZE,
start = Dimens.DEFAULT_MARGIN
)){
var offsetY = 0f
val offsetX = 0f
val increaseValue = 150f
fun drawTimePoint() {
drawCircle(
color = Color.White,
radius = 15.dp.value,
center = Offset(offsetX, offsetY)
)
}
fun drawTimeLine() {
drawLine(
color = Color.White,
start = Offset(offsetX, offsetY),
end = Offset(offsetX, offsetY + increaseValue),
strokeWidth = 5.dp.value
)
offsetY += increaseValue
}
list.forEach {
drawTimePoint()
drawTimeLine()
}
}
}

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.

Draw lines at an angle in jetpack compose without a canvas

so I wanted to draw a line at an angle from point A to point B in jetpack compose.
Is there a way to do it without a canvas, since a canvas won't really work with what I want to do
If you want to draw behind other composables you can use the drawBehind modifier.
If you want to draw both behind and in front you can use the drawWithContentmodifier.
If you also want to cache the result as much as possible you can use the drawWithCache modifier.
Example of a line behind the content and a line in front of the content using the drawWithCache modifier
#Composable
fun DrawWithCacheExample() {
val width = 400.dp
val height = 200.dp
var offsetX by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.size(width, height)
.border(1f.dp, Color.Black, RectangleShape)
.drawWithCache {
onDrawWithContent {
// draw behind the content
drawLine(Color.Black, Offset.Zero, Offset(width.toPx(), height.toPx()), 1f)
// draw the content
drawContent()
// draw in front of the content
drawLine(Color.Black, Offset(0f, height.toPx()), Offset(width.toPx(), 0f), 1f)
}
}
) {
Box(modifier = Modifier
.size(width / 2, height / 2)
.offset { IntOffset(offsetX.roundToInt(), (height / 4).roundToPx()) }
.background(Color.Yellow)
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
)
)
}
}

JetPack Compose make background color fill only a portion of the screen

I'm working with a linear gradient background color in compose. I want it to start and stop at a designated portion of the screen but currently it's fill-in the whole screen. How can I change it. I need it to start 200px down the screen and have a height of 250px and a width of 350px.
Here's my linear gradient
val gradient = Brush.linearGradient(0.3f to Color.Green,1.0f to Color.Blue,start = Offset(0.0f, 50.0f),end = Offset(0.0f, 100.0f))
Box(modifier = Modifier.fillMaxSize().background(gradient))`
You can use drawBehind and draw a rect.
Box(
modifier = Modifier
.fillMaxSize()
.drawBehind {
drawRect(
brush = gradient,
topLeft = Offset(x = 0f, y = 200.dp.toPx()),
size = Size(250.dp.toPx(), 350.dp.toPx())
)
}
) {
}
you have to update LinearGradient offset as well.
You can apply a top padding to your Box to "start 200px down the screen":
val gradient = Brush.linearGradient(0.3f to Color.Green,1.0f to Color.Blue,start = Offset(0.0f, 50.0f),end = Offset(0.0f, 100.0f))
val density = LocalDensity.current
val offsetYDp = density.run { 250.toDp() }
val widthDp = density.run { 350.toDp() }
val heightDp = density.run { 250.toDp() }
Box(
modifier = Modifier
.padding(top = offsetYDp)
.height(heightDp)
.width(widthDp)
.background(gradient)
)

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.

Jetpack Compose - Draw Line with Pattern

I'm looking to draw a line on a canvas with a pattern instead of a color. Here's the code I have right now:
drawLine(
color = progressColor,
start = Offset(if (whiteGap) progressCapWidth else startOffsetBg, yOffset),
end = Offset(endOffsetProgress, yOffset),
strokeWidth = progressHeight.toPx(),
cap = if (roundedCorners) StrokeCap.Round else StrokeCap.Butt,
)
It's part of a custom linear progress bar. Per the design I was given, they want the progress to have this pattern:
This is an example of full progress with this diagonally patterned progress. Is it possible to use a drawable and repeat it instead of a color? Is there a way to just apply/create diagonal white gaps directly in when drawing the line?
We're implementing this whole feature using Jetpack Compose, so I can't do something with traditional XML involved.
Here's how you can draw it with Canvas:
Canvas(
Modifier
.padding(top = 100.dp)
.border(1.dp,Color.Black)
.padding(10.dp)
.height(30.dp)
.fillMaxWidth()
.clip(CircleShape)
) {
val step = 10.dp
val angleDegrees = 45f
val stepPx = step.toPx()
val stepsCount = (size.width / stepPx).roundToInt()
val actualStep = size.width / stepsCount
val dotSize = Size(width = actualStep / 2, height = size.height * 2)
for (i in -1..stepsCount) {
val rect = Rect(
offset = Offset(x = i * actualStep, y = (size.height - dotSize.height) / 2),
size = dotSize,
)
rotate(angleDegrees, pivot = rect.center) {
drawRect(
Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
)
}
}
}
Result:

Categories

Resources