I am currently experimenting with creating native libraries for React Native. My React Native app really struggles with a compass animation on older devices, for this reason I am trying to move this part to native for better performance on Android.
This is the first time for me trying to build a native component, I haven’t found a good information source that explains the thing I am trying to do.
The React Native documentation for a native android component pointed me to a java class with a SimpleViewManager, this class can set a view and its properties. The thing that I am struggling with is implementing a compassActivity in this view. The ultimate goal is to import a API in React Native that takes care of the heading information of the device and the compass animation altogether. My first approach was to return a CompassActivity in the createViewInstance, but this resulted in a lot of errors.
This is my ViewManager:
package com.nativeview;
import android.graphics.Color;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.VideoView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
public class VideoViewManager extends SimpleViewManager<View> {
public static final String REACT_CLASS = "CompassView";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
@NonNull
public View createViewInstance(ThemedReactContext reactContext) {
return new View(reactContext);
}
@ReactProp(name = "color")
public void setColor(View view, String color) {
view.setBackgroundColor(Color.parseColor(color));
}
}
The CompassActivity looks like this:
package com.nativeview;
import android.net.Uri;
import android.util.Log;
import android.widget.VideoView;
import com.facebook.react.ReactActivity;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.TextView;
public class CompassActivity extends AppCompatActivity {
private static final String TAG = "CompassActivity";
private Compass compass;
private ImageView arrowView;
private TextView sotwLabel; // SOTW is for "side of the world"
private float currentAzimuth;
private SOTWFormatter sotwFormatter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compass);
sotwFormatter = new SOTWFormatter(this);
arrowView = findViewById(R.id.main_image_hands);
sotwLabel = findViewById(R.id.sotw_label);
setupCompass();
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "start compass");
compass.start();
}
@Override
protected void onPause() {
super.onPause();
compass.stop();
}
@Override
protected void onResume() {
super.onResume();
compass.start();
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "stop compass");
compass.stop();
}
private void setupCompass() {
compass = new Compass(this);
Compass.CompassListener cl = getCompassListener();
compass.setListener(cl);
}
private void adjustArrow(float azimuth) {
Log.d(TAG, "will set rotation from " + currentAzimuth + " to "
+ azimuth);
Animation an = new RotateAnimation(-currentAzimuth, -azimuth,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
currentAzimuth = azimuth;
an.setDuration(500);
an.setRepeatCount(0);
an.setFillAfter(true);
arrowView.startAnimation(an);
}
private void adjustSotwLabel(float azimuth) {
sotwLabel.setText(sotwFormatter.format(azimuth));
}
private Compass.CompassListener getCompassListener() {
return new Compass.CompassListener() {
@Override
public void onNewAzimuth(final float azimuth) {
// UI updates only in UI thread
// https://stackoverflow.com/q/11140285/444966
runOnUiThread(new Runnable() {
@Override
public void run() {
adjustArrow(azimuth);
adjustSotwLabel(azimuth);
}
});
}
};
}
}
The last part is the Compass class, which is the following:
package com.nativeview;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
public class Compass implements SensorEventListener {
private static final String TAG = "Compass";
public interface CompassListener {
void onNewAzimuth(float azimuth);
}
private CompassListener listener;
private SensorManager sensorManager;
private Sensor gsensor;
private Sensor msensor;
private float[] mGravity = new float[3];
private float[] mGeomagnetic = new float[3];
private float[] R = new float[9];
private float[] I = new float[9];
private float azimuth;
private float azimuthFix;
public Compass(Context context) {
sensorManager = (SensorManager) context
.getSystemService(Context.SENSOR_SERVICE);
gsensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
msensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
}
public void start() {
sensorManager.registerListener(this, gsensor,
SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(this, msensor,
SensorManager.SENSOR_DELAY_GAME);
}
public void stop() {
sensorManager.unregisterListener(this);
}
public void setAzimuthFix(float fix) {
azimuthFix = fix;
}
public void resetAzimuthFix() {
setAzimuthFix(0);
}
public void setListener(CompassListener l) {
listener = l;
}
@Override
public void onSensorChanged(SensorEvent event) {
final float alpha = 0.97f;
synchronized (this) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mGravity[0] = alpha * mGravity[0] + (1 - alpha)
* event.values[0];
mGravity[1] = alpha * mGravity[1] + (1 - alpha)
* event.values[1];
mGravity[2] = alpha * mGravity[2] + (1 - alpha)
* event.values[2];
// mGravity = event.values;
// Log.e(TAG, Float.toString(mGravity[0]));
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
// mGeomagnetic = event.values;
mGeomagnetic[0] = alpha * mGeomagnetic[0] + (1 - alpha)
* event.values[0];
mGeomagnetic[1] = alpha * mGeomagnetic[1] + (1 - alpha)
* event.values[1];
mGeomagnetic[2] = alpha * mGeomagnetic[2] + (1 - alpha)
* event.values[2];
// Log.e(TAG, Float.toString(event.values[0]));
}
boolean success = SensorManager.getRotationMatrix(R, I, mGravity,
mGeomagnetic);
if (success) {
float orientation[] = new float[3];
SensorManager.getOrientation(R, orientation);
// Log.d(TAG, "azimuth (rad): " + azimuth);
azimuth = (float) Math.toDegrees(orientation[0]); // orientation
azimuth = (azimuth + azimuthFix + 360) % 360;
// Log.d(TAG, "azimuth (deg): " + azimuth);
if (listener != null) {
listener.onNewAzimuth(azimuth);
}
}
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
I don't really know if this is possible and what strategy works the best, I would appreciate it if someone could point me in the right direction.