https://kotlinlang.org logo
Title
a

Aaron Yoder

04/07/2021, 2:52 AM
Canvas/Scrollbar question: I was looking at the Scrollbar tutorial in the compose-jb repository and came up with
val stateVertical = rememberScrollState(0)
        val stateHorizontal = rememberScrollState(0)
        Box(modifier = Modifier.verticalScroll(stateVertical).padding(end = 12.dp, bottom = 12.dp).horizontalScroll(stateHorizontal)) {
            Canvas(modifier = Modifier.size(width.dp, height.dp) {
                drawIntoCanvas { canvas ->
					// draw things
                }
            }
        }
        VerticalScrollbar(modifier = Modifier.fillMaxHeight(), adapter = rememberScrollbarAdapter(stateVertical))
        HorizontalScrollbar(modifier = Modifier.fillMaxWidth().padding(end = 12.dp), adapter = rememberScrollbarAdapter(stateHorizontal))
I'm not sure if this is the proper way to implement a scrollable canvas, because while it does seem to scroll across the canvas fine, there don't appear to be any visual scrollbars, just empty space on the bottom and right sides of the global canvas, rather than relative to the window. The other issue I was having was that I was putting a
pointerMoveFilter
modifier on the canvas modifier to get x, y coordinates in the canvas, which apparently isn't the proper way to do it because when adding scroll bars the coordinates of the top-left position are always 0, 0 regardless of where that actually might be on the canvas. I feel like I'm definitely misunderstanding something about scrollbars here. Are they only meant for scrolling through a list, and not across an image? How would I correctly implement it for a canvas, and is there a proper method of getting the relative-to-Canvas x, y coordinates?
i

Igor Demin

04/07/2021, 10:17 AM
Try this:
import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.unit.dp

fun main() = Window {
    val stateVertical = rememberScrollState(0)
    val stateHorizontal = rememberScrollState(0)

    var pointerCoords: Offset? by remember { mutableStateOf(null) }

    Box(Modifier.fillMaxSize()) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .verticalScroll(stateVertical)
                .padding(end = 12.dp, bottom = 12.dp)
                .horizontalScroll(stateHorizontal)
        ) {
            Canvas(
                modifier = Modifier.width(3000.dp).height(4000.dp)
                    .pointerMoveFilter(
                        onMove = { pointerCoords = it; false },
                        onExit = { pointerCoords = null; false }
                    )
            ) {
                drawIntoCanvas {
                    drawCircle(Color.Red, radius = 100f, center = Offset(100f, 100f))
                    if (pointerCoords != null) {
                        val scrollOffset = Offset(stateHorizontal.value.toFloat(), stateVertical.value.toFloat())
                        drawCircle(Color.Blue, radius = 20f, center = pointerCoords!! + scrollOffset)
                    }
                }
            }
        }
        VerticalScrollbar(
            modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd),
            adapter = rememberScrollbarAdapter(stateVertical)
        )
        HorizontalScrollbar(
            modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter).padding(end = 12.dp),
            adapter = rememberScrollbarAdapter(stateHorizontal)
        )
    }
}
Canvas should have fixed size to be able to scroll it.
pointerMoveFilter
 modifier on the canvas modifier to get x, y coordinates in the canvas
Probably we should return not a window-relative coordinates, but a scroll-relative coordinates. Until then you can manually add
scrollOffset
(see example).
👍 1
a

Aaron Yoder

04/08/2021, 12:32 AM
The example given was very helpful and works as I expect. Thanks so much for the information!