Reputation: 83
I am trying to implement a contrast changing image view in which the contrast of an image is changed by a slider and then the image can be saved by the click of a button. Upon saving i want a callback of the name onSave to be called by android, so that the file path can be returned. To this end I have created a Contrast Changing Image View in JS -
import React, {Component} from 'react';
let ReactNative = require('react-native');
let {View,
Image,
requireNativeComponent,
DeviceEventEmitter,
UIManager,
ViewPropTypes} = ReactNative;
import PropTypes from 'prop-types';
import Slider from '@react-native-community/slider';
//https://productcrafters.io/blog/creating-custom-react-native-ui-components-android/
//https://stackoverflow.com/questions/34739670/creating-custom-ui-component-for-android-on-react-native-how-to-send-data-to-js/44207488#44207488
class ContrastEditor extends Component{
constructor(props){
super(props);
this.state = {
constrast : 1,
};
this.onValueChange = this.onValueChange.bind(this);
this.onSave = this.onSave.bind(this);
this.onReset = this.onReset.bind(this);
}
onSave(event) {
console.log(event);
if(event.nativeEvent.fileName){
if (!this.props.onSave) {
return;
}
console.log('test');
this.props.onSave({
fileName: event.nativeEvent.fileName,
saveStatus: event.nativeEvent.saveStatus,
});
}
}
onReset(event){
console.log(event);
if(event.nativeEvent.resetStatus){
if(!this.props.onReset){
return;
}
console.log('test2');
this.props.onReset({resetStatus});
}
}
onValueChange(value){
//this.setState({contrast : value});
}
onSlidingComplete(value){
this.setState({contrast : value});
}
saveImage() {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this.view),
UIManager.getViewManagerConfig('RNContrastChangingImageView').Commands.save,
[],
);
}
resetImage() {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this.view),
UIManager.getViewManagerConfig('RNContrastChangingImageView').Commands.reset,
[],
);
}
render(){
return(
<View
style = {{
flex : 1,
}}>
<RNContrastChangingImageView
ref = {(ref)=>{this.view = ref;}}
style = {{
flex : 1
}}
source = {this.props.source}
contrast = {this.state.contrast}
resizeMode = {'contain'}
onSave={this.onSave}
onReset={this.onReset}/>
<Slider
minimumValue = {-1}
maximumValue = {3}
onValueChange = {
(value)=>{
this.onValueChange(value)
}
}
onSlidingComplete = {
(value)=>{
this.onSlidingComplete(value)
}
}/>
</View>
);
}
}
ContrastEditor.propTypes = {
onSave: PropTypes.func,
onReset : PropTypes.func,
source: PropTypes.string.isRequired,
resizeMode: PropTypes.oneOf(['contain', 'cover', 'stretch']),
}
ContrastEditor.defaultProps = {
resizeMode: 'contain',
onSave : ()=>{},
onReset : ()=>{},
}
let RNContrastChangingImageView = requireNativeComponent(
'RNContrastChangingImageView',
ContrastEditor,
{nativeOnly: { onSave: true, onReset : true}}
);
export default ContrastEditor;
The following is the ViewManager class-
package com.reactcontrastimagelibrary;
import java.util.Map;
import javax.annotation.Nullable;
import android.util.Log;
import android.content.Context;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
//Tutorial on RN bridge https://itnext.io/how-to-build-react-native-bridge-and-get-pdf-viewer-44614f11e08e
public class RNContrastChangingImageManager extends SimpleViewManager<RNContrastChangingImageView> {
private static final String TAG = "ContrastEditor";
public static final int COMMAND_SAVE_IMAGE = 1;
public static final int COMMAND_RESET_IMAGE = 2;
private Context mContext;
private RNContrastChangingImageView view = null;
@Override
public String getName() {
return "RNContrastChangingImageView";
}
public RNContrastChangingImageManager(ReactApplicationContext reactContext) {
mContext = reactContext;
}
@Override
protected RNContrastChangingImageView createViewInstance(ThemedReactContext reactContext) {
if(view == null){
view = new RNContrastChangingImageView(reactContext);
}
return view;
}
@ReactProp(name = "source")
public void setImageUri(RNContrastChangingImageView view, String imgUrl) {
view.setImageUri(imgUrl);
}
@ReactProp(name = "contrast", defaultFloat = 1f)
public void setContrastValue(RNContrastChangingImageView view, float contrast) {
view.setContrast(contrast);
}
@ReactProp(name = "resizeMode")
public void setResizeMode(RNContrastChangingImageView view, String mode) {
view.setResizeMode(mode);
}
@Override
public Map<String,Integer> getCommandsMap() {
Log.d("React"," View manager getCommandsMap:");
return MapBuilder.of(
"save",
COMMAND_SAVE_IMAGE,
"reset",
COMMAND_RESET_IMAGE);
}
@Override
public void receiveCommand(
RNContrastChangingImageView view,
int commandType,
@Nullable ReadableArray args) {
Assertions.assertNotNull(view);
Assertions.assertNotNull(args);
switch (commandType) {
case COMMAND_SAVE_IMAGE: {
Log.d(TAG, "Command called");
view.save();
return;
}
case COMMAND_RESET_IMAGE: {
view.reset();
return;
}
default:
throw new IllegalArgumentException(String.format(
"Unsupported command %d received by %s.",
commandType,
getClass().getSimpleName()));
}
}
@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"save",
MapBuilder.of("registrationName", "onSave"),
"reset",
MapBuilder.of("registrationName", "onReset"))
);
}
}
This is the class for my View-
package com.reactcontrastimagelibrary;
import android.content.Context;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.ParcelFileDescriptor;
import android.net.Uri;
import android.util.Log;
import android.app.Activity;
import androidx.appcompat.widget.AppCompatImageView;
import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.concurrent.ExecutionException;
import java.util.UUID;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class RNContrastChangingImageView extends AppCompatImageView {
public static final String TAG = "ContrastEditor";
private String cacheFolderName = "RNContrastChangingImage";
private Bitmap initialData = null;
private Bitmap imageData = null;
private String imageUri = null;
private double contrast = 1;
protected static Context mContext;
public static RNContrastChangingImageView instance = null;
public String fileName = null;
public RNContrastChangingImageView(Context context) {
super(context);
mContext = context;
//createInstance(context, activity);
}
public static RNContrastChangingImageView getInstance() {
return instance;
}
public static void createInstance(Context context) {
mContext = context;
instance = new RNContrastChangingImageView(context);
}
public void setImageUri(String imgUri) {
Log.d(TAG, "set image");
Log.d(TAG, "image source : " + imgUri);
if (imgUri != this.imageUri) {
this.imageUri = imgUri;
try{
File imgFile = new File(imgUri);
Bitmap bitmap = BitmapFactory.decodeStream(
new FileInputStream(imgFile)
);
Log.d(TAG, "set image source");
this.imageData = bitmap;
this.initialData = bitmap;
this.setImageBitmap(bitmap);
} catch(FileNotFoundException e) {
e.printStackTrace();
}
}
}
public void setContrast(double contrastVal) {
this.contrast = contrastVal;
if (this.imageData != null) {
this.updateImageContrast();
}
}
public void setResizeMode(String mode) {
switch (mode) {
case "cover":
this.setScaleType(ScaleType.CENTER_CROP);
break;
case "stretch":
this.setScaleType(ScaleType.FIT_XY);
break;
case "contain":
default:
this.setScaleType(ScaleType.FIT_CENTER);
break;
}
}
private String generateStoredFileName() throws Exception {
String folderDir = this.mContext.getCacheDir().toString();
File folder = new File( folderDir + "/" + this.cacheFolderName);
if (!folder.exists()) {
boolean result = folder.mkdirs();
if (result) {
Log.d(TAG, "wrote: created folder " + folder.getPath());
} else {
Log.d(TAG, "Not possible to create folder");
throw new Exception("Failed to create the cache directory");
}
}
return folderDir + "/" + this.cacheFolderName + "/" + "contrast_editted" + UUID.randomUUID() + ".png";
}
private void updateImageContrast() {
try {
Mat matImage = new Mat();
Utils.bitmapToMat(this.initialData, matImage);
Scalar imgScalVec = Core.sumElems(matImage);
double[] imgAvgVec = imgScalVec.val;
for (int i = 0; i < imgAvgVec.length; i++) {
imgAvgVec[i] = imgAvgVec[i] / (matImage.cols() * matImage.rows());
}
double imgAvg = (imgAvgVec[0] + imgAvgVec[1] + imgAvgVec[2]) / 3;
int brightness = -(int) ((this.contrast - 1) * imgAvg);
matImage.convertTo(matImage, matImage.type(), this.contrast, brightness);
Bitmap resultImage = Bitmap.createBitmap(
this.imageData.getWidth(),
this.imageData.getHeight(),
this.imageData.getConfig()
);
Utils.matToBitmap(matImage, resultImage);
this.imageData = resultImage;
this.setImageBitmap(resultImage);
} catch (Exception e) {
e.printStackTrace();
}
}
public void reset(){
this.contrast = 1;
this.setImageBitmap(this.initialData);
WritableMap event = Arguments.createMap();
event.putString("resetStatus", "success");
event.putString("action", "reset");
final ReactContext reactContext = (ReactContext) this.getContext();
reactContext.getJSModule(
RCTEventEmitter.class
).receiveEvent(
getId(),
"reset",
event
);
return;
}
public void save(){
String fileName = null;
try{
fileName = generateStoredFileName();
}
catch (Exception e){
Log.d(TAG, "failed to create folder");
}
Mat matImage = new Mat();
Utils.bitmapToMat(this.imageData, matImage);
boolean success = Imgcodecs.imwrite(fileName, matImage);
matImage.release();
WritableMap event = Arguments.createMap();
if(success){
Log.d(TAG, "image saved, fileName: "+fileName);
event.putString("fileName", fileName);
event.putString("saveStatus", "success");
event.putString("action", "save");
} else {
event.putString("fileName", "");
event.putString("saveStatus", "failure");
event.putString("action", "save");
}
ReactContext reactContext = (ReactContext) this.getContext();
reactContext.getJSModule(
RCTEventEmitter.class
).receiveEvent(
getId(),
"save",
event
);
return;
}
}
The methods of significance are
I have followed this answer given here-Creating Custom UI component for android on React Native. How to send data to JS? But I am unable to make onSave and onReset callbacks work and there is a literally no documentation on how to create events with custom names in React-Native. I also tried making the example given in the documentation, by using the topChange event, but that also does not seem to work for some reason. Can anyone point out what is my mistake?
Upvotes: 2
Views: 1194
Reputation: 11
Have you tried to log getId() in your reset function for instance? If it's -1 it means you are not calling it on the actual view. To test if your java/js wiring is working try testing an event in one of the manager functions, which get the actual view as a parameter
Upvotes: 1