Unwanted lines in 2D grid because of potential gaps between items

Viewed 132

I'm trying to create a very simple 2D grid using a 2D array in react-native.

The size of the grid is controlled by a parameter called size. For some of the values of size (e.g 31 but not for 40) there appears to be gaps in the grid.

Following is the 30 line code to generate the 9x9 grid:

import { View } from 'react-native'


const size = 31


const Box = () => {
  let grid = Array.from(Array(9), _ => Array(9).fill(0)) // 9x9 array
  
  return (
    <View style={{ flex: 1, justifyContent: 'center',  alignItems: 'center' }}>
      {grid.map((row, i) => (
        <View key={i} style={{ height: size, flexDirection: 'row', backgroundColor: 'red' }}>
          {row.map((cell, j) =>
            <View key={j} style={{ width: size, height: size, backgroundColor: 'white' }}></View>
          )}
        </View>
      ))}
    </View>
  )
}


export default function App() {
  return (
      <View style={{ flex: 1, position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, backgroundColor: 'gray' }}>
        <Box />
      </View>
  )
}

There is also a snack available to edit and see the changes online here: https://snack.expo.dev/@diby3ndu/test

I'm attaching a screenshot of the grid running on Expo from my android device.

screenshot

As you can see there are unwanted lines in the picture. The lines are not there in the online version though.

Can anyone help me figuring out what may be causing these unwanted lines?

1 Answers

If you log the dimensions of the children components onLayout events, you can see the width calculations aren't rounding out to a whole number when size is equal to an odd number.

import { View, useWindowDimensions } from 'react-native'


const size = 31;

function logLayout(event, label) {
  const { width } = event.nativeEvent.layout;
  console.log(`${label}: ${width}`);
}


const Box = () => {
  let grid = Array.from(Array(9), _ => Array(9).fill(0)) // 9x9 array

  return (
    <View onLayout={(e) => logLayout(e, 'grid width')} style={{ flex: 1, justifyContent: 'center',  alignItems: 'center' }}>
      {grid.map((row, i) => (
        <View onLayout={(e) => logLayout(e, 'row width')} key={i} style={{ height: size, flexDirection: 'row', backgroundColor: 'red' }}>
          {row.map((cell, j) =>
            <View key={j} style={{ width: size, height: size, backgroundColor: 'white' }}></View>
          )}
        </View>
      ))}
    </View>
  )
}


export default function App() {
  const window = useWindowDimensions();

  console.log('window width:', window.width);

  return (
    <View onLayout={(e) => logLayout(e, 'app width')} style={{ flex: 1, position: "absolute", left: 0, right: 0, top: 0, bottom: 0,backgroundColor: '#ccc' }}>
      <Box />
    </View>
  )
}


And the console output:

window width: 411.42857142857144
row width: 278.85714721679697
grid width: 411.4285583496094
app width: 411.4285583496094

Trying other values like 33, 35... etc, this is the case any time size is an odd number, though the gap isn't always visible.

There's a lot happening between react-native and Android's UI controller, especially when you consider pixel ratios w/ densityPixels and the provided dimensions are being mapped to a corresponding value depending on the device. Guessing it has something to do with this rounding and that pixels are a base unit 2.

From experience, working with fixed widths for your row items isn't really a good idea. Responsively, you'll be better off passing a rowItemCount prop, using a fixed padding on your screen and calculating grid item sizes to match the remaining screen area via const gridItemSize = (useWindowDimensions().width / rowItemCount) - (Screen.PADDING * 2), or something along those lines. Your component will end up being much more flexible down the road.

Related