Skip to main content

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 files
  • ConicGradientView class stub file
  • ConicGradientTurboPackage class stub file

Let's begin!

ConicGradientView.kt

Let's start by declaring the custom view that will extend ReactViewGroup (the android class that backs <View /> implementation):

android/src/newarch/java/com/conicgradientpackage/ConicGradientView.kt
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:

android/src/main/java/com/conicgradientpackage/ConicGradientView.kt
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

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
The implementation of old architecture view manager won't be visible in Android Studio when you have new architecture enabled. To handle that, you can open 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.

android/src/main/java/com/conicgradientpackage/ConicGradientTurboPackage.kt
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.

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!