Create a Hand-Drawing Component Using React Native Skia

Recently, I was assigned the task of adding a hand-drawing component to a React Native application. While searching for the ideal library, I came across React Native Skia — a high-performance 2D graphics library that utilizes the same rendering engine that powers Flutter. Upon further investigation, I discovered several blog posts that appeared to be exactly what I needed.

Although they were well-written and helpful, these blog posts were either outdated or relied on additional unnecessary libraries. That made the process more complex than simply copying and pasting code into my project. I decided to create a tutorial that offers a clear and up-to-date approach, using only React Native Skia as the third-party dependency.

Acknowledgment

Before getting started, I’d like to quickly acknowledge the blog posts that helped me accomplish this task:

A Non-Interactive Example

Before building an interactive hand-drawing component, let’s create a non-interactive example to familiarize ourselves with the library. This process will give us a better understanding of the building blocks that we’ll use to build this component: <Path /> and <Canvas />.

Our first example will be a simple component that draws a star.


import { Canvas, Path, Skia } from "@shopify/react-native-skia";
import React from "react";
import { StyleSheet } from "react-native";

// copied from https://shopify.github.io/react-native-skia/docs/shapes/path#using-path-object
const path = Skia.Path.Make();
path.moveTo(128, 0);
path.lineTo(168, 80);
path.lineTo(256, 93);
path.lineTo(192, 155);
path.lineTo(207, 244);
path.lineTo(128, 202);
path.lineTo(49, 244);
path.lineTo(64, 155);
path.lineTo(0, 93);
path.lineTo(88, 80);
path.lineTo(128, 0);
path.close();

export const SketchCanvasWithoutInteraction = () => {
  return (
    <Canvas style={style.container}>
      <Path path={path} color={"black"} style={"stroke"} strokeWidth={2} />
    </Canvas>
  );
};

const style = StyleSheet.create({
  container: {
    flex: 1,
    width: "100%",
  }
});


After completing the non-interactive example, you should see a star shape that was rendered using React Native Skia’s <Canvas /> and <Path /> components.

The <Canvas /> component serves as the root for your drawing. If you are familiar with the HTML canvas element, it behaves similarly. It behaves like a regular React Native view. However, behind the scenes, it uses its own renderer to display its components.

The <Path /> component can be used to create various shapes, including lines, curves, and arcs. Paths are extremely useful because they can form complex shapes by combining multiple straight or curved lines. In this instance, we used it to draw the lines of the star.

The library provides other shapes, such as rectangles and circles, but since we will only being using Paths, I left those out for now.

Adding Interactivity

Now that we’ve familiarized ourselves with React Native Skia’s <Canvas /> and <Path /> components, let’s introduce some interactivity!

import {
  Canvas,
  Path,
  SkPath,
  Skia,
  TouchInfo,
  useTouchHandler,
} from "@shopify/react-native-skia";
import React, { useCallback, useState } from "react";
import { StyleSheet } from "react-native";

export const SketchCanvasWithInteraction = () => {
  const [paths, setPaths] = useState<SkPath[]>([]);

  const onDrawingStart = useCallback((touchInfo: TouchInfo) => {
    setPaths((old) => {
      const { x, y } = touchInfo;
      const newPath = Skia.Path.Make();
      newPath.moveTo(x, y);
      return [...old, newPath];
    });
  }, []);

  const onDrawingActive = useCallback((touchInfo: TouchInfo) => {
    setPaths((currentPaths) => {
      const { x, y } = touchInfo;
      const currentPath = currentPaths[currentPaths.length - 1];
      const lastPoint = currentPath.getLastPt();
      const xMid = (lastPoint.x + x) / 2;
      const yMid = (lastPoint.y + y) / 2;

      currentPath.quadTo(lastPoint.x, lastPoint.y, xMid, yMid);
      return [...currentPaths.slice(0, currentPaths.length - 1), currentPath];
    });
  }, []);

  const touchHandler = useTouchHandler(
    {
      onActive: onDrawingActive,
      onStart: onDrawingStart,
    },
    [onDrawingActive, onDrawingStart]
  );

  return (
    <Canvas style={style.container} onTouch={touchHandler}>
      {paths.map((path, index) => (
        <Path
          key={index}
          path={path}
          color={"black"}
          style={"stroke"}
          strokeWidth={2}
        />
      ))}
    </Canvas>
  );
};

const style = StyleSheet.create({
  container: {
    flex: 1,
    width: "100%",
  },
});

 

Breaking Down the Steps

If you use the above component, you should now be able to draw on your screen! Let’s break down what we added:

First, we create an array using useState to hold all the lines that the user adds.

React Native Skia provides an easy way for the component to detect when a user is interacting with it. The useTouchHandler takes an object that has three callbacks on it: onStart, onActive, and onEnd. In this example we are only using onStart and onActive since there is no need to use onEnd here.

  • onStart is called when the user first presses on the component. In our case, we create a new path that starts at the XY coordinates that the user started pressing at and add it to the paths state. If the user lifts there finger and puts it down again, the onStart method is called again, adding another path to the state.
  • The onActive callback is called after the onActive function has been called when the user starts moving there finger. Here we are able to add lines to the last path in our component using the quadTo method on the path object.

Next, we pass the return value of the useTouchHandler to the prop onTouch on the <Canvas /> element.

Inside of the <Canvas /> element, we loop over the paths in our state array and render them using the <Path /> component. We are utilizing the following props:

  • path – The path the user drew.
  • color – The color of the line being drawn.
  • style – The paint style we are using, “fill” or “stroke.” We are using “stroke” in this example to prevent the space between lines from being filled in.
  • strokeWidth – The thickness of the line that was drawn.

Customizing the Brush

Users can now draw using this component! But they can only draw black lines of uniform width. Let’s allow users to get more creative by drawing different color lines of varying widths!

import {
  Canvas,
  Path,
  SkPath,
  Skia,
  TouchInfo,
  useTouchHandler,
} from "@shopify/react-native-skia";
import React, { useCallback, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

type PathWithColorAndWidth = {
  path: SkPath;
  color: Color;
  strokeWidth: number;
};

export const SketchCanvasWithInteractionAndCustomization = () => {
  const [paths, setPaths] = useState<PathWithColorAndWidth[]>([]);
  const [color, setColor] = useState<Color>(Colors[0]);

  const [strokeWidth, setStrokeWidth] = useState(strokes[0]);

  const onDrawingStart = useCallback(
    (touchInfo: TouchInfo) => {
      setPaths((currentPaths) => {
        const { x, y } = touchInfo;
        const newPath = Skia.Path.Make();
        newPath.moveTo(x, y);
        return [
          ...currentPaths,
          {
            path: newPath,
            color,
            strokeWidth,
          },
        ];
      });
    },
    [color, strokeWidth]
  );

  const onDrawingActive = useCallback((touchInfo: TouchInfo) => {
    setPaths((currentPaths) => {
      const { x, y } = touchInfo;
      const currentPath = currentPaths[currentPaths.length - 1];
      const lastPoint = currentPath.path.getLastPt();
      const xMid = (lastPoint.x + x) / 2;
      const yMid = (lastPoint.y + y) / 2;

      currentPath.path.quadTo(lastPoint.x, lastPoint.y, xMid, yMid);
      return [...currentPaths.slice(0, currentPaths.length - 1), currentPath];
    });
  }, []);

  const touchHandler = useTouchHandler(
    {
      onActive: onDrawingActive,
      onStart: onDrawingStart,
    },
    [onDrawingActive, onDrawingStart]
  );

  return (
    <View style={style.container}>
      <Toolbar
        color={color}
        strokeWidth={strokeWidth}
        setColor={setColor}
        setStrokeWidth={setStrokeWidth}
      />
      <Canvas style={style.container} onTouch={touchHandler}>
        {paths.map((path, index) => (
          <Path
            key={index}
            path={path.path}
            color={path.color}
            style={"stroke"}
            strokeWidth={path.strokeWidth}
          />
        ))}
      </Canvas>
    </View>
  );
};

const Colors = ["black", "red", "blue", "green", "yellow", "white"] as const;

type Color = (typeof Colors)[number];

type ToolbarProps = {
  color: Color;
  strokeWidth: number;
  setColor: (color: Color) => void;
  setStrokeWidth: (strokeWidth: number) => void;
};

const strokes = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

const Toolbar = ({
  color,
  strokeWidth,
  setColor,
  setStrokeWidth,
}: ToolbarProps) => {
  const [showStrokes, setShowStrokes] = useState(false);

  const handleStrokeWidthChange = (stroke: number) => {
    setStrokeWidth(stroke);
    setShowStrokes(false);
  };

  const handleChangeColor = (color: Color) => {
    setColor(color);
  };

  return (
    <>
      {showStrokes && (
        <View style={[style.toolbar, style.strokeToolbar]}>
          {strokes.map((stroke) => (
            <Pressable
              onPress={() => handleStrokeWidthChange(stroke)}
              key={stroke}
            >
              <Text style={style.strokeOption}>{stroke}</Text>
            </Pressable>
          ))}
        </View>
      )}
      <View style={[style.toolbar]}>
        <Pressable
          style={style.currentStroke}
          onPress={() => setShowStrokes(!showStrokes)}
        >
          <Text>{strokeWidth}</Text>
        </Pressable>
        <View style={style.separator} />
        {Colors.map((item) => (
          <ColorButton
            isSelected={item === color}
            key={item}
            color={item}
            onPress={() => handleChangeColor(item)}
          />
        ))}
      </View>
    </>
  );
};

type ColorButtonProps = {
  color: Color;
  isSelected: boolean;
  onPress: () => void;
};

const ColorButton = ({ color, onPress, isSelected }: ColorButtonProps) => {
  return (
    <Pressable
      onPress={onPress}
      style={[
        style.colorButton,
        { backgroundColor: color },
        isSelected && {
          borderWidth: 2,
          borderColor: "black",
        },
      ]}
    />
  );
};

const style = StyleSheet.create({
  container: {
    flex: 1,
    width: "100%",
  },
  strokeOption: {
    fontSize: 18,
    backgroundColor: "#f7f7f7",
  },
  toolbar: {
    backgroundColor: "#ffffff",
    height: 50,
    width: 300,
    borderRadius: 100,
    borderColor: "#f0f0f0",
    borderWidth: 1,
    flexDirection: "row",
    paddingHorizontal: 12,
    justifyContent: "center",
    alignItems: "center",
    alignSelf: "center",
  },
  separator: {
    height: 30,
    borderWidth: 1,
    borderColor: "#f0f0f0",
    marginHorizontal: 10,
  },
  currentStroke: {
    backgroundColor: "#f7f7f7",
    borderRadius: 5,
  },
  strokeToolbar: {
    position: "absolute",
    top: 70,
    justifyContent: "space-between",
    zIndex: 100,
  },
  colorButton: {
    width: 30,
    height: 30,
    borderRadius: 100,
    marginHorizontal: 5,
  },
});

 

 

Using React Native Skia

If you run this example you should be able to switch between colors and line width! Here’s what we did to achieve this:

  • Added a component that allows us to select colors and stroke widths from a predefined list.
  • Added state to track the currently selected color and stroke width.
  • Store the color and stroke width selected when a line was created with the line.
  • Use the stroke width and color properties on the <Path /> component.

Congratulations! You can now create a hand-drawable component using React Native Skia. Feel free to use any of the code referenced in this article. You can see the complete code written in this tutorial here.

Conversation
  • RODIAS FIRSDYNO SEMEVO GOHOUE says:

    i got this error while trying to paste the code and after installed skia library for react native

    Cannot read property ‘canvas’ of undefiened

  • Join the conversation

    Your email address will not be published. Required fields are marked *