Chapter 3. Style and Design

Most of the work involved in making a native app feel polished comes from having well-designed components that can communicate a strong visual identity within the user-experience conventions of the platform. For example, iOS applications tend to rely on bottom tab navigation. The lefthand drawer or the Snackbar notifications are typically seen in Android.

Building a cross-platform application will probably mean making certain design choices that balance user experience, platform conventions, and technical complexity. These tips should help you make those choices more easily.

3.1 Composing Stylesheets

Maintaining a growing stylesheet is a challenge in any web application. Native applications are no different. Fortunately, React components allow us to create a unit of code that combines everything required for a user interface element to render correctly.

In the last few years, the debate around how to organize web styles has led to all sorts of semantics for describing what something is supposed to look like. Whether you are familiar with Object-Oriented CSS, SMACCS, Tachyons, or BEM, any of these design choices rely on the language’s ability to compose stylesheet declarations.

React Native does not support CSS. CSS is a language for describing how something looks, with syntax that reduces the effort in defining common styles. This section illustrates how we can achieve many of the features of CSS using simple JavaScript declarations.

Problem

How do we reuse as many styles as possible and keep the application’s look and feel consistent?

Solution

All applications will have a common set of applicable fonts, colors, and component styles. These might include how rounded a button corner should be, or what the appropriate padding should be between typographic elements. I like to keep these bits of style information in a styles.js file in my project root with key sections that will broadly define the aesthetic of my application:

  1. Color Palette

  2. Typography Choices

  3. Global Styles

Inheriting styles

Here’s an example of what a styles.js file might look like:

import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');

// COLOR
export const colors = {
  PRIMARY: '#005D64',
  SECONDARY: '#CA3F27',
}

// TYPOGRAPHY
const scalingFactors = {
  small: 40,
  normal: 30,
  big: 20,
}


export const fontSizes = {
  H1: {
    fontSize: width / scalingFactors.big,
    lineHeight: (width / scalingFactors.big) * 1.3,
  },

  P: {
    fontSize: width / scalingFactors.normal,
    lineHeight: (width / scalingFactors.normal) * 1.3,
  },

  SMALL: {
    fontSize: width / scalingFactors.small,
  },
}

// GLOBAL STYLES
export const globalStyles = {
  textHeader: {...fontSizes.H1,
    color: colors.PRIMARY,
    paddingTop: 20,
    fontWeight: 'bold',
  },
}

The textHeader component illustrates a classic form of composition. It relies on the fontSizes.H1 key as a basis for the textHeader. If we need to change the overall size of the primary header in our application, we need only change the scaling factors to see these adjustments happen everywhere.

By importing the Dimensions library from React Native, we can perform some simple math operations in our definition of these fontSizes, ensuring that the typography feels the same across platforms and device sizes.1

The biggest benefit to this approach is that all the styles are defined using the same programming language we use to build the rest of the application.

Overriding inline styles

With global styles defined, they can be referenced in your own flavor of the base components. For example, here is a definition for <TextHeading /> and <SecondaryTextHeading /> components:

import React from 'react';
import {
  Text,
} from 'react-native';
import { globalStyles, colors } from '../styles';

export function TextHeading  (props) {
  return <Text style={globalStyles.textHeader} >{props.children}</Text>
}

export function SecondaryTextHeading(props) {
  return <Text
    style={[globalStyles.textHeader, { color: colors.SECONDARY } ]} >
      {props.children}
  </Text>
}

In the preceding example, rather than implement a class that extends React.Component, I use a shorthand for a pure function—a function with no side effects—which supports two JSX components. This syntax provides a hint to the developer that this function will not have any local state.

The <SecondaryTextHeading /> component overrides the color declaration with a style array attribute. Each item in the array is merged together, with the last item in the array overriding any previous declarations. The style attribute in this case will be:

{
  fontSize: width / scalingFactors.big,
  lineHeight: (width / scalingFactors.big) * 1.3,
  color: colors.SECONDARY,
  paddingTop: 20,
  fontWeight: 'bold',
}

See Also

There are some great component libraries in the React Native ecosystem. react-native-elements provides an excellent set of cross-platform components with some of the most common components. NativeBase accomplishes the same goals with a more featureful component library. These libraries are a great way of ensuring that your app will be functional and consistent.

react-native-material-kit aims to provide a complete component library based on Google’s Material Design.

If you find yourself customizing every component, you might be better off developing your own component library.

3.2 Building Flexible Layouts with Flexbox

Your app will run on a number of different form factors and device sizes. This means that setting up a pixel-based design will result in a lot of testing and per-device rework. Avoid most of those headaches by using a flexbox layout.

Problem

How do you build a flexible layout system that will work with different device sizes? Using just a handful of style declarations we can build complex views like the one in Figure 3-1.

A common layout pattern for user interfaces
Figure 3-1. A 3-column flexbox layout

Solution

The layout in Figure 3-1 was rendered using this simple component. While I would recommend using the StyleSheet class for performance and reusability, writing the styles inline helps illustrate how each parent <View /> configures the flow direction of the child <View />:

import React, { Component } from 'react';
import {
  Text,
  View
} from 'react-native';

export default class ThreeColumns extends Component {

  sidebar() {
    const avatarStyle = {
      width: 40,
      height: 40,
      borderRadius: 40,
      justifyContent: 'center',
      backgroundColor: '#A0'
    }
    return <View style={{ flex: 0.2,  backgroundColor: '#333' }}>
      <View style={{ flex: 0.2,   backgroundColor: '#666',
      flexDirection: 'row' }}>
        <View style={{ width: 50, padding: 5, backgroundColor: '#000' }}>
          <View style={avatarStyle} />
        </View>
      </View>
      <View style={{ flex: 0.8 }} />
    </View>
  }

  body() {
    return <View style={{ flex: 0.5, backgroundColor: '#FFF' }}>
      <Text style={{padding: 40, fontSize: 22}}>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        Fusce vestibulum tempor nisl.
      </Text>
    </View>
  }

  rightBar() {
    return <View style={{ flex: 0.1, backgroundColor: '#FFA' }}></View>
  }

  render() {
    return (
      <View style={{ flexDirection: 'row', flex: 1, backgroundColor: '#FFF' }}>
        {this.sidebar()}
        {this.body()}
        {this.rightBar()}
      </View>
    );
  }
}

Flex and FlexDirection

The main render() function wraps a sidebar(), body(), and rightBar() component with a flexDirection: "row" style attribute. The flexDirection will dictate whether block elements should stack vertically or horizontally. By default, a <View /> will stack vertically. The default flexDirection in React Native is column and not row (like in CSS).

In this case, we want our outer container to flow like a row: with the sidebar, body, and rightBar appearing next to each other. The flex value indicates the relative size of the container. There are two commonly used conventions for flex values: 1 or 10. In this case, the outer view has a container size of 1. sidebar() will take up 20% of the component size with a flex value of 0.2. The body() function will return a <View /> with a flex value of 0.5, accounting for 50% of the view. The remaining rightBar() will fill 10%.

Other attributes

There are some other flexbox style declarations for handling alignment and what to do with excess space in the layout. Once you have the right blocks in place, use justifyContent and alignItems to position the child elements. Flexbox views also work well with pixel-based views like the avatarStyle in the preceding code.

Discussion

Flexbox layouts originated in the web design community as a mechanism for handling the challenge of an ever-changing browser window. Fortunately for web developers, you are probably already familiar with the CSS implementation of flexbox, so you should have little trouble adjusting to React Native’s implementation.

See Also

The React Native documentation provides a helpful guide for laying out flexbox views.

3.3 Importing Image Vectors and Icons

Your app will start coming alive once you include icons and other design cues. Fortunately we can use libraries like react-native-vector-icons.

Problem

How do you decide the best way to display vector images in your application?

Solution

Working with images and binaries is easily done with require() statements, but vectors and icons are special. They do not render out of the box in Android or iOS.

Different solutions exist depending on whether you have a number of vectors files, the complexity of the design, whether or not there are multiple colors in the design, and if you need to target a number of platforms.

Convert to images

The simplest solution in some cases is simply to convert the file into a rasterized file format, like PNG or JPG. The React Native packager is smart enough to detect these dependencies and bundle them together. In order for the file to render correctly for different screen densities, it’s helpful to provide alternative versions of the same file. In this case, I have a vector of a lightbulb, bulb.svg, which has been converted into a number of different pixel density equivalent images:

components
└── images
    ├── bulb.svg
    ├── [email protected]
    ├── [email protected]
    ├── [email protected]
    ├── [email protected]
    ├── [email protected]
    ├── [email protected]
    ├── [email protected]
    └── index.js

Vector editing programs like Adobe Illustrator provide an “Export to Screens” function, making exporting different pixel densities easy, as shown in Figure 3-2.

Use vector image tools to generate the pixel densities required
Figure 3-2. Export to Screens capability in Adobe Illustrator

The index.js file uses a require() statement that can infer the correct image and platform to load:

import React, { Component } from 'react';
import { Image } from 'react-native';

export const Bulb = () => <Image source={require('./bulb.png')} />

In the main application, you can now reference the image as though it were any other React component:

import { Bulb } from './components/images'
export default class App extends Component<{}> {
  render() {
    return (
        <View style={{flex: 1, justifyContent: 'center',
        alignItems: 'center' }}>
        <Bulb />
      </View>
    );
  }
}

There are a couple of solutions to vectors: converting them to SVG markup and using a library or converting them to fonts.

Drawing an SVG

react-native-vector-icons provides a set of React components for describing an SVG using React Native components. At the time of this writing, certain attributes such as clip-path are partially supported. This approach requires essentially redrawing the icon in the application.

The same lightbulb can be exported as the following SVG file:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1"
viewBox="0 0 86 114">
   <defs>
      <style>.cls-1{fill:#dcdfe1;}.cls-1,.cls-4,.cls-5{stroke:#555e65;
      stroke-miterlimit:10;
      stroke-width:2px;}.cls-2{fill:#fff;}.cls-3{fill:#faf7de;}
      .cls-4,.cls-5{fill:none;}
			.cls-4{opacity:0.5;}
      </style>
   </defs>
   <title>bulb</title>
   <g id="Lightbulb">
      <ellipse class="cls-1" cx="43" cy="96.61" rx="6.77" ry="5.42" />
      <ellipse class="cls-1" cx="43" cy="92.55" rx="10.16" ry="5.42" />
      <ellipse class="cls-1" cx="43" cy="88.48" rx="10.16" ry="5.42" />
      <ellipse class="cls-1" cx="43" cy="84.42" rx="10.16" ry="5.42" />
      <path class="cls-2" d="M70.08,39.06A27.09,27.09,0,1,0,23.44,58,20,20,
				0,0,1,29, 72.21v3.41c0,5.61,6.52,10.16,14,
				10.16s14-4.55,14-10.16v-3.4a19.94,19.94,0,0,1,
				5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z" />
      <path class="cls-3" d="M44.5,85.1C38.15,85.1,33,81.45,33,77V73.57a22.24,
				22.24,0,0,0-6.45-15.62,25,25,0,0,1,
				16-42.52q.9-.06,1.82-.06a25.08,25.08,0,0,1,25,
				25.05A24.83,24.83,0,0,1,62.27,58,22,22,0,0,0,56,
				73.57V77C56,81.45,50.85,85.1,44.5,85.1Z" />
      <path class="cls-4" d="M34.2,79c0-3,3.94-5.42,8.8-5.42S51.8,76,51.8,79" />
      <path class="cls-5" d="M50.45,42.44h.15A4.62,4.62,0,0,0,46,
				47.18V52h4.45a4.77,4.77,0,0,0,4.74-4.78v0A4.76,
				4.76,0,0,0,50.45,42.44Z" />
      <path class="cls-5" d="M35.55,42.44h-.15A4.62,4.62,0,0,1,40,
				47.18V52H35.55a4.77,4.77,0,0,1-4.74-4.78v0A4.76,
				4.76,0,0,1,35.55,42.44Z" />
      <polyline class="cls-5" points="46 79 46 52 40 52 40 79" />
      <path class="cls-5" d="M70.08,39.06A27.09,27.09,0,1,0,23.44,58,20,20,
				0,0,1,29,72.21v3.41c0,5.61,6.52,10.16,14,
				10.16s14-4.55,14-10.16v-3.4a19.94,19.94,0,0,1,
				5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z" />
   </g>
</svg>

Because react-native-vector-icons supports a subset of the SVG specification, it would need to be redrawn without the style reference:

import React, { Component } from 'react';
import Svg,{
    Ellipse,
    Path,
    Polyline,
} from 'react-native-svg';

export default function() {
  return <Svg height="130" width="100">
    <Ellipse cx="43" cy="96.61" rx="6.77" ry="5.42"   fill="#dcdfe1"
    stroke="#555e65" strokeWidth="2" />
    <Ellipse cx="43" cy="92.55" rx="10.16" ry="5.42"  fill="#dcdfe1"
    stroke="#555e65" strokeWidth="2"/>
    <Ellipse cx="43" cy="88.48" rx="10.16" ry="5.42"  fill="#dcdfe1"
    stroke="#555e65" strokeWidth="2"/>
    <Ellipse cx="43" cy="84.42" rx="10.16" ry="5.42"  fill="#dcdfe1"
    stroke="#555e65" strokeWidth="2"/>
    <Path fill="#Faf7de"  d="M70.08,39.06A27.09,27.09,0,1,0,23.44,58,20,20,0,
			0,1,29,72.21v3.41c0,5.61,6.52,10.16,14,10.16s14-4.55,
			14-10.16v-3.4a19.94,19.94,0,0,1,
			5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z" />
    <Path fill="none" d="M44.5,85.1C38.15,85.1,33,81.45,33,77V73.57a22.24,22.24,
			0,0,0-6.45-15.62,25,25,0,0,1,16-42.52q.9-.06,
			1.82-.06a25.08,25.08,0,0,1,25,25.05A24.83,24.83,
			0,0,1,62.27,58,22,22,0,0,0,56,73.57V77C56,81.45,50.85,
			85.1,44.5,85.1Z" />
    <Path fill="none" d="M34.2,79c0-3,3.94-5.42,8.8-5.42S51.8,76,51.8,79" />
    <Path stroke="#555e65" strokeWidth="2" fill="none" d="M50.45,42.44h.15A4.62,
			4.62,0,0,0,46,47.18V52h4.45a4.77,4.77,
			0,0,0,4.74-4.78v0A4.76,4.76,0,0,0,
			50.45,42.44Z" />
    <Path stroke="#555e65" strokeWidth="2" fill="none" d="M35.55,42.44h-.15A4.62,
			4.62,0,0,1,40,47.18V52H35.55a4.77,4.77,0,0,
			1-4.74-4.78v0A4.76,4.76,0,0,1,35.55,42.44Z" />
    <Polyline stroke="#555e65" strokeWidth="2" fill="none"
    points="46 79 46 52 40 52 40 79" />
    <Path stroke="#555e65" strokeWidth="2" fill="none" d="M70.08,39.06A27.09,
			27.09,0,1,0,23.44,58,20,20,0,0,1,29,72.21v3.41c0,
			5.61,6.52,10.16,14,10.16s14-4.55,14-10.16v-3.4a19.94,
			19.94,0,0,1,5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z" />
  </Svg>
}

The added benefit of this approach is that every attribute can be edited and animated using the rest of the React Native ecosystem. In some cases this kind of effort makes a lot of sense; for example, if you want a vector image to change based on user interaction.

Converting it to a font

If you plan on using the vector in multiple colors and it doesn’t contain any color details, consider making a custom font. IcoMoon makes it easy to turn your vector art into a single font (Figure 3-3).

Upload your SVGs to services like IcoMoon to get a custom font
Figure 3-3. The IcoMoon website makes it easy to build a custom font from SVGs

This approach harkens to the Wyndings font developed by Microsoft decades ago and uses the font file format to represent vector images.

The react-native-vector-icons library provides a set of font wrapper functions in addition to commonly used icon sets like FontAwesome, MaterialIcons, and Ionicons.

Install it like any other React Native package via NPM:

$> npm install react-native-vector-icons --save
$> react-native link

A folder will be created in android/app/src/main/assets/fonts for Android as shown in Figure 3-4.

Fonts in the assets/fonts folder will become available to the app.
Figure 3-4. Android Studio requires a copy of the font

The linker should also add a Resources folder to your iOS project file that contains the set of free fonts. I suggest making sure that the free fonts provided are rendering correctly in your application before loading any custom fonts.

To add an icon set you’ve downloaded from IcoMoon, you will need two files from the ZIP file provided by IcoMoon: selection.json and icomoon.ttf. The IcoMoon package will compile all your vector images into different character keys of a font.

For iOS, you will then need to reference the icomoon.ttf file in the Resources folder and include it as part of the list of Fonts provided by application in the info.plist as shown in Figure 3-5. For Android, copy the icomoon.ttf file to the android/app/src/main/assets/fonts folder.

Add icomoon.ttf as a project reference and in the Info.plist
Figure 3-5. Configure Xcode to reference icomoon.ttf

You can now reference the component by icon name. Following is an example of using the icomoon.ttf file with an icon called webinar next to a FontAwesome icon called rocket:

import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';

// Custom IcoMoon Icon
import { createIconSetFromIcoMoon } from 'react-native-vector-icons';
import icoMoonConfig from './fonts/selection.json';
const Icon = createIconSetFromIcoMoon(icoMoonConfig);


export default class App extends Component<{}> {
  render() {
    return (
        <View style={{flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Icon name='webinar' size={30} color='#F00' />
        <FontAwesomeIcon name='rocket' size={30} color='#333' />
      </View>
    );
  }
}

Discussion

Any binary assets need to be bundled with your React Native project. iOS and Android will both need references to those assets.

3.4 Looping Animations

In Recipe 2.2, we used the react-native-progress component to build a pie chart that would change progress amounts based on a user tapping <TouchableHighlight />. Indeterminate progress can be presented to the user by combining the Animated library provided by React Native and the react-native-progress component. By combining these two libraries, we can build a simple component that will loop forever.

Problem

How do you communicate that a task is in process when you don’t know how long it will take?

Solution

Indeterminate progress indicators help you buy time while your application finishes loading. Let’s start by defining a constructor with a local state variable in the components/loading.js file:

  constructor(props) {
    super(props);
    this.state = {
      loop: new Animated.Value(0),
    };
  }

The loop variable will refer to an instance of Animated.Value that increments from 0 to 1.

componentDidMount() is a special function React will call before it renders a component for the first time. We will use this hook into the render loop to configure our loop:

  componentDidMount() {
    Animated.loop(
      Animated.timing(this.state.loop, {
        toValue: 1,
        duration: 500,
      }),
    ).start();
  }

Finally we will set up an interpolation function so that a corresponding rotation degree results from every value of this.state.loop between 0 and 1. We do not have a direct reference to the animation loop because all interpolation is happening within native components that we are configuring. This approach ensures smooth animations across platforms.

The render() function relies on react-native-progress first presented in Recipe 2.2:

  render() {
    const interpolation = this.state.loop.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '360deg']
    })
    const animationStyle = {
      transform: [ { rotate: interpolation }  ]
    }
    return <View>
      <Animated.View style={animationStyle}>
        <Pie borderWidth={2} progress={0.2} size={100} color='#2224FF' />
      </Animated.View>
    </View>
  }

The completed <Loading /> component looks like this:

import React, { Component } from 'react';
import {
  Animated,
  View
} from 'react-native';

import Progress, { Pie } from 'react-native-progress';

export default class Loading extends Component {
  constructor(props) {
    super(props);
    this.state = {
      loop: new Animated.Value(0),
    };
  }

  componentDidMount() {
    Animated.loop(
      Animated.timing(this.state.loop, {
        toValue: 1,
        duration: 500,
      }),
    ).start();
  }

  render() {
    const interpolation = this.state.loop.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '360deg']
    })
    const animationStyle = {
      transform: [ { rotate: interpolation }  ]
    }
    return <View>
      <Animated.View style={animationStyle}>
        <Pie borderWidth={2} progress={0.2} size={100} color='#2224FF' />
      </Animated.View>
    </View>
  }
}

Discussion

In this example, you will notice that the animation is applied to an <Animated.View /> component instead of a regular <View /> component. These components are designed to accept values from either an interpolation or an Animated.Value component. This approach avoids calling on the React.js render pipeline, which would increase the overhead required to render a single frame of the animation.

You should be able to include the <Loading /> component in your application and watch a spinning pie animation.

See Also

The React Native documentation provides an extensive guide explaining some of the design choices. There are also plenty of examples.

See the React Native Animation Guide.

1 See Chapter 9 of Learning React Native, 1E (O’Reilly Media) for more about responsive design and font sizes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.173.249