Skip to content

Commit

Permalink
feat(nativeUserLocation): implement customizable pulsing (#3202)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfazekas authored Nov 26, 2023
1 parent fbe655a commit dba1707
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.mapbox.android.core.permissions.PermissionsManager
import com.mapbox.bindgen.Value
import com.mapbox.maps.Image
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.locationcomponent.LocationComponentConstants
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.maps.plugin.locationcomponent.R as LR
import com.rnmapbox.rnmbx.R
Expand All @@ -26,6 +30,8 @@ import com.rnmapbox.rnmbx.components.mapview.OnMapReadyCallback
import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView
import com.rnmapbox.rnmbx.utils.BitmapUtils
import com.rnmapbox.rnmbx.utils.Logger
import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotBoolean
import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotString
import com.rnmapbox.rnmbx.v11compat.image.AppCompatResourcesV11
import com.rnmapbox.rnmbx.v11compat.image.ImageHolder
import com.rnmapbox.rnmbx.v11compat.image.toDrawable
Expand Down Expand Up @@ -85,6 +91,12 @@ class RNMBXNativeUserLocation(context: Context) : AbstractMapFeature(context), O
_apply()
}

var pulsing: ReadableMap? = null
set(value) {
field = value
_apply()
}

private fun imageNameUpdated(image: PuckImagePart, name: String?) {
if (name != null) {
imageNames[image] = name
Expand Down Expand Up @@ -153,6 +165,41 @@ class RNMBXNativeUserLocation(context: Context) : AbstractMapFeature(context), O
this.puckBearingEnabled?.let {
location2.puckBearingEnabled = it
}

pulsing?.let { pulsing ->
pulsing.getAndLogIfNotString("kind")?.also { kind ->
if (kind == "default") {
location2.pulsingEnabled = true
}
}
if (pulsing.hasKey("color")) {
when (pulsing.getType("color")) {
ReadableType.Map ->
location2.pulsingColor = ColorPropConverter.getColor(pulsing.getMap("color"), mContext)
ReadableType.Number ->
location2.pulsingColor = pulsing.getInt("color")
else ->
Logger.e(LOG_TAG, "pusling.color should be either a map or a number, but was ${pulsing.getDynamic("color")}")
}
}
pulsing.getAndLogIfNotBoolean("isEnabled")?.let { enabled ->
location2.pulsingEnabled = enabled
}
if (pulsing.hasKey("radius")) {
when (pulsing.getType("radius")) {
ReadableType.Number ->
location2.pulsingMaxRadius = pulsing.getDouble("radius").toFloat()
ReadableType.String ->
if (pulsing.getString("radius") == "accuracy") {
location2.pulsingMaxRadius = LocationComponentConstants.PULSING_MAX_RADIUS_FOLLOW_ACCURACY
} else {
Logger.e(LOG_TAG, "Expected pulsing/radius to be a number or accuracy but was ${pulsing.getString("radius")}")
}
else ->
Logger.e(LOG_TAG, "Expected pulsing/radius to be a number or accuracy but was ${pulsing.getString("radius")}")
}
}
}
}

override fun addToMap(mapView: RNMBXMapView) {
Expand Down Expand Up @@ -229,6 +276,10 @@ class RNMBXNativeUserLocation(context: Context) : AbstractMapFeature(context), O
}
}
// endregion

companion object {
const val LOG_TAG = "RNMBXNativeUserLocation"
}
}

fun makeDefaultLocationPuck2D(context: Context, renderMode: RenderMode): LocationPuck2D {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.rnmapbox.rnmbx.components.location

import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
Expand Down Expand Up @@ -82,6 +83,13 @@ class RNMBXNativeUserLocationManager : ViewGroupManager<RNMBXNativeUserLocation>
view.visible = value
}

@ReactProp(name = "pulsing")
override fun setPulsing(view: RNMBXNativeUserLocation, value: Dynamic) {
if (!value.isNull) {
view.pulsing = value.asMap()
}
}

@Nonnull
override fun createViewInstance(@Nonnull reactContext: ThemedReactContext): RNMBXNativeUserLocation {
return RNMBXNativeUserLocation(reactContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "visible":
mViewManager.setVisible(view, value == null ? false : (boolean) value);
break;
case "pulsing":
mViewManager.setPulsing(view, new DynamicFromObject(value));
break;
default:
super.setProperty(view, propName, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ public interface RNMBXNativeUserLocationManagerInterface<T extends View> {
void setTopImage(T view, Dynamic value);
void setScale(T view, Dynamic value);
void setVisible(T view, boolean value);
void setPulsing(T view, Dynamic value);
}
27 changes: 27 additions & 0 deletions docs/NativeUserLocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,33 @@ The size of the images, as a scale factor applied to the size of the specified i

[Custom Native UserLocation](../examples/UserLocation/CustomNativeUserLocation)

### pulsing

```tsx
| {
/**
* Flag determining whether the pulsing circle animation.
*/
isEnabled?: boolean;

/**
* The color of the pulsing circle.
*/
color?: number | ColorValue;

/**
* Circle radius configuration for the pulsing circle animation.
* - accuracy: Pulsing circle animates with the `horizontalAccuracy` form the latest puck location.
* - number: Pulsing circle should animate with the constant radius.
*/
radius?: 'accuracy' | number;
}
| 'default'
```
The configration parameters for sonar-like pulsing circle animation shown around the 2D puck.



### visible

```tsx
Expand Down
7 changes: 7 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4204,6 +4204,13 @@
"default": "none",
"description": "The size of the images, as a scale factor applied to the size of the specified image. Supports expressions based on zoom.\n\n@example\n[\"interpolate\",[\"linear\"], [\"zoom\"], 10.0, 1.0, 20.0, 4.0]]\n@example\n2.0"
},
{
"name": "pulsing",
"required": false,
"type": "\\| {\n /**\n * Flag determining whether the pulsing circle animation.\n */\n isEnabled?: boolean;\n\n /**\n * The color of the pulsing circle.\n */\n color?: number \\| ColorValue;\n\n /**\n * Circle radius configuration for the pulsing circle animation.\n * - accuracy: Pulsing circle animates with the `horizontalAccuracy` form the latest puck location.\n * - number: Pulsing circle should animate with the constant radius.\n */\n radius?: 'accuracy' \\| number;\n }\n\\| 'default'",
"default": "none",
"description": "The configration parameters for sonar-like pulsing circle animation shown around the 2D puck."
},
{
"name": "visible",
"required": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const UserLocationNativeAnimated = () => {
topImage="topImage"
visible={true}
scale={['interpolate', ['linear'], ['zoom'], 10, 1.0, 20, 4.0]}
pulsing={{
isEnabled: true,
color: 'teal',
radius: 50.0,
}}
/>
</MapView>
</SafeAreaView>
Expand Down
16 changes: 11 additions & 5 deletions ios/RNMBX/RNMBXFabricPropConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,34 @@ BOOL RNMBXPropConvert_Optional_BOOL(const folly::dynamic &dyn, NSString* propert
NSString* RNMBXPropConvert_Optional_NSString(const folly::dynamic &dyn, NSString* propertyName);
id RNMBXPropConvert_Optional_ExpressionDouble(const folly::dynamic &dyn, NSString* propertyName);
BOOL RNMBXPropConvert_BOOL(const folly::dynamic &dyn, NSString* propertyName);
NSDictionary* RNMBXPropConvert_Optional_NSDictionary(const folly::dynamic &dyn, NSString* propertyName);

#define RNMBX_OPTIONAL_RPOP_BOOL_NSNumber(name) \
#define RNMBX_OPTIONAL_PROP_BOOL_NSNumber(name) \
if ((!oldProps.get() || oldViewProps.name != newViewProps.name) && !newViewProps.name.isNull()) { \
_view.name = RNMBXPropConvert_Optional_BOOL_NSNumber(newViewProps.name, @#name); \
}

#define RNMBX_OPTIONAL_RPOP_BOOL(name) \
#define RNMBX_OPTIONAL_PROP_BOOL(name) \
if ((!oldProps.get() || oldViewProps.name != newViewProps.name) && !newViewProps.name.isNull()) { \
_view.name = RNMBXPropConvert_Optional_BOOL(newViewProps.name, @#name); \
}

#define RNMBX_OPTIONAL_RPOP_NSString(name) \
#define RNMBX_OPTIONAL_PROP_NSString(name) \
if ((!oldProps.get() || oldViewProps.name != newViewProps.name) && !newViewProps.name.isNull()) { \
_view.name = RNMBXPropConvert_Optional_NSString(newViewProps.name, @#name); \
}

#define RNMBX_OPTIONAL_RPOP_ExpressionDouble(name) \
#define RNMBX_OPTIONAL_PROP_ExpressionDouble(name) \
if ((!oldProps.get() || oldViewProps.name != newViewProps.name) && !newViewProps.name.isNull()) { \
_view.name = RNMBXPropConvert_Optional_ExpressionDouble(newViewProps.name, @#name); \
}

#define RNMBX_RPOP_BOOL(name) \
#define RNMBX_PROP_BOOL(name) \
if ((!oldProps.get() || oldViewProps.name != newViewProps.name)) { \
_view.name = RNMBXPropConvert_BOOL(newViewProps.name, @#name); \
}

#define RNMBX_OPTIONAL_PROP_NSDictionary(name) \
if ((!oldProps.get() || oldViewProps.name != newViewProps.name)) { \
_view.name = RNMBXPropConvert_Optional_NSDictionary(newViewProps.name, @#name); \
}
5 changes: 5 additions & 0 deletions ios/RNMBX/RNMBXFabricPropConvert.mm
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,9 @@ id RNMBXPropConvert_Optional_ExpressionDouble(const folly::dynamic &dyn, NSStrin
}
}

NSDictionary* RNMBXPropConvert_Optional_NSDictionary(const folly::dynamic &dyn, NSString* propertyName)
{
return RNMBXPropConvert_ID(dyn);
}

#endif
63 changes: 47 additions & 16 deletions ios/RNMBX/RNMBXNativeUserLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public class RNMBXNativeUserLocation: UIView, RNMBXMapComponent {

@objc
public var puckBearingEnabled: Bool = false

@objc
public var pulsing: NSDictionary? = nil

@objc
override public func didSetProps(_ props: [String]) {
Expand Down Expand Up @@ -151,20 +154,7 @@ public class RNMBXNativeUserLocation: UIView, RNMBXMapComponent {
return
}

if (visible) {
if images.isEmpty {
location.options.puckType = .puck2D(.makeDefault(showBearing: puckBearingEnabled))
} else {
location.options.puckType = .puck2D(
Puck2DConfiguration(
topImage: self.images[.top],
bearingImage: self.images[.bearing],
shadowImage: self.images[.shadow],
scale: toDoubleValue(value: scale, name: "scale")
)
)
}
} else {
if (!visible) {
let emptyImage = UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1)).image { _ in }
location.options.puckType = .puck2D(
Puck2DConfiguration(
Expand All @@ -174,9 +164,50 @@ public class RNMBXNativeUserLocation: UIView, RNMBXMapComponent {
scale: Value.constant(1.0)
)
)
return
} else {
var configuration : Puck2DConfiguration = images.isEmpty ?
.makeDefault(showBearing: puckBearingEnabled) : Puck2DConfiguration(
topImage: self.images[.top],
bearingImage: self.images[.bearing],
shadowImage: self.images[.shadow])

if let scale = toDoubleValue(value: scale, name: "scale") {
configuration.scale = scale
}

if let pulsing = pulsing {
if let kind = pulsing["kind"] as? String, kind == "default" {
configuration.pulsing = .default
} else {
var pulsingConfig = Puck2DConfiguration.Pulsing()
if let isEnabled = pulsing["isEnabled"] as? Bool {
pulsingConfig.isEnabled = isEnabled
}

if let radius = pulsing["radius"] as? String {
if radius == "accuracy" {
pulsingConfig.radius = .accuracy
} else {
Logger.log(level: .error, message: "expected pulsing/radius to be either a number or accuracy but was \(radius)")
}
} else if let radius = pulsing["radius"] as? NSNumber {
pulsingConfig.radius = .constant(radius.doubleValue)
}

if let color = pulsing["color"] as? Any {
if let uicolor = RCTConvert.uiColor(color) {
pulsingConfig.color = uicolor
} else {
Logger.log(level: .error, message: "expected color to be a color but was \(color)")
}
}

configuration.pulsing = pulsingConfig
}
}
location.options.puckType = .puck2D(configuration)
}


location.options.puckBearingEnabled = puckBearingEnabled
if let puckBearing = _puckBearing {
location.options.puckBearing = puckBearing
Expand Down
15 changes: 8 additions & 7 deletions ios/RNMBX/RNMBXNativeUserLocationComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
const auto &oldViewProps = static_cast<const RNMBXNativeUserLocationProps &>(*oldProps);
const auto &newViewProps = static_cast<const RNMBXNativeUserLocationProps &>(*props);

RNMBX_OPTIONAL_RPOP_NSString(puckBearing)
RNMBX_OPTIONAL_RPOP_BOOL(puckBearingEnabled)
RNMBX_OPTIONAL_RPOP_NSString(bearingImage)
RNMBX_OPTIONAL_RPOP_NSString(shadowImage)
RNMBX_OPTIONAL_RPOP_NSString(topImage)
RNMBX_OPTIONAL_RPOP_ExpressionDouble(scale)
RNMBX_RPOP_BOOL(visible)
RNMBX_OPTIONAL_PROP_NSString(puckBearing)
RNMBX_OPTIONAL_PROP_BOOL(puckBearingEnabled)
RNMBX_OPTIONAL_PROP_NSString(bearingImage)
RNMBX_OPTIONAL_PROP_NSString(shadowImage)
RNMBX_OPTIONAL_PROP_NSString(topImage)
RNMBX_OPTIONAL_PROP_ExpressionDouble(scale)
RNMBX_PROP_BOOL(visible)
RNMBX_OPTIONAL_PROP_NSDictionary(pulsing)

[super updateProps:props oldProps:oldProps];

Expand Down
1 change: 1 addition & 0 deletions ios/RNMBX/RNMBXNativeUserLocationViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ @interface RCT_EXTERN_REMAP_MODULE(RNMBXNativeUserLocation, RNMBXNativeUserLocat
RCT_EXPORT_VIEW_PROPERTY(visible, BOOL);
RCT_EXPORT_VIEW_PROPERTY(puckBearing, NSString);
RCT_EXPORT_VIEW_PROPERTY(puckBearingEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(pulsing, NSDictionary);

@end

Loading

0 comments on commit dba1707

Please sign in to comment.