React Native Bridging — How to Make Linear Gradient View

Khoa Pham
Khoa Pham
Oct 18, 2018 · 9 min read
Image for post
Image for post

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.

Image for post
Image for post

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.

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

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

Image for post
Image for post

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 RNGradientViewManagerRCT_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

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 RNGradientView : RCTView@end

RNGradientView.m

#import "RNGradientView.h"
#import <UIKit/UIKit.h>
@interface RNGradientView()
@property CAGradientLayer *gradientLayer;
@property UIColor *_fromColor;
@property UIColor *_toColor;
@property NSNumber *_progress;
@property NSNumber *_cornerRadius;
@end
@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.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

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

Image for post
Image for post

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
public String getName() {
return "RNGradientView";
}
@Override
protected RNGradientView createViewInstance(ThemedReactContext reactContext) {
return new RNGradientView(reactContext);
}
// Properties@ReactProp(name = "progress")
public void setProgress(RNGradientView view, @Nullable float progress) {
view.setProgress(progress);
}
@ReactProp(name = "cornerRadius")
public void setCornerRadius(RNGradientView view, @Nullable float cornerRadius) {
view.setCornerRadius(cornerRadius);
}
@ReactProp(name = "fromColor", customType = "Color")
public void setFromColor(RNGradientView view, @Nullable int color) {
view.setFromColor(color);
}
@ReactProp(name = "toColor", customType = "Color")
public void setToColor(RNGradientView view, @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

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"
android:shape="rectangle">
<gradient
android:startColor="#3B5998"
android:endColor="#00000000"
android:angle="45"/>
</shape>

For more information, you can read

We can also use the class directly ShapeDrawable in code

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
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new RNGradientViewManager()
);
}
}

Then head over to MainApplication.java to declare our package

@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.

If you like this post, consider visiting my other articles and apps 🔥

React Native Training

Stories and tutorials for developers interested in React…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store