JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.15k stars 1.17k forks source link

When drawing a grid on the desktop platform, there are always gaps between the grids #3917

Closed panpf closed 11 months ago

panpf commented 11 months ago

Describe the bug I tried splitting a component into a fixed number of grids and drawing a rectangle for each grid. On the desktop platform there is always a gap between the cells. On the Android platform it fits perfectly.

Affected platforms

Versions

To Reproduce

@Composable
fun DrawGridTest() {
    Column(
        Modifier
            .fillMaxSize()
            .padding(20.dp)
    ) {
        Text(text = "10x10 Grid")
        Spacer(modifier = Modifier.size(10.dp))

        Box(Modifier
            .fillMaxSize()
            .background(Color.Green)
            .drawWithContent {
                val number = 10
                val tileSize = Size(
                    width = size.width / number,
                    height = size.height / number,
                )
                var x = 0
                var y = 0
                do {
                    val dstRect = Rect(
                        left = x * tileSize.width,
                        top = y * tileSize.height,
                        right = (x + 1) * tileSize.width,
                        bottom = (y + 1) * tileSize.height,
                    )

                    drawRect(color = Color.Red, topLeft = dstRect.topLeft, size = dstRect.size)

                    if (x < number - 1) {
                        x++
                    } else if (x == number - 1) {
                        x = 0
                        y++
                    }
                } while (y < number)
            }
        )
    }
}

Screenshots Running effect on desktop platform:

Snipaste_2023-11-09_17-31-24

Running effect on Android: Screenshot_20231109_173318

Additional context

Please help me to solve this problem!

igordmn commented 11 months ago

Android (Pixel 6 emulator) also has these lines:

image
igordmn commented 11 months ago

When you draw a rect with floating values, it becomes antialised (with transparent color) on its borders.

When you connect 2 such rects, the blending result also isn't completely opaque.

To avoid this, you need to round positions and sizes. There is no solution I am aware of that can be implemented on the framework level.

panpf commented 11 months ago

My real need is to draw an image and I have a simplified example of drawing image tiles.

  1. It needs to add this dependency:

    implementation("io.github.panpf.zoomimage:zoomimage-compose:1.0.0-beta05")
  2. And a card image resource, sample_huge_card.jpg: sample_huge_card

  3. Please run the following code on the desktop platform:

    
    @Composable
    fun DrawTilesExample() {
    val cardImageSize = IntSize(7557, 5669)
    val cardThumbnailImageSize = cardImageSize / 8
    val density = LocalDensity.current
    val cardThumbnailDpSize = remember(density, cardThumbnailImageSize) {
        with(density) {
            DpSize(cardThumbnailImageSize.width.toDp(), cardThumbnailImageSize.height.toDp())
        }
    }
    
    Box(Modifier.fillMaxSize()) {
        var tiles by remember { mutableStateOf(emptyList<Tile>()) }
        val logger = rememberZoomImageLogger()
        val zoomState = rememberZoomableState(logger)
        Box(modifier = Modifier
            .size(cardThumbnailDpSize)
            .zoom(logger, zoomState)    // double tap zoom、drag pan, from zoomimage library
            .drawWithContent {
                drawContent()
                tiles.forEach { tile ->
                    val imageBitmap = tile.imageBitmap
                    val srcRect = tile.srcRect
                    val drawRect = calculateTileDrawRect(cardImageSize, cardThumbnailImageSize, srcRect)
                    drawImage(
                        imageBitmap,
                        srcOffset = IntOffset.Zero,
                        srcSize = IntSize(imageBitmap.width, imageBitmap.height),
                        dstOffset = drawRect.topLeft,
                        dstSize = drawRect.size,
                    )
                }
            }
        )
    
        if (tiles.isEmpty()) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }
    
        LaunchedEffect(Unit) {
            tiles = decodeTiles("sample_huge_card.jpg", cardImageSize)
        }
    }
    }

private fun calculateTileSrcRects(imageSize: IntSize, columnCount: Int, rowCount: Int): List { val tileSize = IntSize( width = ceil(imageSize.width / columnCount.toFloat()).toInt(), height = ceil(imageSize.height / rowCount.toFloat()).toInt(), ) val tileRects = mutableListOf() for (y in 0 until rowCount) { for (x in 0 until columnCount) { val left = x tileSize.width val top = y tileSize.height val srcRect = IntRect( left = left, top = top, right = (left + tileSize.width).coerceAtMost(imageSize.width - 1), bottom = (top + tileSize.height).coerceAtMost(imageSize.height - 1) ) tileRects.add(srcRect) } } return tileRects.toList() }

private fun calculateTileDrawRect(imageSize: IntSize, thumbnailSize: IntSize, srcRect: IntRect): IntRect { val widthScale: Float = imageSize.width / (thumbnailSize.width.toFloat()) val heightScale: Float = imageSize.height / (thumbnailSize.height.toFloat()) // // all floor. Render the result, with gaps // val drawRect = IntRect( // left = floor(srcRect.left / widthScale).toInt(), // top = floor(srcRect.top / heightScale).toInt(), // right = floor(srcRect.right / widthScale).toInt(), // bottom = floor(srcRect.bottom / heightScale).toInt() // ) // // all ceil. Render the result, with gaps // val drawRect = IntRect( // left = ceil(srcRect.left / widthScale).toInt(), // top = ceil(srcRect.top / heightScale).toInt(), // right = ceil(srcRect.right / widthScale).toInt(), // bottom = ceil(srcRect.bottom / heightScale).toInt() // ) // // all left top floor, right bottom ceil. Render the result, the tiles are misplaced // val drawRect = IntRect( // left = floor(srcRect.left / widthScale).toInt(), // top = floor(srcRect.top / heightScale).toInt(), // right = ceil(srcRect.right / widthScale).toInt(), // bottom = ceil(srcRect.bottom / heightScale).toInt() // ) // all round. Render the result, with gaps val drawRect = IntRect( left = round(srcRect.left / widthScale).toInt(), top = round(srcRect.top / heightScale).toInt(), right = round(srcRect.right / widthScale).toInt(), bottom = round(srcRect.bottom / heightScale).toInt() ) return drawRect }

@OptIn(ExperimentalComposeUiApi::class) suspend fun decodeTiles(resourcePath: String, imageSize: IntSize): List { return withContext(Dispatchers.IO) { val inputStream = ResourceLoader.Default.load(resourcePath).buffered() ImageIO.createImageInputStream(inputStream).use { imageStream -> val imageReader = ImageIO.getImageReaders(imageStream).next().apply { input = imageStream } val tileSrcRects = calculateTileSrcRects(imageSize, columnCount = 20, rowCount = 20) tileSrcRects.map { tileSrcRect -> val readParam = imageReader.defaultReadParam.apply { sourceRegion = Rectangle( / x = / tileSrcRect.left, / y = / tileSrcRect.top, / width = / tileSrcRect.width, / height = / tileSrcRect.height ) } val imageBitmap = imageReader.read(0, readParam).toComposeImageBitmap() Tile(tileSrcRect, imageBitmap) } } } }

data class Tile(val srcRect: IntRect, val imageBitmap: ImageBitmap)



4. Double tap on the image to zoom in it, then drag the image and you will see a white gap, the gap is the background of the window
![Snipaste_2023-11-14_14-00-31](https://github.com/JetBrains/compose-multiplatform/assets/3250512/65cdbd5a-478a-40dc-ae2b-5b8141bf6c08)

I tried using round in the calculateTileDrawRect function to adjust the drawRect as you said, but it still didn't solve the problem.

Please help me solve this problem, whether it is adjusting drawRect or srcRect
igordmn commented 11 months ago

In the second snippet we scale the canvas, so it also messes with coordinates of its content. Disabling antialiasing helps in this case:

                    drawIntoCanvas {
                        it.drawImageRect(
                            imageBitmap,
                            srcOffset = IntOffset.Zero,
                            srcSize = IntSize(imageBitmap.width, imageBitmap.height),
                            dstOffset = drawRect.topLeft,
                            dstSize = drawRect.size,
                            paint = Paint().apply {
                                isAntiAlias = false
                            }
                        )
                    }
panpf commented 11 months ago

In the second snippet we scale the canvas, so it also messes with coordinates of its content. Disabling antialiasing helps in this case:

                    drawIntoCanvas {
                        it.drawImageRect(
                            imageBitmap,
                            srcOffset = IntOffset.Zero,
                            srcSize = IntSize(imageBitmap.width, imageBitmap.height),
                            dstOffset = drawRect.topLeft,
                            dstSize = drawRect.size,
                            paint = Paint().apply {
                                isAntiAlias = false
                            }
                        )
                    }

This is useful, thank you very much!

okushnikov commented 2 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.