Android implementation
Let's use Android Studio for writing Android code. Launch Android Studio and open the project under <your-project-dir>/android
path
When the project is opened, find conic-gradient-package
inside project-tree
The conic-gradient-package
contains 3 packages with the same name com.conicgradientpackage
. After expanding them, you'll notice that these contain following things:
- code-generated Java spec files
ConicGradientViewManager
view manager class stub filesConicGradientView
class stub fileConicGradientTurboPackage
class stub file
Let's begin!
- Kotlin
- Java
ConicGradientView.kt
Let's start by declaring the custom view that will extend ReactViewGroup
(the android class that backs <View />
implementation):
package com.conicgradientpackage
import android.graphics.*
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.views.view.ReactViewGroup
class ConicGradientView(private val reactContext: ReactContext) : ReactViewGroup(reactContext) {
private var mColors = mutableListOf(Color.RED, Color.YELLOW)
private var mLocations = mutableListOf(0f, 1f)
private var mCenterPointX = 0.5
private var mCenterPointY = 0.5
fun setColors(colors: ReadableArray) {
mColors.clear()
for (i in 0 until colors.size()) {
if (colors.getType(i) == ReadableType.Map) {
mColors.add(i, ColorPropConverter.getColor(colors.getMap(i), reactContext))
} else {
mColors.add(i, colors.getInt(i))
}
}
}
fun setLocations(locations: ReadableArray) {
mLocations.clear()
for (i in 0 until locations.size()) {
mLocations.add(i, locations.getDouble(i).toFloat())
}
}
fun setCenterPoint(centerPoint: ReadableMap) {
mCenterPointX = if (centerPoint.hasKey("x") && !centerPoint.isNull("x")) {
centerPoint.getDouble("x")
} else {
0.5
}
mCenterPointY = if (centerPoint.hasKey("y") && !centerPoint.isNull("y")) {
centerPoint.getDouble("y")
} else {
0.5
}
}
companion object {
const val NAME = "ConicGradientView"
}
}
In setColors
, setLocations
, setCenterPoint
we save and parse the values received from props.
And we create static NAME
with the name based on JS specification.
Looks good, but it doesn't do anything with gradient rendering - let's add it:
class ConicGradientView(private val reactContext: ReactContext) : ReactViewGroup(reactContext) {
private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mPath = Path()
private var mRect = RectF()
private var mColors = mutableListOf(Color.RED, Color.YELLOW)
private var mLocations = mutableListOf(0f, 1f)
private var mCenterPointX = 0.5
private var mCenterPointY = 0.5
private var mWidth = 0
private var mHeight = 0
init {
/**
* This will invoke internal `getOrCreateReactViewBackground` method
* to initialize ReactViewBackgroundDrawable for this view,
* if ReactViewBackgroundDrawable, then view will not draw the gradient
*/
setBorderRadius(0f)
}
fun setColors(colors: ReadableArray) {
mColors.clear()
for (i in 0 until colors.size()) {
if (colors.getType(i) == ReadableType.Map) {
mColors.add(i, ColorPropConverter.getColor(colors.getMap(i), reactContext))
} else {
mColors.add(i, colors.getInt(i))
}
}
prepareGradient()
}
fun setLocations(locations: ReadableArray) {
mLocations.clear()
for (i in 0 until locations.size()) {
mLocations.add(i, locations.getDouble(i).toFloat())
}
prepareGradient()
}
fun setCenterPoint(centerPoint: ReadableMap) {
mCenterPointX = if (centerPoint.hasKey("x") && !centerPoint.isNull("x")) {
centerPoint.getDouble("x")
} else {
0.5
}
mCenterPointY = if (centerPoint.hasKey("y") && !centerPoint.isNull("y")) {
centerPoint.getDouble("y")
} else {
0.5
}
prepareGradient()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = w
mHeight = h
preparePath()
prepareGradient()
}
override fun dispatchDraw(canvas: Canvas?) {
canvas?.drawPath(mPath, mPaint)
super.dispatchDraw(canvas)
}
private fun preparePath() {
mPath.reset()
mRect.set(
0f,
0f,
mWidth.toFloat(),
mHeight.toFloat()
)
mPath.addRect(mRect, Path.Direction.CW)
}
private fun prepareGradient() {
if (mColors.size != mLocations.size) {
return
}
mPaint.shader = SweepGradient(
(mCenterPointX * mWidth).toFloat(),
(mCenterPointY * mHeight).toFloat(),
mColors.toIntArray(),
mLocations.toFloatArray()
)
invalidate()
}
companion object {
const val NAME = "ConicGradientView"
}
}
Displaying the gradient is just a bit more difficult than on iOS - first we need to grab the width and height of the view,
we can do it thanks to View#onSizeChanged
method.
After that we create helper private methods (preparePath
, prepareGradient
) that prepare the path and shader for our gradient background.
Whenever each "input" value changes we are recomputing the gradient and invalidating the view (telling it that it needs to be redrawn).
The actual gradient drawing takes place in View#dispatchDraw
method.
It provides canvas argument that we can use to draw the background.
You may have noticed, that we also have the setBorderRadius
invoked in init
function.
It's a workaround for ReactViewGroup
based classes. Without it, the gradient wouldn't be displayed at all, if you wouldn't set some color/border props on the JS side.
ConicGradientViewManager.kt
Paste the following code in android/src/newarch/java/com/conicgradientpackage/ConicGradientViewManager.kt
package com.conicgradientpackage
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.ConicGradientViewManagerDelegate
import com.facebook.react.viewmanagers.ConicGradientViewManagerInterface
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
@ReactModule(name = ConicGradientView.NAME)
class ConicGradientViewManager : ReactViewManager(), ConicGradientViewManagerInterface<ReactViewGroup> {
private var mDelegate = ConicGradientViewManagerDelegate(this)
override fun getName() = ConicGradientView.NAME
override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = mDelegate
override fun createViewInstance(reactContext: ThemedReactContext): ConicGradientView {
return ConicGradientView(reactContext)
}
@ReactProp(name = "colors")
override fun setColors(view: ReactViewGroup, colors: ReadableArray?) {
check(view is ConicGradientView)
if (colors == null) {
return
}
view.setColors(colors)
}
@ReactProp(name = "locations")
override fun setLocations(view: ReactViewGroup, locations: ReadableArray?) {
check(view is ConicGradientView)
if (locations == null) {
return
}
view.setLocations(locations)
}
@ReactProp(name = "centerPoint")
override fun setCenterPoint(view: ReactViewGroup, centerPoint: ReadableMap?) {
check(view is ConicGradientView)
if (centerPoint == null) {
return
}
view.setCenterPoint(centerPoint)
}
}
Let's see what's happening here:
- we declare the view manager class that extends
ReactViewManager
and implement code-generated spec interface - we override required methods:
getName
,getDelegate
&createViewInstance
- we override props setters based on JS spec
Old architecture view manager
android/src/oldarch/java/com/conicgradientpackage/ConicGradientViewManager.kt
at other text editor and paste following content:package com.conicgradientpackage
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.view.ReactViewManager
@ReactModule(name = ConicGradientView.NAME)
class ConicGradientViewManager : ReactViewManager() {
override fun getName() = ConicGradientView.NAME
override fun createViewInstance(reactContext: ThemedReactContext): ConicGradientView {
return ConicGradientView(reactContext)
}
@ReactProp(name = "colors")
fun setColors(view: ConicGradientView, colors: ReadableArray?) {
if (colors == null) {
return
}
view.setColors(colors)
}
@ReactProp(name = "locations")
fun setLocations(view: ConicGradientView, locations: ReadableArray?) {
if (locations == null) {
return
}
view.setLocations(locations)
}
@ReactProp(name = "centerPoint")
fun setCenterPoint(view: ConicGradientView, centerPoint: ReadableMap?) {
if (centerPoint == null) {
return
}
view.setCenterPoint(centerPoint)
}
}
Let's finalize it by exporting the view manager in the TurboReactPackage
instance.
ConicGradientTurboPackage.kt
The last thing we need to do is to export ConicGradientViewManager
in the TurboReactPackage
instance. Let's go to ConicGradientTurboPackage.kt
and add our new module.
package com.conicgradientpackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.facebook.react.uimanager.ViewManager
class ConicGradientTurboPackage : TurboReactPackage() {
/**
* Initialize and export modules based on the name of the required module
*/
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return null
}
/**
* Declare info about exported modules
*/
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
/**
* Here declare the array of exported modules
*/
val moduleList: Array<Class<out NativeModule?>> = arrayOf(
)
val reactModuleInfoMap: MutableMap<String, ReactModuleInfo> = HashMap()
/**
* And here just iterate on that array and produce the info provider instance
*/
for (moduleClass in moduleList) {
val reactModule = moduleClass.getAnnotation(ReactModule::class.java) ?: continue
reactModuleInfoMap[reactModule.name] =
ReactModuleInfo(
reactModule.name,
moduleClass.name,
true,
reactModule.needsEagerInit,
reactModule.hasConstants,
reactModule.isCxxModule,
TurboModule::class.java.isAssignableFrom(moduleClass)
)
}
return ReactModuleInfoProvider { reactModuleInfoMap }
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
/**
* Here declare the list of exported native components
*/
return listOf(ConicGradientViewManager())
}
}
Here the most important bit is createViewManagers
method, which returns collection of view manager classes. Because our package exports only a single view, we register one-element list, with ConicGradientViewManager
class.
ConicGradientView.java
Let's start by declaring the custom view that will extend ReactViewGroup
(the android class that backs <View />
implementation):
package com.conicgradientpackage;
import android.graphics.*;
import com.facebook.react.bridge.ColorPropConverter;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.views.view.ReactViewGroup;
public class ConicGradientView extends ReactViewGroup {
private final ReactContext reactContext;
private int[] mColors = {Color.TRANSPARENT, Color.TRANSPARENT};
private float[] mLocations = {0f, 0f};
private Double mCenterPointX = 0.5;
private Double mCenterPointY = 0.5;
public static final String NAME = "ConicGradientView";
public ConicGradientView(ReactContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
public void setColors(ReadableArray colors) {
mColors = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
if (colors.getType(i) == ReadableType.Map) {
mColors[i] = ColorPropConverter.getColor(colors.getMap(i), reactContext);
} else {
mColors[i] = colors.getInt(i);
}
}
}
public void setLocations(ReadableArray locations) {
mLocations = new float[locations.size()];
for (int i = 0; i < locations.size(); i++) {
mLocations[i] = (float)locations.getDouble(i);
}
}
public void setCenterPoint(ReadableMap centerPoint) {
mCenterPointX = 0.5;
mCenterPointY = 0.5;
if (centerPoint.hasKey("x") && !centerPoint.isNull("x")) {
mCenterPointX = centerPoint.getDouble("x");
}
if (centerPoint.hasKey("y") && !centerPoint.isNull("y")) {
mCenterPointY = centerPoint.getDouble("y");
}
}
}
In setColors
, setLocations
, setCenterPoint
we save and parse the values received from props.
And we create static NAME
with the name based on JS specification.
Looks good, but it doesn't do anything with gradient rendering - let's add it:
class ConicGradientView(private val reactContext: ReactContext) : ReactViewGroup(reactContext) {
private final ReactContext reactContext;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path mPath = new Path();
private final RectF mRect = new RectF();
private int[] mColors = {Color.TRANSPARENT, Color.TRANSPARENT};
private float[] mLocations = {0f, 0f};
private Double mCenterPointX = 0.5;
private Double mCenterPointY = 0.5;
private int mWidth = 0;
private int mHeight = 0;
public static final String NAME = "ConicGradientView";
public ConicGradientView(ReactContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
/**
* This will invoke internal `getOrCreateReactViewBackground` method
* to initialize ReactViewBackgroundDrawable for this view,
* if ReactViewBackgroundDrawable, then view will not draw the gradient
*/
this.setBorderRadius(0f);
}
public void setColors(ReadableArray colors) {
mColors = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
if (colors.getType(i) == ReadableType.Map) {
mColors[i] = ColorPropConverter.getColor(colors.getMap(i), reactContext);
} else {
mColors[i] = colors.getInt(i);
}
}
prepareGradient();
}
public void setLocations(ReadableArray locations) {
mLocations = new float[locations.size()];
for (int i = 0; i < locations.size(); i++) {
mLocations[i] = (float)locations.getDouble(i);
}
prepareGradient();
}
public void setCenterPoint(ReadableMap centerPoint) {
mCenterPointX = 0.5;
mCenterPointY = 0.5;
if (centerPoint.hasKey("x") && !centerPoint.isNull("x")) {
mCenterPointX = centerPoint.getDouble("x");
}
if (centerPoint.hasKey("y") && !centerPoint.isNull("y")) {
mCenterPointY = centerPoint.getDouble("y");
}
prepareGradient();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
preparePath();
prepareGradient();
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (canvas != null) {
canvas.drawPath(mPath, mPaint);
}
super.dispatchDraw(canvas);
}
private void preparePath() {
mPath.reset();
mRect.set(
0f,
0f,
(float) mWidth,
(float) mHeight
);
mPath.addRect(mRect, Path.Direction.CW);
}
private void prepareGradient() {
if (mColors.length != mLocations.length) {
return;
}
mPaint.setShader(new SweepGradient(
(float) (mCenterPointX * mWidth),
(float) (mCenterPointY * mHeight),
mColors,
mLocations
));
invalidate();
}
}
Displaying the gradient is just a bit more difficult than on iOS - first we need to grab the width and height of the view,
we can do it thanks to View#onSizeChanged
method.
After that we create helper private methods (preparePath
, prepareGradient
) that prepare the path and shader for our gradient background.
Whenever each "input" value changes we are recomputing the gradient and invalidating the view (telling it that it needs to be redrawn).
The actual gradient drawing takes place in View#dispatchDraw
method.
It provides canvas argument that we can use to draw the background.
You may have noticed, that we also have the setBorderRadius
invoked in constructor.
It's a workaround for ReactViewGroup
based classes. Without it, the gradient wouldn't be displayed at all, if you wouldn't set some color/border props on the JS side.
ConicGradientViewManager.java
Paste the following code in android/src/newarch/java/com/conicgradientpackage/ConicGradientViewManager.java
package com.conicgradientpackage;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.ConicGradientViewManagerDelegate;
import com.facebook.react.viewmanagers.ConicGradientViewManagerInterface;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.views.view.ReactViewManager;
@ReactModule(name = ConicGradientView.NAME)
public class ConicGradientViewManager extends ReactViewManager implements ConicGradientViewManagerInterface<ReactViewGroup> {
private final ConicGradientViewManagerDelegate mDelegate = new ConicGradientViewManagerDelegate(this);
@Override
public String getName() {
return ConicGradientView.NAME;
}
@Override
protected ViewManagerDelegate<ReactViewGroup> getDelegate() {
return mDelegate;
}
@Override
public ConicGradientView createViewInstance(ThemedReactContext reactContext) {
return new ConicGradientView(reactContext);
}
@Override
@ReactProp(name = "colors")
public void setColors(ReactViewGroup view, @Nullable ReadableArray colors) {
if (!(view instanceof ConicGradientView)) {
throw new IllegalStateException("Check failed.");
}
ConicGradientView typedView = (ConicGradientView)view;
if (colors == null) {
return;
}
typedView.setColors(colors);
}
@Override
@ReactProp(name = "locations")
public void setLocations(ReactViewGroup view, @Nullable ReadableArray locations) {
if (!(view instanceof ConicGradientView)) {
throw new IllegalStateException("Check failed.");
}
ConicGradientView typedView = (ConicGradientView)view;
if (locations == null) {
return;
}
typedView.setLocations(locations);
}
@Override
@ReactProp(name = "centerPoint")
public void setCenterPoint(ReactViewGroup view, @Nullable ReadableMap centerPoint) {
if (!(view instanceof ConicGradientView)) {
throw new IllegalStateException("Check failed.");
}
ConicGradientView typedView = (ConicGradientView)view;
if (centerPoint == null) {
return;
}
typedView.setCenterPoint(centerPoint);
}
}
Let's see what's happening here:
- we declare the view manager class that extends
ReactViewManager
and implement code-generated spec interface - we override required methods:
getName
,getDelegate
&createViewInstance
- we override props setters based on JS spec
Old architecture view manager
android/src/oldarch/java/com/conicgradientpackage/ConicGradientViewManager.java
at other text editor and paste following content:package com.conicgradientpackage;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.view.ReactViewManager;
@ReactModule(name = ConicGradientView.NAME)
public class ConicGradientViewManager extends ReactViewManager {
@Override
public String getName() {
return ConicGradientView.NAME;
}
@Override
public ConicGradientView createViewInstance(ThemedReactContext reactContext) {
return new ConicGradientView(reactContext);
}
@ReactProp(name = "colors")
public void setColors(ConicGradientView view, @Nullable ReadableArray colors) {
if (colors == null) {
return;
}
view.setColors(colors);
}
@ReactProp(name = "locations")
public void setLocations(ConicGradientView view, @Nullable ReadableArray locations) {
if (locations == null) {
return;
}
view.setLocations(locations);
}
@ReactProp(name = "centerPoint")
public void setCenterPoint(ConicGradientView view, @Nullable ReadableMap centerPoint) {
if (centerPoint == null) {
return;
}
view.setCenterPoint(centerPoint);
}
}
Let's finalize it by exporting the view manager in the TurboReactPackage
instance.
ConicGradientTurboPackage.java
The last thing we need to do is to export ConicGradientViewManager
in the TurboReactPackage
instance. Let's go to ConicGradientTurboPackage.java
and add our new module.
package com.conicgradientpackage;
import androidx.annotation.Nullable;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConicGradientTurboPackage extends TurboReactPackage {
/**
* Initialize and export modules based on the name of the required module
*/
@Override
@Nullable
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
return null;
}
/**
* Declare info about exported modules
*/
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
/**
* Here declare the array of exported modules
*/
Class<? extends NativeModule>[] moduleList = new Class[] {
};
final Map<String, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
/**
* And here just iterate on that array and produce the info provider instance
*/
for (Class<? extends NativeModule> moduleClass : moduleList) {
ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class);
reactModuleInfoMap.put(
reactModule.name(),
new ReactModuleInfo(
reactModule.name(),
moduleClass.getName(),
true,
reactModule.needsEagerInit(),
reactModule.hasConstants(),
reactModule.isCxxModule(),
TurboModule.class.isAssignableFrom(moduleClass)
)
);
}
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
return reactModuleInfoMap;
}
};
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
/**
* Here declare the list of exported native components
*/
return Arrays.<ViewManager>asList(new ConicGradientViewManager());
}
}
Here the most important bit is createViewManagers
method, which returns collection of view manager classes. Because our package exports only a single view, we register one-element list, with ConicGradientViewManager
class.
You can check training repo for Kotlin implementation here and Java implementation here.
That's Android part, now let's wrap things up and try to use Conic gradient in action!