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 filesRangeSliderView
class stub fileRangeSliderTurboPackage
class stub file
Let's start implementing!
- Kotlin
- Java
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.
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.
- Groovy script (build.gradle)
- Kotlin script (build.gradle.kts)
buildscript {
// ...
}
// ...
android {
// ...
}
repositories {
// ...
}
dependencies {
// ...
// Add the dependency to the MaterialComponents library
implementation "com.google.android.material:material:1.8.0"
}
buildscript {
// ...
}
// ...
plugins {
// ...
}
// ...
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.
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:
- <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
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"
}
}
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"
}
}
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:
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:
// ...
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:
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:
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:
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 ingetName
(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.
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:
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
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.
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.
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.
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.
- Groovy script (build.gradle)
- Kotlin script (build.gradle.kts)
buildscript {
// ...
}
// ...
android {
// ...
}
repositories {
// ...
}
dependencies {
// ...
// Add the dependency to the MaterialComponents library
implementation "com.google.android.material:material:1.8.0"
}
buildscript {
// ...
}
// ...
plugins {
// ...
}
// ...
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.
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:
- <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.java
, OnRangeSliderViewEndDragEvent.java
& OnRangeSliderViewValueChangeEvent.java
package com.rangesliderpackage;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
public class OnRangeSliderViewBeginDragEvent extends Event<OnRangeSliderViewBeginDragEvent> {
public static final String NAME = "topRangeSliderViewBeginDrag";
public static final String EVENT_PROP_NAME = "onRangeSliderViewBeginDrag";
public OnRangeSliderViewBeginDragEvent(int surfaceId, int viewId) {
super(surfaceId, viewId);
}
@Override
public String getEventName() {
return NAME;
}
@Nullable
@Override
public WritableMap getEventData() {
return Arguments.createMap();
}
}
package com.rangesliderpackage;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
public class OnRangeSliderViewEndDragEvent extends Event<OnRangeSliderViewEndDragEvent> {
private final double leftKnobValue;
private final double rightKnobValue;
private static final String LEFT_KNOB_KEY = "leftKnobValue";
private static final String RIGHT_KNOB_KEY = "rightKnobValue";
public static final String NAME = "topRangeSliderViewEndDrag";
public static final String EVENT_PROP_NAME = "onRangeSliderViewEndDrag";
public OnRangeSliderViewEndDragEvent(
int surfaceId,
int viewId,
double leftKnobValue,
double rightKnobValue
) {
super(surfaceId, viewId);
this.leftKnobValue = leftKnobValue;
this.rightKnobValue = rightKnobValue;
}
@Override
public String getEventName() {
return NAME;
}
@Nullable
@Override
public WritableMap getEventData() {
return createPayload();
}
private WritableMap createPayload() {
WritableMap payload = Arguments.createMap();
payload.putDouble(LEFT_KNOB_KEY, leftKnobValue);
payload.putDouble(RIGHT_KNOB_KEY, rightKnobValue);
return payload;
}
}
package com.rangesliderpackage;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
public class OnRangeSliderViewValueChangeEvent extends Event<OnRangeSliderViewValueChangeEvent> {
private final double leftKnobValue;
private final double rightKnobValue;
private static final String LEFT_KNOB_KEY = "leftKnobValue";
private static final String RIGHT_KNOB_KEY = "rightKnobValue";
public static final String NAME = "topRangeSliderViewValueChange";
public static final String EVENT_PROP_NAME = "onRangeSliderViewValueChange";
public OnRangeSliderViewValueChangeEvent(
int surfaceId,
int viewId,
double leftKnobValue,
double rightKnobValue
) {
super(surfaceId, viewId);
this.leftKnobValue = leftKnobValue;
this.rightKnobValue = rightKnobValue;
}
@Override
public String getEventName() {
return NAME;
}
@Nullable
@Override
public WritableMap getEventData() {
return createPayload();
}
private WritableMap createPayload() {
WritableMap payload = Arguments.createMap();
payload.putDouble(LEFT_KNOB_KEY, leftKnobValue);
payload.putDouble(RIGHT_KNOB_KEY, rightKnobValue);
return payload;
}
}
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.java
Now, let's declare the custom view that will hold our range slider:
package com.rangesliderpackage;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PixelUtil;
import com.google.android.material.slider.RangeSlider;
public class RangeSliderView extends FrameLayout {
private final ReactContext reactContext;
private final RangeSlider slider;
private float mLastLeftKnobValue = 0f;
private float mLastRightKnobValue = 1f;
public static final String NAME = "RangeSliderView";
public RangeSliderView(ReactContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.slider = new RangeSlider(reactContext);
this.slider.setTrackHeight((int) PixelUtil.toPixelFromDIP(10f));
this.slider.setThumbTintList(ColorStateList.valueOf(Color.BLUE));
this.slider.addOnSliderTouchListener(new RangeSlider.OnSliderTouchListener() {
@Override
public void onStartTrackingTouch(@NonNull RangeSlider slider) {
//
}
@Override
public void onStopTrackingTouch(@NonNull RangeSlider slider) {
//
}
});
this.slider.addOnChangeListener(
(slider, value, fromUser) -> {
float newLeftKnobValue = slider.getValues().get(0);
float newRightKnobValue = slider.getValues().get(1);
if (
Math.abs(newLeftKnobValue - mLastLeftKnobValue) < 0.1f &&
Math.abs(newRightKnobValue - mLastRightKnobValue) < 0.1f
) {
return;
}
mLastLeftKnobValue = newLeftKnobValue;
mLastRightKnobValue = newRightKnobValue;
// ...
}
);
this.addView(this.slider, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
}
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:
// ...
public class RangeSliderView extends FrameLayout {
// ...
public void setActiveColor(@Nullable Integer activeColor) {
int newColor = Color.BLUE;
if (activeColor != null) {
newColor = activeColor;
}
slider.setTrackActiveTintList(ColorStateList.valueOf(newColor));
}
public void setInactiveColor(@Nullable Integer inactiveColor) {
int newColor = Color.GRAY;
if (inactiveColor != null) {
newColor = inactiveColor;
}
slider.setTrackInactiveTintList(ColorStateList.valueOf(newColor));
}
public void setMinValue(double minValue) {
slider.setValueFrom((float) minValue);
}
public void setMaxValue(double maxValue) {
slider.setValueTo((float) maxValue);
}
public void setLeftKnobValue(double leftKnobValue) {
if (Double.isNaN(leftKnobValue)) {
return;
}
if (slider.getValues().size() < 2) {
slider.setValues((float) leftKnobValue, (float) leftKnobValue + 1);
return;
}
float rightKnobValue = slider.getValues().get(1);
slider.setValues((float) leftKnobValue, rightKnobValue);
}
public void setRightKnobValue(double rightKnobValue) {
if (Double.isNaN(rightKnobValue)) {
return;
}
if (slider.getValues().size() < 1) {
slider.setValues((float) rightKnobValue - 1, (float) rightKnobValue);
return;
}
float leftKnobValue = slider.getValues().get(0);
slider.setValues(leftKnobValue, (float) rightKnobValue);
}
public void setStep(int step) {
slider.setStepSize((float) step);
}
}
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:
public class RangeSliderView extends FrameLayout {
public RangeSliderView(ReactContext reactContext) {
//
}
public interface OnRangeSliderViewListener {
void onRangeSliderViewBeginDrag();
void onRangeSliderViewEndDrag(double leftKnobValue, double rightKnobValue);
void onRangeSliderViewValueChange(double leftKnobValue, double rightKnobValue);
}
// ...
}
Next, we'll add a listener property and use it to send events to the receiver:
public class RangeSliderView extends FrameLayout {
public RangeSliderView(ReactContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.slider = new RangeSlider(reactContext);
this.slider.setTrackHeight((int) PixelUtil.toPixelFromDIP(10f));
this.slider.setThumbTintList(ColorStateList.valueOf(Color.BLUE));
this.slider.addOnSliderTouchListener(new RangeSlider.OnSliderTouchListener() {
@Override
public void onStartTrackingTouch(@NonNull RangeSlider slider) {
sendOnRangeSliderViewBeginDragEvent();
}
@Override
public void onStopTrackingTouch(@NonNull RangeSlider slider) {
sendOnRangeSliderViewEndDragEvent((double) slider.getValues().get(0), (double) slider.getValues().get(1));
}
});
this.slider.addOnChangeListener(
(slider, value, fromUser) -> {
float newLeftKnobValue = slider.getValues().get(0);
float newRightKnobValue = slider.getValues().get(1);
if (
Math.abs(newLeftKnobValue - mLastLeftKnobValue) < 0.1f &&
Math.abs(newRightKnobValue - mLastRightKnobValue) < 0.1f
) {
return;
}
mLastLeftKnobValue = newLeftKnobValue;
mLastRightKnobValue = newRightKnobValue;
sendOnRangeSliderViewValueChangeEvent(newLeftKnobValue, newRightKnobValue);
}
);
this.addView(this.slider, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
public interface OnRangeSliderViewListener {
void onRangeSliderViewBeginDrag();
void onRangeSliderViewEndDrag(double leftKnobValue, double rightKnobValue);
void onRangeSliderViewValueChange(double leftKnobValue, double rightKnobValue);
}
private @Nullable OnRangeSliderViewListener mListener = null;
public void setOnRangeSliderViewListener(@Nullable OnRangeSliderViewListener listener) {
mListener = listener;
}
// ...
private void sendOnRangeSliderViewValueChangeEvent(double leftKnobValue, double rightKnobValue) {
@Nullable final OnRangeSliderViewListener listener = mListener;
if (listener != null) {
listener.onRangeSliderViewValueChange(leftKnobValue, rightKnobValue);
}
}
private void sendOnRangeSliderViewBeginDragEvent() {
@Nullable final OnRangeSliderViewListener listener = mListener;
if (listener != null) {
listener.onRangeSliderViewBeginDrag();
}
}
private void sendOnRangeSliderViewEndDragEvent(double leftKnobValue, double rightKnobValue) {
@Nullable final OnRangeSliderViewListener listener = mListener;
if (listener != null) {
listener.onRangeSliderViewEndDrag(leftKnobValue, rightKnobValue);
}
}
}
Cool! Now we have everything in place, let's use it in view manager class.
Complete RangeSliderView.java
file
package com.rangesliderpackage;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PixelUtil;
import com.google.android.material.slider.RangeSlider;
public class RangeSliderView extends FrameLayout {
private final ReactContext reactContext;
private final RangeSlider slider;
private float mLastLeftKnobValue = 0f;
private float mLastRightKnobValue = 1f;
public static final String NAME = "RangeSliderView";
public RangeSliderView(ReactContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.slider = new RangeSlider(reactContext);
this.slider.setTrackHeight((int) PixelUtil.toPixelFromDIP(10f));
this.slider.setThumbTintList(ColorStateList.valueOf(Color.BLUE));
this.slider.addOnSliderTouchListener(new RangeSlider.OnSliderTouchListener() {
@Override
public void onStartTrackingTouch(@NonNull RangeSlider slider) {
sendOnRangeSliderViewBeginDragEvent();
}
@Override
public void onStopTrackingTouch(@NonNull RangeSlider slider) {
sendOnRangeSliderViewEndDragEvent((double) slider.getValues().get(0), (double) slider.getValues().get(1));
}
});
this.slider.addOnChangeListener(
(slider, value, fromUser) -> {
float newLeftKnobValue = slider.getValues().get(0);
float newRightKnobValue = slider.getValues().get(1);
if (
Math.abs(newLeftKnobValue - mLastLeftKnobValue) < 0.1f &&
Math.abs(newRightKnobValue - mLastRightKnobValue) < 0.1f
) {
return;
}
mLastLeftKnobValue = newLeftKnobValue;
mLastRightKnobValue = newRightKnobValue;
sendOnRangeSliderViewValueChangeEvent(newLeftKnobValue, newRightKnobValue);
}
);
this.addView(this.slider, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
public interface OnRangeSliderViewListener {
void onRangeSliderViewBeginDrag();
void onRangeSliderViewEndDrag(double leftKnobValue, double rightKnobValue);
void onRangeSliderViewValueChange(double leftKnobValue, double rightKnobValue);
}
private @Nullable OnRangeSliderViewListener mListener = null;
public void setOnRangeSliderViewListener(@Nullable OnRangeSliderViewListener listener) {
mListener = listener;
}
public void setActiveColor(@Nullable Integer activeColor) {
int newColor = Color.BLUE;
if (activeColor != null) {
newColor = activeColor;
}
slider.setTrackActiveTintList(ColorStateList.valueOf(newColor));
}
public void setInactiveColor(@Nullable Integer inactiveColor) {
int newColor = Color.GRAY;
if (inactiveColor != null) {
newColor = inactiveColor;
}
slider.setTrackInactiveTintList(ColorStateList.valueOf(newColor));
}
public void setMinValue(double minValue) {
slider.setValueFrom((float) minValue);
}
public void setMaxValue(double maxValue) {
slider.setValueTo((float) maxValue);
}
public void setLeftKnobValue(double leftKnobValue) {
if (Double.isNaN(leftKnobValue)) {
return;
}
if (slider.getValues().size() < 2) {
slider.setValues((float) leftKnobValue, (float) leftKnobValue + 1);
return;
}
float rightKnobValue = slider.getValues().get(1);
slider.setValues((float) leftKnobValue, rightKnobValue);
}
public void setRightKnobValue(double rightKnobValue) {
if (Double.isNaN(rightKnobValue)) {
return;
}
if (slider.getValues().size() < 1) {
slider.setValues((float) rightKnobValue - 1, (float) rightKnobValue);
return;
}
float leftKnobValue = slider.getValues().get(0);
slider.setValues(leftKnobValue, (float) rightKnobValue);
}
public void setStep(int step) {
slider.setStepSize((float) step);
}
private void sendOnRangeSliderViewValueChangeEvent(double leftKnobValue, double rightKnobValue) {
@Nullable final OnRangeSliderViewListener listener = mListener;
if (listener != null) {
listener.onRangeSliderViewValueChange(leftKnobValue, rightKnobValue);
}
}
private void sendOnRangeSliderViewBeginDragEvent() {
@Nullable final OnRangeSliderViewListener listener = mListener;
if (listener != null) {
listener.onRangeSliderViewBeginDrag();
}
}
private void sendOnRangeSliderViewEndDragEvent(double leftKnobValue, double rightKnobValue) {
@Nullable final OnRangeSliderViewListener listener = mListener;
if (listener != null) {
listener.onRangeSliderViewEndDrag(leftKnobValue, rightKnobValue);
}
}
}
RangeSliderViewManager.java
The view manager class will connect the slider with our RN app - let's start by creating the boilerplate:
package com.rangesliderpackage;
import android.view.View;
import androidx.annotation.Nullable;
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.uimanager.events.EventDispatcher;
import com.facebook.react.viewmanagers.RangeSliderViewManagerDelegate;
import com.facebook.react.viewmanagers.RangeSliderViewManagerInterface;
import java.util.List;
import java.util.Map;
@ReactModule(name = RangeSliderView.NAME)
public class RangeSliderViewManager extends ViewGroupManager<RangeSliderView> implements RangeSliderViewManagerInterface<RangeSliderView> {
private final RangeSliderViewManagerDelegate mDelegate = new RangeSliderViewManagerDelegate(this);
@Override
public String getName() {
return RangeSliderView.NAME;
}
@Override
public ViewManagerDelegate<RangeSliderView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(RangeSliderView root, String commandId, ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public RangeSliderView createViewInstance(ThemedReactContext reactContext) {
return new RangeSliderView(reactContext);
}
@Override
@ReactProp(name = "activeColor", customType = "Color")
public void setActiveColor(RangeSliderView view, @Nullable Integer activeColor) {
view.setActiveColor(activeColor);
}
@Override
@ReactProp(name = "inactiveColor", customType = "Color")
public void setInactiveColor(RangeSliderView view, @Nullable Integer inactiveColor) {
view.setInactiveColor(inactiveColor);
}
@Override
@ReactProp(name = "minValue")
public void setMinValue(RangeSliderView view, double value) {
view.setMinValue(value);
}
@Override
@ReactProp(name = "maxValue")
public void setMaxValue(RangeSliderView view, double value) {
view.setMaxValue(value);
}
@Override
@ReactProp(name = "leftKnobValue")
public void setLeftKnobValue(RangeSliderView view, double value) {
view.setLeftKnobValue(value);
}
@Override
@ReactProp(name = "rightKnobValue")
public void setRightKnobValue(RangeSliderView view, double value) {
view.setRightKnobValue(value);
}
@Override
@ReactProp(name = "step")
public void setStep(RangeSliderView view, int step) {
view.setStep(step);
}
@Override
public void setLeftKnobValueProgrammatically(RangeSliderView view, double value) {
view.setLeftKnobValue(value);
}
@Override
public void setRightKnobValueProgrammatically(RangeSliderView view, double value) {
view.setRightKnobValue(value);
}
@Override
public void addView(RangeSliderView parent, View child, int index) {
// That component does not accept child views
}
@Override
public void addViews(RangeSliderView parent, List<View> views) {
// That component does not accept child views
}
@Override
public void removeAllViews(RangeSliderView parent) {
// That component does not accept child views
}
@Override
public void removeView(RangeSliderView parent, View view) {
// That component does not accept child views
}
@Override
public void removeViewAt(RangeSliderView parent, int index) {
// 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 ingetName
(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.
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:
package com.rangesliderpackage;
import android.view.View;
import androidx.annotation.Nullable;
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.uimanager.events.EventDispatcher;
import com.facebook.react.viewmanagers.RangeSliderViewManagerDelegate;
import com.facebook.react.viewmanagers.RangeSliderViewManagerInterface;
import java.util.List;
import java.util.Map;
@ReactModule(name = RangeSliderView.NAME)
public class RangeSliderViewManager extends ViewGroupManager<RangeSliderView> implements RangeSliderViewManagerInterface<RangeSliderView> {
// ...
@Override
protected void addEventEmitters(ThemedReactContext reactContext, RangeSliderView view) {
super.addEventEmitters(reactContext, view);
view.setOnRangeSliderViewListener(new RangeSliderView.OnRangeSliderViewListener() {
@Override
public void onRangeSliderViewValueChange(double leftKnobValue, double rightKnobValue) {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewValueChangeEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId(),
leftKnobValue,
rightKnobValue
)
);
}
}
@Override
public void onRangeSliderViewBeginDrag() {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewBeginDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId()
)
);
}
}
@Override
public void onRangeSliderViewEndDrag(double leftKnobValue, double rightKnobValue) {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewEndDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId(),
leftKnobValue,
rightKnobValue
)
);
}
}
});
}
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
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.java
file
package com.rangesliderpackage;
import android.view.View;
import androidx.annotation.Nullable;
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.uimanager.events.EventDispatcher;
import com.facebook.react.viewmanagers.RangeSliderViewManagerDelegate;
import com.facebook.react.viewmanagers.RangeSliderViewManagerInterface;
import java.util.List;
import java.util.Map;
@ReactModule(name = RangeSliderView.NAME)
public class RangeSliderViewManager extends ViewGroupManager<RangeSliderView> implements RangeSliderViewManagerInterface<RangeSliderView> {
private final RangeSliderViewManagerDelegate mDelegate = new RangeSliderViewManagerDelegate(this);
@Override
public String getName() {
return RangeSliderView.NAME;
}
@Override
public ViewManagerDelegate<RangeSliderView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(RangeSliderView root, String commandId, ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public RangeSliderView createViewInstance(ThemedReactContext reactContext) {
return new RangeSliderView(reactContext);
}
@Override
@ReactProp(name = "activeColor", customType = "Color")
public void setActiveColor(RangeSliderView view, @Nullable Integer activeColor) {
view.setActiveColor(activeColor);
}
@Override
@ReactProp(name = "inactiveColor", customType = "Color")
public void setInactiveColor(RangeSliderView view, @Nullable Integer inactiveColor) {
view.setInactiveColor(inactiveColor);
}
@Override
@ReactProp(name = "minValue")
public void setMinValue(RangeSliderView view, double value) {
view.setMinValue(value);
}
@Override
@ReactProp(name = "maxValue")
public void setMaxValue(RangeSliderView view, double value) {
view.setMaxValue(value);
}
@Override
@ReactProp(name = "leftKnobValue")
public void setLeftKnobValue(RangeSliderView view, double value) {
view.setLeftKnobValue(value);
}
@Override
@ReactProp(name = "rightKnobValue")
public void setRightKnobValue(RangeSliderView view, double value) {
view.setRightKnobValue(value);
}
@Override
@ReactProp(name = "step")
public void setStep(RangeSliderView view, int step) {
view.setStep(step);
}
@Override
public void setLeftKnobValueProgrammatically(RangeSliderView view, double value) {
view.setLeftKnobValue(value);
}
@Override
public void setRightKnobValueProgrammatically(RangeSliderView view, double value) {
view.setRightKnobValue(value);
}
@Override
public void addView(RangeSliderView parent, View child, int index) {
// That component does not accept child views
}
@Override
public void addViews(RangeSliderView parent, List<View> views) {
// That component does not accept child views
}
@Override
public void removeAllViews(RangeSliderView parent) {
// That component does not accept child views
}
@Override
public void removeView(RangeSliderView parent, View view) {
// That component does not accept child views
}
@Override
public void removeViewAt(RangeSliderView parent, int index) {
// That component does not accept child views
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, RangeSliderView view) {
super.addEventEmitters(reactContext, view);
view.setOnRangeSliderViewListener(new RangeSliderView.OnRangeSliderViewListener() {
@Override
public void onRangeSliderViewValueChange(double leftKnobValue, double rightKnobValue) {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewValueChangeEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId(),
leftKnobValue,
rightKnobValue
)
);
}
}
@Override
public void onRangeSliderViewBeginDrag() {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewBeginDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId()
)
);
}
}
@Override
public void onRangeSliderViewEndDrag(double leftKnobValue, double rightKnobValue) {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewEndDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId(),
leftKnobValue,
rightKnobValue
)
);
}
}
});
}
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
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
android/src/oldarch/java/com/rangesliderpackage/RangeSliderViewManager.java
at other text editor and paste following content:package com.rangesliderpackage;
import android.view.View;
import androidx.annotation.Nullable;
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;
import com.facebook.react.uimanager.events.EventDispatcher;
import java.util.List;
import java.util.Map;
@ReactModule(name = RangeSliderView.NAME)
public class RangeSliderViewManager extends ViewGroupManager<RangeSliderView> {
@Override
public String getName() {
return RangeSliderView.NAME;
}
@Override
public void receiveCommand(RangeSliderView root, String commandId, @Nullable ReadableArray args) {
super.receiveCommand(root, commandId, args);
switch (commandId) {
case "setLeftKnobValueProgrammatically":
final double leftKnobValue = args.getDouble(0);
setLeftKnobValueProgrammatically(root, leftKnobValue);
break;
case "setRightKnobValueProgrammatically":
final double rightKnobValue = args.getDouble(0);
setRightKnobValueProgrammatically(root, rightKnobValue);
break;
}
}
@Override
public RangeSliderView createViewInstance(ThemedReactContext reactContext) {
return new RangeSliderView(reactContext);
}
@ReactProp(name = "activeColor", customType = "Color")
public void setActiveColor(RangeSliderView view, @Nullable Integer activeColor) {
view.setActiveColor(activeColor);
}
@ReactProp(name = "inactiveColor", customType = "Color")
public void setInactiveColor(RangeSliderView view, @Nullable Integer inactiveColor) {
view.setInactiveColor(inactiveColor);
}
@ReactProp(name = "minValue")
public void setMinValue(RangeSliderView view, double value) {
view.setMinValue(value);
}
@ReactProp(name = "maxValue")
public void setMaxValue(RangeSliderView view, double value) {
view.setMaxValue(value);
}
@ReactProp(name = "leftKnobValue")
public void setLeftKnobValue(RangeSliderView view, double value) {
view.setLeftKnobValue(value);
}
@ReactProp(name = "rightKnobValue")
public void setRightKnobValue(RangeSliderView view, double value) {
view.setRightKnobValue(value);
}
@ReactProp(name = "step")
public void setStep(RangeSliderView view, int step) {
view.setStep(step);
}
private void setLeftKnobValueProgrammatically(RangeSliderView view, double value) {
view.setLeftKnobValue(value);
}
private void setRightKnobValueProgrammatically(RangeSliderView view, double value) {
view.setRightKnobValue(value);
}
@Override
public void addView(RangeSliderView parent, View child, int index) {
// That component does not accept child views
}
@Override
public void addViews(RangeSliderView parent, List<View> views) {
// That component does not accept child views
}
@Override
public void removeAllViews(RangeSliderView parent) {
// That component does not accept child views
}
@Override
public void removeView(RangeSliderView parent, View view) {
// That component does not accept child views
}
@Override
public void removeViewAt(RangeSliderView parent, int index) {
// That component does not accept child views
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, RangeSliderView view) {
super.addEventEmitters(reactContext, view);
view.setOnRangeSliderViewListener(new RangeSliderView.OnRangeSliderViewListener() {
@Override
public void onRangeSliderViewValueChange(double leftKnobValue, double rightKnobValue) {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewValueChangeEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId(),
leftKnobValue,
rightKnobValue
)
);
}
}
@Override
public void onRangeSliderViewBeginDrag() {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewBeginDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId()
)
);
}
}
@Override
public void onRangeSliderViewEndDrag(double leftKnobValue, double rightKnobValue) {
final EventDispatcher dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId());
if (dispatcher != null) {
dispatcher.dispatchEvent(
new OnRangeSliderViewEndDragEvent(
UIManagerHelper.getSurfaceId(reactContext),
view.getId(),
leftKnobValue,
rightKnobValue
)
);
}
}
});
}
@Override
public Map getExportedCustomDirectEventTypeConstants() {
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.java
The last thing we need to do is to export RangeSliderViewManager
in the TurboReactPackage
instance. Let's go to RangeSliderTurboPackage.java
and add our new module.
package com.rangesliderpackage;
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 RangeSliderTurboPackage 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 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!