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 range-slider-package inside project-tree

The range-slider-package contains 3 packages with the same name com.rangesliderpackage. After expanding them, you'll notice that these contain following things:

  • code-generated Java spec files
  • RangeSliderViewManager view manager class stub files
  • RangeSliderView class stub file
  • RangeSliderTurboPackage class stub file

Let's start implementing!

Add native library as dependency for the package

When developing some Android native code, you often need to use some external package, whether it's from Jetpack, MaterialComponents or some 3rd party. Usually those libraries are integrated with Gradle.

tip

For more information on how to add dependencies to Android project visit Android's dedicated docs.

Each RN library that includes some Android native code is, in fact, integrated with Gradle and our range slider will also need to depend on MaterialComponents dependency. So let's not waste time and navigate to the build.gradle in our package.

android/build.gradle
buildscript {
// ...
}

// ...

android {
// ...
}

repositories {
// ...
}

dependencies {
// ...

// Add the dependency to the MaterialComponents library
implementation "com.google.android.material:material:1.8.0"
}

To add a dependency we need to write implementation keyword and declare the package (and its version) we want to include. After that you can invoke Gradle Sync in the Android Studio.

info

In this guide, version 1.8.0 of MaterialComponents library is used. In your case this version may be different, you can visit Maven Repository and check available versions.

Change Android theme to Material3

To use MaterialComponents library, we need to change the Android app's theme to the Material theme. To do that, let's navigate to our tutorial app and go to styles.xml in Android resources directory:

android/app/src/main/res/values/styles.xml
- <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
+ <style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">

Events

Let's start by handling direct events - go to OnRangeSliderViewBeginDragEvent.kt, OnRangeSliderViewEndDragEvent.kt & OnRangeSliderViewValueChangeEvent.kt

android/src/main/java/com/rangesliderpackage/OnRangeSliderViewBeginDragEvent.kt
package com.rangesliderpackage

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class OnRangeSliderViewBeginDragEvent(
surfaceId: Int,
viewId: Int
) : Event<OnRangeSliderViewBeginDragEvent>(surfaceId, viewId) {
override fun getEventName() = NAME

override fun getEventData(): WritableMap? {
return Arguments.createMap()
}

companion object {
const val NAME = "topRangeSliderViewBeginDrag"
const val EVENT_PROP_NAME = "onRangeSliderViewBeginDrag"
}
}
android/src/main/java/com/rangesliderpackage/OnRangeSliderViewEndDragEvent.kt
package com.rangesliderpackage

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class OnRangeSliderViewEndDragEvent(
surfaceId: Int,
viewId: Int,
private val leftKnobValue: Double,
private val rightKnobValue: Double
) : Event<OnRangeSliderViewEndDragEvent>(surfaceId, viewId) {
override fun getEventName() = NAME

override fun getEventData(): WritableMap? {
return createPayload()
}

private fun createPayload() = Arguments.createMap().apply {
putDouble(LEFT_KNOB_KEY, leftKnobValue)
putDouble(RIGHT_KNOB_KEY, rightKnobValue)
}

companion object {
private const val LEFT_KNOB_KEY = "leftKnobValue"
private const val RIGHT_KNOB_KEY = "rightKnobValue"
const val NAME = "topRangeSliderViewEndDrag"
const val EVENT_PROP_NAME = "onRangeSliderViewEndDrag"
}
}
android/src/main/java/com/rangesliderpackage/OnRangeSliderViewValueChangeEvent.kt
package com.rangesliderpackage

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class OnRangeSliderViewValueChangeEvent(
surfaceId: Int,
viewId: Int,
private val leftKnobValue: Double,
private val rightKnobValue: Double
) : Event<OnRangeSliderViewValueChangeEvent>(surfaceId, viewId) {
override fun getEventName() = NAME

override fun getEventData(): WritableMap? {
return createPayload()
}

private fun createPayload() = Arguments.createMap().apply {
putDouble(LEFT_KNOB_KEY, leftKnobValue)
putDouble(RIGHT_KNOB_KEY, rightKnobValue)
}

companion object {
private const val LEFT_KNOB_KEY = "leftKnobValue"
private const val RIGHT_KNOB_KEY = "rightKnobValue"
const val NAME = "topRangeSliderViewValueChange"
const val EVENT_PROP_NAME = "onRangeSliderViewValueChange"
}
}

In each case we are creating event class that extends RN's Event. Those classes take at least 2 arguments - surfaceId and viewId. To construct the payload object, we use Arguments.createMap utility helper. And we also define static constants that will be used to register events with specified JS name.

RangeSliderView.kt

Now, let's declare the custom view that will hold our range slider:

android/src/main/java/com/rangesliderpackage/RangeSliderView.kt
package com.rangesliderpackage

import android.content.res.ColorStateList
import android.graphics.Color
import android.widget.FrameLayout
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.PixelUtil
import com.google.android.material.slider.RangeSlider
import kotlin.math.abs

class RangeSliderView(private val reactContext: ReactContext) : FrameLayout(reactContext) {
private var mLastLeftKnobValue = 0f
private var mLastRightKnobValue = 1f
private var slider = RangeSlider(reactContext).apply {
trackHeight = PixelUtil.toPixelFromDIP(10f).toInt()
thumbTintList = ColorStateList.valueOf(Color.BLUE)
addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: RangeSlider) {
//
}

override fun onStopTrackingTouch(slider: RangeSlider) {
//
}
})
addOnChangeListener { slider, _, _ ->
val newLeftKnobValue = slider.values[0]
val newRightKnobValue = slider.values[1]
if (abs(newLeftKnobValue - mLastLeftKnobValue) < 0.1f && abs(newRightKnobValue - mLastRightKnobValue) < 0.1f) {
return@addOnChangeListener
}
mLastLeftKnobValue = newLeftKnobValue
mLastRightKnobValue = newRightKnobValue

// ...
}
}

init {
this.addView(slider, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}

companion object {
const val NAME = "RangeSliderView"
}
}

Our custom view extends FrameLayout class, it declares static NAME constant, that's value matches the one from JS specification, it also declares RangeSlider instance with some default configuration.

As a next step, let's forward props to the slider:

android/src/main/java/com/rangesliderpackage/RangeSliderView.kt
// ...

class RangeSliderView(private val reactContext: ReactContext) : FrameLayout(reactContext) {
// ...

init {
this.addView(slider, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}

fun setActiveColor(activeColor: Int?) {
slider.trackActiveTintList = ColorStateList.valueOf(activeColor ?: Color.BLUE)
}

fun setInactiveColor(inactiveColor: Int?) {
slider.trackInactiveTintList = ColorStateList.valueOf(inactiveColor ?: Color.GRAY)
}

fun setMinValue(minValue: Double) {
slider.valueFrom = minValue.toFloat()
}

fun setMaxValue(maxValue: Double) {
slider.valueTo = maxValue.toFloat()
}

fun setLeftKnobValue(leftKnobValue: Double) {
if (leftKnobValue.isNaN()) {
return
}
if (slider.values.count() < 2) {
slider.values = listOf(leftKnobValue.toFloat(), leftKnobValue.toFloat() + 1)
return
}
val rightKnobValue = slider.values[1]
slider.values = listOf(leftKnobValue.toFloat(), rightKnobValue)
}

fun setRightKnobValue(rightKnobValue: Double) {
if (rightKnobValue.isNaN()) {
return
}
if (slider.values.isEmpty()) {
slider.values = listOf(rightKnobValue.toFloat() - 1, rightKnobValue.toFloat())
return
}
val leftKnobValue = slider.values[0]
slider.values = listOf(leftKnobValue, rightKnobValue.toFloat())
}

fun setStep(step: Int) {
slider.stepSize = step.toFloat()
}

// ...
}

Here we are defining public setter function that will be used by view manager class. Inside those functions we parse arguments and pass them to the slider.

Good, we communicate with our slider, but we still have to make the slider communicate back with us! We'll do it by introducing listener property, that view manager class will use to receive events from slider.

Let's start by defining the interface:

android/src/main/java/com/rangesliderpackage/RangeSliderView.kt
class RangeSliderView(private val reactContext: ReactContext) : FrameLayout(reactContext) {
interface OnRangeSliderViewListener {
fun onRangeSliderViewBeginDrag()
fun onRangeSliderViewEndDrag(leftKnobValue: Double, rightKnobValue: Double)
fun onRangeSliderViewValueChange(leftKnobValue: Double, rightKnobValue: Double)
}

// ...
}

Next, we'll add a listener property and use it to send events to the receiver:

android/src/main/java/com/rangesliderpackage/RangeSliderView.kt
class RangeSliderView(private val reactContext: ReactContext) : FrameLayout(reactContext) {
interface OnRangeSliderViewListener {
fun onRangeSliderViewBeginDrag()
fun onRangeSliderViewEndDrag(leftKnobValue: Double, rightKnobValue: Double)
fun onRangeSliderViewValueChange(leftKnobValue: Double, rightKnobValue: Double)
}

private var mListener: OnRangeSliderViewListener? = null
private var mLastLeftKnobValue = 0f
private var mLastRightKnobValue = 1f
private var slider = RangeSlider(reactContext).apply {
trackHeight = PixelUtil.toPixelFromDIP(10f).toInt()
thumbTintList = ColorStateList.valueOf(Color.BLUE)
addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: RangeSlider) {
sendOnRangeSliderViewBeginDragEvent()
}

override fun onStopTrackingTouch(slider: RangeSlider) {
sendOnRangeSliderViewEndDragEvent(slider.values[0].toDouble(), slider.values[1].toDouble())
}
})
addOnChangeListener { slider, _, _ ->
val newLeftKnobValue = slider.values[0]
val newRightKnobValue = slider.values[1]
if (abs(newLeftKnobValue - mLastLeftKnobValue) < 0.1f && abs(newRightKnobValue - mLastRightKnobValue) < 0.1f) {
return@addOnChangeListener
}
mLastLeftKnobValue = newLeftKnobValue
mLastRightKnobValue = newRightKnobValue
sendOnRangeSliderViewValueChangeEvent(newLeftKnobValue.toDouble(), newRightKnobValue.toDouble())
}
}

init {
this.addView(slider, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}

fun setOnRangeSliderViewListener(listener: OnRangeSliderViewListener?) {
mListener = listener
}

// ...

private fun sendOnRangeSliderViewValueChangeEvent(leftKnobValue: Double, rightKnobValue: Double) {
mListener?.onRangeSliderViewValueChange(leftKnobValue, rightKnobValue)
}

private fun sendOnRangeSliderViewBeginDragEvent() {
mListener?.onRangeSliderViewBeginDrag()
}

private fun sendOnRangeSliderViewEndDragEvent(leftKnobValue: Double, rightKnobValue: Double) {
mListener?.onRangeSliderViewEndDrag(leftKnobValue, rightKnobValue)
}

// ...
}

Cool! Now we have everything in place, let's use it in view manager class.

Complete RangeSliderView.kt file
package com.rangesliderpackage

import android.content.res.ColorStateList
import android.graphics.Color
import android.widget.FrameLayout
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.PixelUtil
import com.google.android.material.slider.RangeSlider
import kotlin.math.abs

class RangeSliderView(private val reactContext: ReactContext) : FrameLayout(reactContext) {
interface OnRangeSliderViewListener {
fun onRangeSliderViewBeginDrag()
fun onRangeSliderViewEndDrag(leftKnobValue: Double, rightKnobValue: Double)
fun onRangeSliderViewValueChange(leftKnobValue: Double, rightKnobValue: Double)
}

private var mListener: OnRangeSliderViewListener? = null
private var mLastLeftKnobValue = 0f
private var mLastRightKnobValue = 1f
private var slider = RangeSlider(reactContext).apply {
trackHeight = PixelUtil.toPixelFromDIP(10f).toInt()
thumbTintList = ColorStateList.valueOf(Color.BLUE)
addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: RangeSlider) {
sendOnRangeSliderViewBeginDragEvent()
}

override fun onStopTrackingTouch(slider: RangeSlider) {
sendOnRangeSliderViewEndDragEvent(slider.values[0].toDouble(), slider.values[1].toDouble())
}
})
addOnChangeListener { slider, _, _ ->
val newLeftKnobValue = slider.values[0]
val newRightKnobValue = slider.values[1]
if (abs(newLeftKnobValue - mLastLeftKnobValue) < 0.1f && abs(newRightKnobValue - mLastRightKnobValue) < 0.1f) {
return@addOnChangeListener
}
mLastLeftKnobValue = newLeftKnobValue
mLastRightKnobValue = newRightKnobValue
sendOnRangeSliderViewValueChangeEvent(newLeftKnobValue.toDouble(), newRightKnobValue.toDouble())
}
}

init {
this.addView(slider, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}

fun setOnRangeSliderViewListener(listener: OnRangeSliderViewListener?) {
mListener = listener
}

fun setActiveColor(activeColor: Int?) {
slider.trackActiveTintList = ColorStateList.valueOf(activeColor ?: Color.BLUE)
}

fun setInactiveColor(inactiveColor: Int?) {
slider.trackInactiveTintList = ColorStateList.valueOf(inactiveColor ?: Color.GRAY)
}

fun setMinValue(minValue: Double) {
slider.valueFrom = minValue.toFloat()
}

fun setMaxValue(maxValue: Double) {
slider.valueTo = maxValue.toFloat()
}

fun setLeftKnobValue(leftKnobValue: Double) {
if (leftKnobValue.isNaN()) {
return
}
if (slider.values.count() < 2) {
slider.values = listOf(leftKnobValue.toFloat(), leftKnobValue.toFloat() + 1)
return
}
val rightKnobValue = slider.values[1]
slider.values = listOf(leftKnobValue.toFloat(), rightKnobValue)
}

fun setRightKnobValue(rightKnobValue: Double) {
if (rightKnobValue.isNaN()) {
return
}
if (slider.values.isEmpty()) {
slider.values = listOf(rightKnobValue.toFloat() - 1, rightKnobValue.toFloat())
return
}
val leftKnobValue = slider.values[0]
slider.values = listOf(leftKnobValue, rightKnobValue.toFloat())
}

fun setStep(step: Int) {
slider.stepSize = step.toFloat()
}

private fun sendOnRangeSliderViewValueChangeEvent(leftKnobValue: Double, rightKnobValue: Double) {
mListener?.onRangeSliderViewValueChange(leftKnobValue, rightKnobValue)
}

private fun sendOnRangeSliderViewBeginDragEvent() {
mListener?.onRangeSliderViewBeginDrag()
}

private fun sendOnRangeSliderViewEndDragEvent(leftKnobValue: Double, rightKnobValue: Double) {
mListener?.onRangeSliderViewEndDrag(leftKnobValue, rightKnobValue)
}

companion object {
const val NAME = "RangeSliderView"
}
}

RangeSliderViewManager.kt

The view manager class will connect the slider with our RN app - let's start by creating the boilerplate:

android/src/newarch/java/com/rangesliderpackage/RangeSliderViewManager.kt
package com.rangesliderpackage

import android.view.View
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RangeSliderViewManagerDelegate
import com.facebook.react.viewmanagers.RangeSliderViewManagerInterface

@ReactModule(name = RangeSliderView.NAME)
class RangeSliderViewManager : ViewGroupManager<RangeSliderView>(), RangeSliderViewManagerInterface<RangeSliderView> {
private val mDelegate = RangeSliderViewManagerDelegate(this)

override fun getName() = RangeSliderView.NAME

override fun getDelegate(): ViewManagerDelegate<RangeSliderView> = mDelegate

override fun receiveCommand(root: RangeSliderView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): RangeSliderView {
return RangeSliderView(reactContext)
}

@ReactProp(name = "activeColor", customType = "Color")
override fun setActiveColor(view: RangeSliderView, activeColor: Int?) {
view.setActiveColor(activeColor)
}

@ReactProp(name = "inactiveColor", customType = "Color")
override fun setInactiveColor(view: RangeSliderView, inactiveColor: Int?) {
view.setInactiveColor(inactiveColor)
}

@ReactProp(name = "minValue")
override fun setMinValue(view: RangeSliderView, value: Double) {
view.setMinValue(value)
}

@ReactProp(name = "maxValue")
override fun setMaxValue(view: RangeSliderView, value: Double) {
view.setMaxValue(value)
}

@ReactProp(name = "leftKnobValue")
override fun setLeftKnobValue(view: RangeSliderView, value: Double) {
view.setLeftKnobValue(value)
}

@ReactProp(name = "rightKnobValue")
override fun setRightKnobValue(view: RangeSliderView, value: Double) {
view.setRightKnobValue(value)
}

@ReactProp(name = "step")
override fun setStep(view: RangeSliderView, step: Int) {
view.setStep(step)
}

override fun setLeftKnobValueProgrammatically(view: RangeSliderView?, value: Double) {
view?.setLeftKnobValue(value)
}

override fun setRightKnobValueProgrammatically(view: RangeSliderView?, value: Double) {
view?.setRightKnobValue(value)
}

override fun addView(parent: RangeSliderView, child: View?, index: Int) {
// That component does not accept child views
}

override fun addViews(parent: RangeSliderView, views: MutableList<View>?) {
// That component does not accept child views
}

override fun removeAllViews(parent: RangeSliderView) {
// That component does not accept child views
}

override fun removeView(parent: RangeSliderView, view: View?) {
// That component does not accept child views
}

override fun removeViewAt(parent: RangeSliderView, index: Int) {
// That component does not accept child views
}
}

So we are doing a bunch of things in the view manager class:

  • we create codegenerated delegate and return it from getDelegate method
  • we use custom view's NAME constant in getName (this needs to match the name from JS specification)
  • we use delegate to handle native commands in receiveCommand method
  • we initialize instance of our custom view in createViewInstance method
  • we handle all props and native commands

You may have noticed, that view manager class also overrides add/remove view methods. Those methods can be used to control how the child views should be added/removed in the view managed by the view manager. In our case, we prevent adding/removal to be sure that our slider view does not have any child views.

info

Usually when the android view does not handle any children, you will use SimpleViewManager class instead of ViewGroupManager - here the latter is used, just to showcase that add/remove view methods exist and can be overriden

We are in the half way, now it's time to handle event emitting based on the values received from the slider. Let's add the following snippet:

android/src/newarch/java/com/rangesliderpackage/RangeSliderViewManager.kt
package com.rangesliderpackage

import android.view.View
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RangeSliderViewManagerDelegate
import com.facebook.react.viewmanagers.RangeSliderViewManagerInterface

@ReactModule(name = RangeSliderView.NAME)
class RangeSliderViewManager : ViewGroupManager<RangeSliderView>(), RangeSliderViewManagerInterface<RangeSliderView> {
// ...

override fun addEventEmitters(reactContext: ThemedReactContext, view: RangeSliderView) {
super.addEventEmitters(reactContext, view)
view.setOnRangeSliderViewListener(object : RangeSliderView.OnRangeSliderViewListener {
override fun onRangeSliderViewValueChange(
leftKnobValue: Double,
rightKnobValue: Double
) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewValueChangeEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id,
leftKnobValue,
rightKnobValue
)
)
}

override fun onRangeSliderViewBeginDrag() {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewBeginDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id
)
)
}

override fun onRangeSliderViewEndDrag(leftKnobValue: Double, rightKnobValue: Double) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewEndDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id,
leftKnobValue,
rightKnobValue
)
)
}
})
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.of(
OnRangeSliderViewValueChangeEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewValueChangeEvent.EVENT_PROP_NAME),
OnRangeSliderViewBeginDragEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewBeginDragEvent.EVENT_PROP_NAME),
OnRangeSliderViewEndDragEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewEndDragEvent.EVENT_PROP_NAME)
)
}
}

To handle the events, we can override addEventEmitters method on the view manager class. In that method, we can register listener where we'll dispatch events based on received values. Dispatching events is available thanks to UIManagerHelper.getEventDispatcherForReactTag method - it needs context and the id (react tag) of the view. And each dispatched event gets at least 2 arguments - surfaceId (obtained with UIManagerHelper.getSurfaceId) and id of the view.

However, to dispatch events we need to also register their names, so that we can consume them in the JS code. This is done in getExportedCustomDirectEventTypeConstants method that we can override on the view manager class.

Complete RangeSliderViewManager.kt file
package com.rangesliderpackage

import android.view.View
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RangeSliderViewManagerDelegate
import com.facebook.react.viewmanagers.RangeSliderViewManagerInterface

@ReactModule(name = RangeSliderView.NAME)
class RangeSliderViewManager : ViewGroupManager<RangeSliderView>(), RangeSliderViewManagerInterface<RangeSliderView> {
private val mDelegate = RangeSliderViewManagerDelegate(this)

override fun getName() = RangeSliderView.NAME

override fun getDelegate(): ViewManagerDelegate<RangeSliderView> = mDelegate

override fun receiveCommand(root: RangeSliderView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): RangeSliderView {
return RangeSliderView(reactContext)
}

@ReactProp(name = "activeColor", customType = "Color")
override fun setActiveColor(view: RangeSliderView, activeColor: Int?) {
view.setActiveColor(activeColor)
}

@ReactProp(name = "inactiveColor", customType = "Color")
override fun setInactiveColor(view: RangeSliderView, inactiveColor: Int?) {
view.setInactiveColor(inactiveColor)
}

@ReactProp(name = "minValue")
override fun setMinValue(view: RangeSliderView, value: Double) {
view.setMinValue(value)
}

@ReactProp(name = "maxValue")
override fun setMaxValue(view: RangeSliderView, value: Double) {
view.setMaxValue(value)
}

@ReactProp(name = "leftKnobValue")
override fun setLeftKnobValue(view: RangeSliderView, value: Double) {
view.setLeftKnobValue(value)
}

@ReactProp(name = "rightKnobValue")
override fun setRightKnobValue(view: RangeSliderView, value: Double) {
view.setRightKnobValue(value)
}

@ReactProp(name = "step")
override fun setStep(view: RangeSliderView, step: Int) {
view.setStep(step)
}

override fun setLeftKnobValueProgrammatically(view: RangeSliderView?, value: Double) {
view?.setLeftKnobValue(value)
}

override fun setRightKnobValueProgrammatically(view: RangeSliderView?, value: Double) {
view?.setRightKnobValue(value)
}

override fun addView(parent: RangeSliderView, child: View?, index: Int) {
// That component does not accept child views
}

override fun addViews(parent: RangeSliderView, views: MutableList<View>?) {
// That component does not accept child views
}

override fun removeAllViews(parent: RangeSliderView) {
// That component does not accept child views
}

override fun removeView(parent: RangeSliderView, view: View?) {
// That component does not accept child views
}

override fun removeViewAt(parent: RangeSliderView, index: Int) {
// That component does not accept child views
}

override fun addEventEmitters(reactContext: ThemedReactContext, view: RangeSliderView) {
super.addEventEmitters(reactContext, view)
view.setOnRangeSliderViewListener(object : RangeSliderView.OnRangeSliderViewListener {
override fun onRangeSliderViewValueChange(
leftKnobValue: Double,
rightKnobValue: Double
) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewValueChangeEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id,
leftKnobValue,
rightKnobValue
)
)
}

override fun onRangeSliderViewBeginDrag() {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewBeginDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id
)
)
}

override fun onRangeSliderViewEndDrag(leftKnobValue: Double, rightKnobValue: Double) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewEndDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id,
leftKnobValue,
rightKnobValue
)
)
}
})
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.of(
OnRangeSliderViewValueChangeEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewValueChangeEvent.EVENT_PROP_NAME),
OnRangeSliderViewBeginDragEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewBeginDragEvent.EVENT_PROP_NAME),
OnRangeSliderViewEndDragEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewEndDragEvent.EVENT_PROP_NAME)
)
}
}
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/rangesliderpackage/RangeSliderViewManager.kt at other text editor and paste following content:

package com.rangesliderpackage

import android.view.View
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactProp

@ReactModule(name = RangeSliderView.NAME)
class RangeSliderViewManager : ViewGroupManager<RangeSliderView>() {
override fun getName() = RangeSliderView.NAME

override fun receiveCommand(root: RangeSliderView, commandId: String?, args: ReadableArray?) {
super.receiveCommand(root, commandId, args)

when (commandId) {
"setLeftKnobValueProgrammatically" -> {
val value = args!!.getDouble(0)
setLeftKnobValueProgrammatically(root, value)
}
"setRightKnobValueProgrammatically" -> {
val value = args!!.getDouble(0)
setRightKnobValueProgrammatically(root, value)
}
}
}

override fun createViewInstance(reactContext: ThemedReactContext): RangeSliderView {
return RangeSliderView(reactContext)
}

@ReactProp(name = "activeColor", customType = "Color")
fun setActiveColor(view: RangeSliderView, activeColor: Int?) {
view.setActiveColor(activeColor)
}

@ReactProp(name = "inactiveColor", customType = "Color")
fun setInactiveColor(view: RangeSliderView, inactiveColor: Int?) {
view.setInactiveColor(inactiveColor)
}

@ReactProp(name = "minValue")
fun setMinValue(view: RangeSliderView, value: Double) {
view.setMinValue(value)
}

@ReactProp(name = "maxValue")
fun setMaxValue(view: RangeSliderView, value: Double) {
view.setMaxValue(value)
}

@ReactProp(name = "leftKnobValue")
fun setLeftKnobValue(view: RangeSliderView, value: Double) {
view.setLeftKnobValue(value)
}

@ReactProp(name = "rightKnobValue")
fun setRightKnobValue(view: RangeSliderView, value: Double) {
view.setRightKnobValue(value)
}

@ReactProp(name = "step")
fun setStep(view: RangeSliderView, step: Int) {
view.setStep(step)
}

private fun setLeftKnobValueProgrammatically(view: RangeSliderView, value: Double) {
view.setLeftKnobValue(value)
}

private fun setRightKnobValueProgrammatically(view: RangeSliderView, value: Double) {
view.setRightKnobValue(value)
}

override fun addView(parent: RangeSliderView, child: View?, index: Int) {
// That component does not accept child views
}

override fun addViews(parent: RangeSliderView, views: MutableList<View>?) {
// That component does not accept child views
}

override fun removeAllViews(parent: RangeSliderView) {
// That component does not accept child views
}

override fun removeView(parent: RangeSliderView, view: View?) {
// That component does not accept child views
}

override fun removeViewAt(parent: RangeSliderView, index: Int) {
// That component does not accept child views
}

override fun addEventEmitters(reactContext: ThemedReactContext, view: RangeSliderView) {
super.addEventEmitters(reactContext, view)
view.setOnRangeSliderViewListener(object : RangeSliderView.OnRangeSliderViewListener {
override fun onRangeSliderViewValueChange(
leftKnobValue: Double,
rightKnobValue: Double
) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewValueChangeEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id,
leftKnobValue,
rightKnobValue
)
)
}

override fun onRangeSliderViewBeginDrag() {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewBeginDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id
)
)
}

override fun onRangeSliderViewEndDrag(leftKnobValue: Double, rightKnobValue: Double) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
?.dispatchEvent(
OnRangeSliderViewEndDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.id,
leftKnobValue,
rightKnobValue
)
)
}
})
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.of(
OnRangeSliderViewValueChangeEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewValueChangeEvent.EVENT_PROP_NAME),
OnRangeSliderViewBeginDragEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewBeginDragEvent.EVENT_PROP_NAME),
OnRangeSliderViewEndDragEvent.NAME,
MapBuilder.of("registrationName", OnRangeSliderViewEndDragEvent.EVENT_PROP_NAME)
)
}
}

Let's finalize it by exporting the view manager in the TurboReactPackage instance.

RangeSliderTurboPackage.kt

The last thing we need to do is to export RangeSliderViewManager in the TurboReactPackage instance. Let's go to RangeSliderTurboPackage.kt and add our new module.

android/src/main/java/com/rangesliderpackage/RangeSliderTurboPackage.kt
package com.rangesliderpackage

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 RangeSliderTurboPackage : 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(RangeSliderViewManager())
}
}

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 RangeSliderViewManager 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 see our range slider in action!