How to Make Linear Gradient View
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
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
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, likeScrollView
andTextInput
, 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 ofRCTViewManager
. 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 theRCTUIManager
, which delegates back to them to set and update the properties of the views as necessary. TheRCTViewManager
s 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 : [email protected]
RNGradientViewManager.m
#import "RNGradientViewManager.h"#import "RNGradientView.h"
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
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 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
// 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 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 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 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 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
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 .
This object can be defined in an XML file with the <shape>
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 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
@Overrideprotected 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.