React Native Library for Responsive and Universal Apps
Install
In your project folder, yarn add react-native-responsive-grid
*For best results, use React Native 0.50 or later
Percentage-based Dynamic, Responsive Layout
This grid eschews complex Flex sizing in favor of percentage-based sizing while at the same time relying pn the 1-dimensional constraint-based layout of Flexbox for vertical and horizontal alignment. The grid aims to deliver the predictability of the percentage-based model with the more sophisticated alignment capability of the Flexbox model.
The grid construct is extended beyond its common form as a rectilinear 2D Grid and generalized as Row and Column components that can be nested in an alternating pattern to build a tree of Views of any shape and depth. That is to say, a Row View (Row for short) may contain one or more Column Views (Column for short), each of which may contain one or more Rows, which in turn may contain one or more Columns, and so on. Both Rows and Columns can be styled using predictable, percentage-based dimensions and, in case of Columns, percentage-based horizontal offsets. Rows can be aligned inside Columns, vertically (along main axis,) and aligned and stretched horizontally (along cross axis.) Columns can be aligned inside Rows, horizontally (along main axis), and and aligned and stretched vertically (along cross axis.) Additionally, the multiple lines created by a wrapped Row may be aligned and stretched vertically relative to the parent Column. And with these basic features, we can build the entire UI component tree (or an individual component's subtree) as a consistent, repeatable and recursive pattern, one that has predictable and dynamic --not only static-- responsiveness and the ability to trigger specific adaptive behavior.
When To Use Flexbox Sizing:
In some cases when having an absolutely sized view followed (vertically or horizontally) by a view that must take up the remaining space, we'll need to use a wrapping grid element -- Column (to wrap vertical layout) or Row (to wrap horizontal layout) -- with style={{flex: 1}} and same on the Flex sized element that it wraps along with the absolutely sized element. However, such mixing of absolute and Flex sizing is not recommended as it won't lead to a fully responsive UI layout.
The only other reason to use Flexbox grow/shrink sizing with this Grid is for grow-and-shrink-in-place UI (aka "squishy" UI) where elements shrink and grow in elastic fashion and relative to each other instead of undergoing dynamic re-layout change and/or staying in proportion to screen width.
Examples
You may use this grid to build responsive 2D layouts that maintain their relative proportions, change their basic structure in a predictable way and dynamically decide what content to display, based on screen size, aspect ratio, and orientation.
Example 1: Universal, Responsive Pinterest Layout
Example 2: Reponsive Tiles for Universal Apps
Example 3: Selecting an image with the right aspect ratio
Example 4: Responsive Break Points (Row Wrapping)
Example 5: FlatList + Row & Column Wrapping
The demos in the videos above show some of the possibilities, but this grid is capable of more complex responsive and adaptive behavior.
Example 1: Universal, Responsive Pinterest Layout
This examples showcases 2-dimensional Constraint-Based Layout using a custom layout in a few lines of code. Flexbox fails us here in that it does not support a 2-dimensional constraint layout. This is precisely why React Native needs native support for display:'grid' Until then you may use this grid with your own constraint-based layout. This example shows a simplified Pinterest-like layout. You may extend it to build a masonry effect using a box packing algorithm and Flexbox's 1-dimensional constraint-based elastic layout. One thing this grid is not designed to do is to implement transitions but it can be forked and extended to do that (would happy take a PR.)
Example 2: Reponsive Tiles for Universal Apps
This examples showcases the grid's 1-dimensional Constraint-Based Layout using Flexbox wrapping behavior.
The problem it solves is how to make a tiled screen layout that looks consistent across all screen sizes and aspect ratios, It involves the following:
-
How to size tiles such that they change size relative to the size of the screen as well as retain their shape (width/height aspect ratio)
-
How do we hide/show tiles on demand and fill the void left by hidden tiles.
The goal is how to do the above in an elegant and declarative way that allows the average user to work without all the tedious implementation details of doing it in row Flexbox and JS.
This example also showes how to use alignLines='stretch' for wrapped row content to have the wrapped lines fill the whole screen. It's the right way to partition a Row vertically in 1/n tall lines where n is the number of wrapping-stacked fullWidth columns.
Example 3: Selecting an image with the right aspect ratio
In this demo, the grid picks the image with the closest aspect ratio to the device aspect ratio, dynamically, taking into account the current device orientation. The images themselves must be sized and cropped by the designer so that they match the common device aspect ratios (see below) while also showing the part of the image that the designer intends to show for each aspect ratio. Since there could be many aspect ratios that correspond to different devices we should have multiple such images (and, optionally, their rotated versions.)
The following table maps some common device aspect ratios to the ratio of width/height of devices known to this developer, for both landscape and portrait device orientations. The physical device aspect ratio does not change with device rotation (i.e. a device with 16:9 aspect ratio does not become one with a 9:16 aspect ratio when it's rotated, although it does mathematically), but since the width and height get flipped when changing orientation from portrait to lanscape and vice versa, we need to have two images per each physical device aspect ratio, one for portrait mode and the other for landscape. However, if our app only supports portrait or landscape mode then we only need to have the one corresponding to that orientation.
Aspect Ratio | Width | Height | Width/Height Ratio (landscape) | Devices |
---|---|---|---|---|
'16:9' | 568 | 320 | 1.77 | iPhone 5 |
'16:9' | 667 | 375 | 1.77 | iPhone 6 & 7 |
'16:9' | 736 | 414 | 1.77 | iPhone 6 Plus & 7 Plus |
'16:10' | ? | ? | 1.6 | ? |
'3:2' | 480 | 320 | 1.5 | iPhone 4 |
'4:3' | 1024 | 768 | 1.33 | iPad Mini, iPad Air and small iPad Pro |
'4:3' | 1366 | 1024 | 1.33 | Large iPad Pro |
'1:1' | 1 | ? | ? | ? |
Aspect Ratio | Width | Height | Width/Height Ratio (portrait) | Devices |
---|---|---|---|---|
'16:9' | 320 | 568 | 0.56 | iPhone 5 |
'16:9' | 375 | 667 | 0.56 | iPhone 6 & 7 |
'16:9' | 414 | 736 | 0.56 | iPhone 6 Plus & 7 Plus |
'16:10' | ? | ? | 0.625 | ? |
'3:2' | 320 | 480 | 0.66 | iPhone 4 |
'4:3' | 768 | 1024 | 0.75 | iPad Mini, iPad Air and small iPad Pro |
'4:3' | 1024 | 1366 | 0.75 | Large iPad Pro |
'1:1' | 1 | ? | ? | ? |
<Grid>{(state, setState) => (
<Row fullHeight aspectRatio={{ratio: '3:2', orientation: "portrait"}}>
<Image source={require('./assets/homepage hero-3-2-portrait.jpg')} style={styles.homeImage}></Image>
</Row>
<Row fullHeight aspectRatio={{ratio: '3:2', orientation: "landscape"}}>
<Image source={require('./assets/homepage hero-3-2-landscape.jpg')} style={styles.homeImage}></Image>
</Row>
<Row fullHeight aspectRatio={{ratio: '16:9', orientation: "portrait"}}>
<Image source={require('./assets/homepage hero-16-9-portrait.jpg')} style={styles.homeImage}></Image>
</Row>
<Row fullHeight aspectRatio={{ratio: '16:9', orientation: "landscape"}}>
<Image source={require('./assets/homepage hero-16-9-landscape.jpg')} style={styles.homeImage}></Image>
</Row>
)}
</Grid>
Example 4: Responsive Break Points (Row Wrapping)
A more basic example of he grid's 1-Dimensional Constraint-Based Layout using Flexbox.
In the second demo, the grid folds columns in rows based on the screen-device-depebdent xxSize
prop provided on the column (which can be percentage based, e.g. smSize, or point based, e.g. smSizePoints. This means that different break points can be supplied for the different screen sizes in both absolute and relative terms. This example demonstrates how to get Row content (e.g. child Columns) to wrap at certain break points (which can be supplied per screen width)
The following are the preset screen widths (in points) at which breaks maybe specified (where row wraps columns within it into new horozintal lines):
-
SMALL_Width: 375 (0-375)
-
MEDIUM_Width: 767 (376-767)
-
LARGE_Width: 1023 (768-1023)
-
XLARGE_Width: 1024+
-
SMALL_Height: 667 (0-667)
-
MEDIUM_Height: 1023 (668-1023)
-
LARGE_Height: 1365 (1024-1365)
-
XLARGE_Height: 1366+
The preset values may be overridden with setBreakPoints
which merges the parameter object with the defaults. Each cutoff specifies the upper end for that range. XLARGE_Width
is inferred from anything above LARGE_Width
. BreakPoints should be set early in the app such as in index.js
. An example overriding the SMALL_Width
, MEDIUM_Width
, and LARGE_Width
break points:
import { setBreakPoints } from 'react-native-responsive-grid';
setBreakPoints({
SMALL_Width: 414,
MEDIUM_Width: 600,
LARGE_Width: 1024
})
<Row style={{paddingTop: '6%', paddingBottom: '6%', backgroundColor: 'white', borderBottomColor: 'lightgray', borderBottomWidth: 1}}>
<Col size={80} offset={6}>
<Row>
<Col size={50} smSize={100}>
<Text style={{fontSize: 15, color: '#BD1206', fontWeight:'bold'}}>March 9, 2017</Text>
<Row>
<Col size={5}>
<FontAwesome name='cutlery' size={17} color='gray'/>
</Col>
<Col size={60} offset={2.5}>
<Text style={{fontSize: 12, color: 'gray', lineHeight: 20}}>TAKEOUT ORDER</Text>
</Col>
</Row>
</Col>
<Col size={50} smSize={100}>
<Text style={{fontSize: 16, color: '#0a0a0a'}}>Double Cheese Burger</Text>
</Col>
</Row>
</Col>
<Col size={14} offset={-6} hAlign='right'>
<MaterialIcons name="keyboard-arrow-right" size={28} color="#BD1206" style={{left: 5}} />
</Col>
</Row>
Example 5: FlatList + Row & Column Wrapping
FlatList is a virtualized replacement for React Native's old ListView component. Using FlatList as a container is supported by this grid. This example also demonstrate wrapping Column content based on screen size. See ('size' prop under the Props section.) It also demonstrates who to wrap Row content (e.g. child columns) based on screen size (also see Example 4)
import React, { Component } from 'react';
import {
FlatList,
Text,
ScrollView
} from 'react-native';
import { Row, Column as Col, Grid} from 'react-native-responsive-grid'
import { MaterialIcons } from '@expo/vector-icons';
import faker from 'faker';
let j = 0
const randomUsers = (count = 10) => {
const arr = [];
for (let i = 0; i < count; i++) {
arr.push({
key: faker.random.uuid(),
date: faker.date.weekday(),
name: faker.name.firstName(),
job: faker.name.jobTitle(),
index: j++
})
}
return arr
}
export default class Home extends Component {
state = {
refreshing: false,
data: randomUsers(10),
};
onEndReached = () => {
const data = [
...this.state.data,
...randomUsers(10),
]
this.setState(state => ({
data
}));
};
onRefresh = () => {
this.setState({
data: randomUsers(10),
});
}
render() {
return (
<FlatList
data={this.state.data}
initialNumToRender={10}
onEndReachedThreshold={1}
onEndReached={this.onEndReached}
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
renderItem={
({ item }) => {
return (
<Row key={item.key} style={{paddingTop: '6%', paddingBottom: '6%', backgroundColor: 'white', borderBottomColor: 'lightgray', borderBottomWidth: 1}}>
<Col size={80} offset={6} >
<Row>
<Col size={60} smSize={100}>
<Text style={{fontSize: 15, color: '#BD1206', fontWeight:'bold'}}>{String(item.date)}</Text>
<Row>
<Col size={10}>
<MaterialIcons name='person' size={17} color='gray'/>
</Col>
<Col smSize={60} size={87.5} offset={2.5}>
<Text style={{fontSize: 12, color: 'gray', lineHeight: 20}}>{item.job}</Text>
</Col>
</Row>
</Col>
<Col size={40} smSize={100}>
<Text style={{fontSize: 16, color: '#0a0a0a'}}>{item.name}</Text>
</Col>
</Row>
</Col>
<Col size={8} offset={-6} hAlign='right'>
<Text>{item.index}</Text>
</Col>
</Row>
)
}}
/>
)
}
}
Components
-
Row: Flexbox View with flexDirection set to 'row' with convenient props and dynamic behavior.
-
Col: Flexbox View with flexDirection set to 'column' with convenient props and dynamic behavior.
-
Grid: an optional, stateful, component with style={{flex: 1}}. The Grid uses the children-as-funnction pattern and passes its state to its children, and allows state to be declared in its props, which will have the latest screen and grid info after orientation changes. It also passes it's render-triggering async setState method to its children.
Important:
Grid component is required if you need to re-run the render() function in response to orientation change (many examples here)
Grid component is also required if you use aspectRatio prop on Rows or Columns since the selection of content of the closest aspect ratio requires re-running the render function after orientation change.
Below is an example:
export const Home = () => (
<Grid state={
{
someState: 'yyz',
anotherState: 'abc'
}}>
{({state, setState}) => (
{/* possibly other JSX here */}
<Col fullWidth style={{backgroundColor: 'lightgray'}}>
<ScrollView removeClippedSubviews={true} >
<Row fullHeight>
{layout(state)}
</Row>
</ScrollView>
</Col>
)}
</Grid>)
Utils
import { Row, Column as Col, ScreenInfo, Grid} from './grid'
ScreenInfo(sizeOnly: Boolean)
This will return the following data:
{
mediaSize: mediaSizeWidth,
mediaSizeWidth,
mediaSizeHeight,
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
aspectRatio: {currentNearestRatio, currentOrientation}
}
- mediaSize is one of
sm
,md
,lg
,xl
screen width categories and is aliased to mediaSizeWidth - mediaSizeHeight is the same but for screen height. It's used for hiding/showing Rows wit
hidden
prop based on screen height category and for Rowsize
props.
if sizeOnly
is true it will drop aspectRatio and its 'nearest match' calculation (shaves a few ms)
Methods
Row and Column both have .hide()
and .show()
instance methods. The instance reference you get from a ref callback will have these methods. See Example #1 for usage.
Instance Variables
These are provided mainly for unit tests, except for componentInstance.hidden
and componentInstance.shown
which can be used to tell the state of the component.