How to Make Linear Gradient View with Bridging in React Native

Issue #264

Original post https://medium.com/react-native-training/react-native-bridging-how-to-make-linear-gradient-view-83c3805373b7


React Native lets us build mobile apps using only Javascript. It works by providing a common interface that talks to native iOS and Android components. There are enough essentials components to get started, but the cooler thing is that it is easy to build our own, hence we are not limited by React Native. In this post we will implement a linear gradient view, which is not supported by default in React Native, using native UI component, particularly CAGradientLayer in iOS and GradientDrawable in Android.

In Javascript there are hundreds of libraries for a single problem and you should check if you really need it or not. A search on Google for linear gradient shows a bunch of libraries, like react-native-linear-gradient. The less dependencies the better. Linear gradient is in fact very easy to build and we probably don’t need to add extra dependencies. Also integrating and following updates with 3rd libraries are painful, I would avoid that as much as possible.

Native UI component vs Native module

In React Native, there are native UI component and native module. React Native moves pretty fast so most of the articles will be outdated, it’s best to consult official documentation for the latest React Native version. This post will try to give you overview of the whole picture because for now the official guide seems not completed.

In simple explanation, native UI component is about making UIView in iOS or View in Android available as React.Component and used in render function in Javascript.

There are tons of native UI widgets out there ready to be used in the latest apps — some of them are part of the platform, others are available as third-party libraries, and still more might be in use in your very own portfolio. React Native has several of the most critical platform components already wrapped, like ScrollView and TextInput, but not all of them, and certainly not ones you might have written yourself for a previous app.

Native module is more general in that we make any native class available in Javascript.

Sometimes an app needs access to platform API, and React Native doesn’t have a corresponding module yet. Maybe you want to reuse some existing Objective-C, Swift or C++ code without having to reimplement it in JavaScript, or write some high performance, multi-threaded code such as for image processing, a database, or any number of advanced extensions.

View Manager

To expose native UI views, we use the ViewManager as the bridge, it is RCTViewManager in iOS and SimpleViewManager in Android. Then inside this ViewManager we can just return our custom view. I see it’s good to use Objective C/Java for the ViewManager to match React Native classes, and the custom view we can use either Swift/Objective C in iOS and Kotlin/Java in Android.

I prefer to use Swift, but in this post to remove the overhead of introducing bridging header from Swift to Objective C, we use Objective C for simplicity. We also add the native source code directly into iOS and Android project, but in the future we can extract them easily to a React Native library.

For now let ‘s use the name RNGradientViewManager and RNGradientView to stay consistent between iOS and Android. The RN prefix is arbitrary, you can use any prefix you want, but here I use it to indicate that these classes are meant to be used in Javascript side in React Native.

Implement in iOS

Project structure

Add these Objective-C classes to the projects, I usually place them inside NativeComponents folder

Native views are created and manipulated by subclasses of RCTViewManager. These subclasses are similar in function to view controllers, but are essentially singletons - only one instance of each is created by the bridge. They expose native views to the RCTUIManager, which delegates back to them to set and update the properties of the views as necessary. The RCTViewManagers are also typically the delegates for the views, sending events back to JavaScript via the bridge.

RNGradientViewManager

Create a RNGradientViewManager that inherits from RCTViewManager

RNGradientViewManager.h

#import <React/RCTViewManager.h>
@interface RNGradientViewManager : RCTViewManager
@end

RNGradientViewManager.m

#import "RNGradientViewManager.h"
#import "RNGradientView.h"

[@implementation](http://twitter.com/implementation) RNGradientViewManager

RCT_EXPORT_MODULE()

- (UIView *)view {
  return [[RNGradientView alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(progress, NSNumber);
RCT_EXPORT_VIEW_PROPERTY(cornerRadius, NSNumber);
RCT_EXPORT_VIEW_PROPERTY(fromColor, UIColor);
RCT_EXPORT_VIEW_PROPERTY(toColor, UIColor);

[@end](http://twitter.com/end)

In iOS we use macro RCT_EXPORT_MODULE() to automatically register the module with the bridge when it loads. The optional js_name argument will be used as the JS module name. If omitted, the JS module name will match the Objective-C class name.

#define RCT_EXPORT_MODULE(js_name)

The ViewManager, not the View, is the facade to the Javascript side, so we expose properties using RCT_EXPORT_VIEW_PROPERTY . Note that we do that inside @implementation RNGradientViewManager

Here we specify the types as NSNumber and UIColor , and later in Javascript we can just pass number and color hex string, and React Native can do the conversions for us. In older versions of React Native, we need processColor in Javascript or RCTConvert color in iOS side, but we don’t need to perform manual conversion now.

RNGradientView

In the Native UI component example for iOS, they use WKWebView but here we make a RNGradientView which subclasses from RCTView to take advantage of many features of React Native views, and to avoid some problems we can get if using a normal UIView

RNGradientView.h

#import <UIKit/UIKit.h>
#import <React/RCTView.h>

[@interface](http://twitter.com/interface) RNGradientView : RCTView

[@end](http://twitter.com/end)

RNGradientView.m

#import "RNGradientView.h"
#import <UIKit/UIKit.h>

[@interface](http://twitter.com/interface) RNGradientView()
[@property](http://twitter.com/property) CAGradientLayer *gradientLayer;

[@property](http://twitter.com/property) UIColor *_fromColor;
[@property](http://twitter.com/property) UIColor *_toColor;
[@property](http://twitter.com/property) NSNumber *_progress;
[@property](http://twitter.com/property) NSNumber *_cornerRadius;
[@end](http://twitter.com/end)

[@implementation](http://twitter.com/implementation) RNGradientView

// MARK: - Init

- (instancetype)initWithFrame:(CGRect)frame
{
  self = [super initWithFrame:frame];
  if (self) {
    self.gradientLayer = [self makeGradientLayer];
    [self.layer addSublayer:self.gradientLayer];

self._fromColor = [UIColor blackColor];
    self._toColor = [UIColor whiteColor];
    self._progress = [@0](http://twitter.com/0).5;

[self update];
  }
  return self;
}

// MARK: - Life cycle

- (void)layoutSubviews {
  [super layoutSubviews];

self.gradientLayer.frame = CGRectMake(
    0, 0,
    self.bounds.size.width*self._progress.floatValue,
    self.bounds.size.height
  );
}

// MARK: - Properties

- (void)setFromColor:(UIColor *)color {
  self._fromColor = color;
  [self update];
}

- (void)setToColor:(UIColor *)color {
  self._toColor = color;
  [self update];
}

- (void)setProgress:(NSNumber *)progress {
  self._progress = progress;
  [self update];
}

- (void)setCornerRadius:(NSNumber *)cornerRadius {
  self._cornerRadius = cornerRadius;
  [self update];
}

// MARK: - Helper

- (void)update {
  self.gradientLayer.colors = @[
    (id)self._fromColor.CGColor,
    (id)self._toColor.CGColor
  ];

self.gradientLayer.cornerRadius = self._cornerRadius.floatValue;

[self setNeedsLayout];
}

- (CAGradientLayer *)makeGradientLayer {
  CAGradientLayer *gradientLayer = [CAGradientLayer layer];

gradientLayer.masksToBounds = true;

gradientLayer.startPoint = CGPointMake(0.0, 0.5);
  gradientLayer.endPoint = CGPointMake(1.0, 0.5);
  gradientLayer.anchorPoint = CGPointZero;

return gradientLayer;
}

[@end](http://twitter.com/end)

We can implement anything we want in this native view, in this case we use CAGradientLayer to get nicely displayed linear gradient. Since RNGradientViewManager exposes some properties like progress, cornerRadius, fromColor, toColor we need to implement some setters as they will be called by React Native when we update values in Javascript side. In the setter we call setNeedsLayout to tell the view to invalidate the layout, hence layoutSubviews will be called again.

requireNativeComponent

Open project in Visual Studio Code, add GradientView.js to src/nativeComponents . The folder name is arbitrary, but it’s good to stay organised.

import { requireNativeComponent } from 'react-native'

module.exports = requireNativeComponent('RNGradientView', null)

Here we use requireNativeComponent to load our RNGradientView . We only need this one Javascript file for interacting with both iOS and Android. You can name the module as RNGradientView but I think the practice in Javascript is that we don’t use prefix, so we name just GradientView .

const requireNativeComponent = (uiViewClassName: string): string =>
  createReactNativeComponentClass(uiViewClassName, () =>
    getNativeComponentAttributes(uiViewClassName),
  );

module.exports = requireNativeComponent;

Before I tried to use export default for the native component, but this way the view is not rendered at all, even if I wrap it inside React.Component . It seems we must use module.exports for the native component to be properly loaded.

Now using it is as easy as declare the GradientView with JSX syntax

import GradientView from 'nativeComponents/GradientView'

export default class Profile extends React.Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <GradientView
          style={styles.progress}
          fromColor={R.colors.progress.from}
          toColor={R.colors.progress.to}
          cornerRadius={5.0}
          progress={0.8} />
    )
  }
}

Implement in Android

Project structure

Add these Java classes to the projects, I usually place them inside nativeComponents folder

RNGradientManager

Create a RNGradientManager that extends SimpleViewManager
RNGradientManager.java

package com.onmyway133.myApp.nativeComponents;

import android.support.annotation.Nullable;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;

public class RNGradientViewManager extends SimpleViewManager<RNGradientView> {
    [@Override](http://twitter.com/Override)
    public String getName() {
        return "RNGradientView";
    }

[@Override](http://twitter.com/Override)
    protected RNGradientView createViewInstance(ThemedReactContext reactContext) {
        return new RNGradientView(reactContext);
    }

// Properties

[@ReactProp](http://twitter.com/ReactProp)(name = "progress")
    public void setProgress(RNGradientView view, [@Nullable](http://twitter.com/Nullable) float progress) {
        view.setProgress(progress);
    }

[@ReactProp](http://twitter.com/ReactProp)(name = "cornerRadius")
    public void setCornerRadius(RNGradientView view, [@Nullable](http://twitter.com/Nullable) float cornerRadius) {
        view.setCornerRadius(cornerRadius);
    }

[@ReactProp](http://twitter.com/ReactProp)(name = "fromColor", customType = "Color")
    public void setFromColor(RNGradientView view, [@Nullable](http://twitter.com/Nullable) int color) {
        view.setFromColor(color);
    }

[@ReactProp](http://twitter.com/ReactProp)(name = "toColor", customType = "Color")
    public void setToColor(RNGradientView view, [@Nullable](http://twitter.com/Nullable) int color) {
        view.setToColor(color);
    }
}

We usually use Color as android.graphics.Color , but for the GradientDrawable that we are going to use, it use color as ARGB integer. So it’s nifty that React Native deals with Color as int type. We also need to specify customType = “Color” as Color is something kinda custom.

RNGradientView

This is where we implement our view, we can do that in Kotlin if we like.

RNGradientView.java

package com.onmyway133.myApp.nativeComponents;

import android.content.Context;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ScaleDrawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;

public class RNGradientView extends View {

    float progress;
    float cornerRadius;
    int fromColor;
    int toColor;

    public RNGradientView(Context context) {
        super(context);
    }

    public RNGradientView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RNGradientView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public RNGradientView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    // update

    void update() {
        GradientDrawable gradient = new GradientDrawable();
        gradient.setColors(new int[] {
            this.fromColor,
            this.toColor
        });
         gradient.setOrientation(GradientDrawable.Orientation.*LEFT_RIGHT*);
        gradient.setGradientType(GradientDrawable.*LINEAR_GRADIENT*);
        gradient.setShape(GradientDrawable.*RECTANGLE*);
        gradient.setCornerRadius(this.cornerRadius * 4);

        ScaleDrawable scale = new ScaleDrawable(gradient, Gravity.*LEFT*, 1, -1);
        scale.setLevel((int)(this.progress * 10000));

        this.setBackground(scale);
    }

    // Getter & setter

    public void setProgress(float progress) {
        this.progress = progress;
        this.update();
    }

    public void setCornerRadius(float cornerRadius) {
        this.cornerRadius = cornerRadius;
        this.update();
    }

    public void setFromColor(int fromColor) {
        this.fromColor = fromColor;
        this.update();
    }

    public void setToColor(int toColor) {
        this.toColor = toColor;
        this.update();
    }
}

Pay attention to the setColors as it use an array of int

Sets the colors used to draw the gradient.
Each color is specified as an ARGB integer and the array must contain at least 2 colors.

If we call setBackground with the GradientDrawable it will be stretched to fill the view. In our case we want to support progress which determines how long the gradient should show. To fix that we use ScaleDrawable which is a Drawable that changes the size of another Drawable based on its current level value.

The same value for cornerRadius works in iOS, but for Android we need to use higher values, that’s why the multiplication in gradient.setCornerRadius(this.cornerRadius * 4)

Shape drawable

Another way to implement gradient is to use Shape Drawable with xml , it’s the equivalent of using xib in iOS. We can create something like gradient.xml and put that inside /res/drawable

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
    android:shape="rectangle">
    <gradient
        android:startColor="#3B5998"
        android:endColor="#00000000"
        android:angle="45"/>    
</shape>

For more information, you can read
Android Shape Drawables Tutorial
Have you ever wanted to reduce your Android application’s size or make it look more interesting? If yes, then you…android.jlelse.eu

We can also use the class directly ShapeDrawable in code

A Drawable object that draws primitive shapes. A ShapeDrawable takes a Shape object and manages its presence on the screen. If no Shape is given, then the ShapeDrawable will default to a RectShape.
This object can be defined in an XML file with the element.

GradientManagerPackage

In iOS we use RCT_EXPORT_MODULE to register the component, but in Android, things are done explicitly using Package . A package can register both native module and native UI component. In this case we deal with just UI component, so let’s return RNGradientManager in createViewManagers

GradientManagerPackage.java

package com.onmyway133.myApp.nativeComponents;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class RNGradientViewPackage implements ReactPackage {
    [@Override](http://twitter.com/Override)
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

[@Override](http://twitter.com/Override)
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
            new RNGradientViewManager()
        );
    }
}

Then head over to MainApplication.java to declare our package

[@Override](http://twitter.com/Override)
protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
      new MainReactPackage(),
      new RNGradientViewPackage()
  );
}

That’s it for Android. We already have the GradientView.js written earlier, when running the app in Android, it will look up and load our RNGradientView

Where to go from here

Hope you learn something about native UI component. In the post we only touch the surfaces on what native UI component can do, which is just passing configurations from Javascript to native. There are a lot more to discover, like event handling, thread, styles, custom types, please consult the official documentation for correct guidance.

Comments