├── .gitignore ├── LICENSE ├── README.md ├── android ├── android.iml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── reactlibrary │ ├── RNTScratchViewManager.java │ ├── ScratchView.java │ └── ScratchViewPackage.java ├── index.js ├── ios ├── RNScratch.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── RNTScratchViewManager.h ├── RNTScratchViewManager.m ├── ScratchView.h ├── ScratchView.m ├── ScratchViewDelegate.h ├── ScratchViewTools.h └── ScratchViewTools.m ├── package.json └── src └── ScratchView.js /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConduitMobileRND/react-native-scratch/7bf57e76d8a3801c793cb8c2b4628560c7175177/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Como R&D 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-scratch 3 | 4 | ## Getting started 5 | 6 | `$ npm install react-native-scratch --save` 7 | 8 | ### Mostly automatic installation 9 | 10 | `$ react-native link react-native-scratch` 11 | 12 | ### Manual installation 13 | 14 | 15 | #### iOS 16 | 17 | 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` 18 | 2. Go to `node_modules` ➜ `react-native-scratch` and add `RNScratch.xcodeproj` 19 | 3. In XCode, in the project navigator, select your project. Add `libRNScratch.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 20 | 4. Run your project (`Cmd+R`)< 21 | 22 | #### Android 23 | 24 | 1. Open up `android/app/src/main/java/[...]/MainActivity.java` 25 | - Add `import com.como.RNTScratchView.ScratchViewPackage;` to the imports at the top of the file 26 | - Add `new ScratchViewPackage()` to the list returned by the `getPackages()` method 27 | 2. Append the following lines to `android/settings.gradle`: 28 | ``` 29 | include ':react-native-scratch' 30 | project(':react-native-scratch').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-scratch/android') 31 | ``` 32 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 33 | ``` 34 | implementation project(':react-native-scratch') 35 | ``` 36 | 37 | 38 | ## Usage 39 | 40 | The ScratchView will fill its containing view and cover all other content untill you scratch it 41 | Just put it as the last component in your view 42 | ```javascript 43 | import React, { Component } from 'react'; 44 | import { View } from 'react-native'; 45 | import ScratchView from 'react-native-scratch' 46 | 47 | class MyView extends Component { 48 | 49 | onImageLoadFinished = ({ id, success }) => { 50 | // Do something 51 | } 52 | 53 | onScratchProgressChanged = ({ value, id }) => { 54 | // Do domething like showing the progress to the user 55 | } 56 | 57 | onScratchDone = ({ isScratchDone, id }) => { 58 | // Do something 59 | } 60 | 61 | onScratchTouchStateChanged = ({ id, touchState }) => { 62 | // Example: change a state value to stop a containing 63 | // FlatList from scrolling while scratching 64 | this.setState({ scrollEnabled: !touchState }); 65 | } 66 | 67 | render() { 68 | return ( 69 | // will be covered by the ScratchView 70 | // will be covered by the ScratchView 71 | } 85 | ) 86 | } 87 | 88 | export default MyView; 89 | ``` 90 | -------------------------------------------------------------------------------- /android/android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | def DEFAULT_COMPILE_SDK_VERSION = 28 4 | def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" 5 | def DEFAULT_TARGET_SDK_VERSION = 16 6 | 7 | android { 8 | compileSdkVersion project.hasProperty('compileSdkVersion') ? project.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION 9 | buildToolsVersion project.hasProperty('buildToolsVersion') ? project.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION 10 | 11 | defaultConfig { 12 | minSdkVersion 16 13 | targetSdkVersion project.hasProperty('targetSdkVersion') ? project.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation 'com.facebook.react:react-native:+' 19 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConduitMobileRND/react-native-scratch/7bf57e76d8a3801c793cb8c2b4628560c7175177/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 19 17:13:51 IST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/RNTScratchViewManager.java: -------------------------------------------------------------------------------- 1 | package com.como.RNTScratchView; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import com.facebook.react.bridge.ReadableArray; 6 | import com.facebook.react.common.MapBuilder; 7 | import com.facebook.react.uimanager.SimpleViewManager; 8 | import com.facebook.react.uimanager.ThemedReactContext; 9 | import com.facebook.react.uimanager.annotations.ReactProp; 10 | 11 | import java.util.Map; 12 | 13 | public class RNTScratchViewManager extends SimpleViewManager { 14 | public static final String REACT_CLASS = "RNTScratchView"; 15 | public static final String EVENT_IMAGE_LOAD = "onImageLoadFinished"; 16 | public static final String EVENT_TOUCH_STATE_CHANGED = "onTouchStateChanged"; 17 | public static final String EVENT_SCRATCH_PROGRESS_CHANGED = "onScratchProgressChanged"; 18 | public static final String EVENT_SCRATCH_DONE = "onScratchDone"; 19 | 20 | @ReactProp(name = "placeholderColor") 21 | public void setPlaceholderColor(final ScratchView scratchView, @Nullable String placeholderColor) { 22 | if (scratchView != null) { 23 | scratchView.setPlaceholderColor(placeholderColor); 24 | } 25 | } 26 | 27 | @ReactProp(name = "threshold") 28 | public void setThreshold(final ScratchView scratchView, float threshold) { 29 | if (scratchView != null) { 30 | scratchView.setThreshold(threshold); 31 | } 32 | } 33 | 34 | @ReactProp(name = "brushSize") 35 | public void setBrushSize(final ScratchView scratchView, float brushSize) { 36 | if (scratchView != null) { 37 | scratchView.setBrushSize(brushSize); 38 | } 39 | } 40 | 41 | @ReactProp(name = "imageUrl") 42 | public void setImageUrl(final ScratchView scratchView, @Nullable String imageUrl) { 43 | if (scratchView != null) { 44 | scratchView.setImageUrl(imageUrl); 45 | } 46 | } 47 | 48 | @ReactProp(name = "resourceName") 49 | public void setResourceName(final ScratchView scratchView, @Nullable String resourceName) { 50 | if (scratchView != null) { 51 | scratchView.setResourceName(resourceName); 52 | } 53 | } 54 | 55 | @ReactProp(name = "localImageName") // deprecated 56 | public void setLocalImageName(final ScratchView scratchView, @Nullable String localImageName) { 57 | if (scratchView != null) { 58 | scratchView.setResourceName(localImageName); 59 | } 60 | } 61 | 62 | @ReactProp(name = "resizeMode") 63 | public void setResizeMode(final ScratchView scratchView, @Nullable String resizeMode) { 64 | if (scratchView != null) { 65 | scratchView.setResizeMode(resizeMode); 66 | } 67 | } 68 | 69 | @Override 70 | public String getName() { 71 | return REACT_CLASS; 72 | } 73 | 74 | @Override 75 | public ScratchView createViewInstance(ThemedReactContext context) { 76 | return new ScratchView(context); 77 | } 78 | 79 | @javax.annotation.Nullable 80 | @Override 81 | public Map getCommandsMap() { 82 | return MapBuilder.of("reset", 0); 83 | } 84 | 85 | @Override 86 | public void receiveCommand(ScratchView view, int commandId, @javax.annotation.Nullable ReadableArray args) { 87 | super.receiveCommand(view, commandId, args); 88 | if (commandId == 0) { 89 | view.reset(); 90 | } 91 | } 92 | 93 | public Map getExportedCustomBubblingEventTypeConstants() { 94 | return MapBuilder.builder() 95 | .put(EVENT_IMAGE_LOAD, 96 | MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", EVENT_IMAGE_LOAD))) 97 | .put(EVENT_TOUCH_STATE_CHANGED, 98 | MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", EVENT_TOUCH_STATE_CHANGED))) 99 | .put(EVENT_SCRATCH_PROGRESS_CHANGED, 100 | MapBuilder.of("phasedRegistrationNames", 101 | MapBuilder.of("bubbled", EVENT_SCRATCH_PROGRESS_CHANGED))) 102 | .put(EVENT_SCRATCH_DONE, 103 | MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", EVENT_SCRATCH_DONE))) 104 | .build(); 105 | } 106 | } -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/ScratchView.java: -------------------------------------------------------------------------------- 1 | package com.como.RNTScratchView; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.Path; 10 | import android.graphics.PorterDuff; 11 | import android.graphics.PorterDuffXfermode; 12 | import java.util.ArrayList; 13 | import android.graphics.Rect; 14 | import androidx.annotation.Nullable; 15 | import android.util.AttributeSet; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.ViewTreeObserver; 19 | 20 | import com.facebook.react.bridge.Arguments; 21 | import com.facebook.react.bridge.ReactContext; 22 | import com.facebook.react.bridge.WritableMap; 23 | import com.facebook.react.uimanager.events.RCTEventEmitter; 24 | 25 | import java.io.InputStream; 26 | import java.net.URL; 27 | 28 | public class ScratchView extends View implements View.OnTouchListener { 29 | boolean imageTakenFromView = false; 30 | float threshold = 0; 31 | float brushSize = 0; 32 | String imageUrl = null; 33 | String resourceName = null; 34 | String resizeMode = "stretch"; 35 | Bitmap image; 36 | Path path; 37 | float minDimension; 38 | float gridSize; 39 | ArrayList> grid; 40 | boolean cleared; 41 | int clearPointsCounter; 42 | float scratchProgress; 43 | int placeholderColor = -1; 44 | 45 | Paint imagePaint = new Paint(); 46 | Paint pathPaint = new Paint(); 47 | 48 | boolean inited = false; 49 | 50 | Rect imageRect = null; 51 | 52 | public ScratchView(Context context) { 53 | super(context); 54 | init(); 55 | } 56 | 57 | public ScratchView(Context context, AttributeSet attrs) { 58 | super(context, attrs); 59 | init(); 60 | } 61 | 62 | public ScratchView(Context context, AttributeSet attrs, int defStyle) { 63 | super(context, attrs, defStyle); 64 | init(); 65 | } 66 | 67 | private void init() { 68 | setFocusable(true); 69 | setFocusableInTouchMode(true); 70 | setOnTouchListener(this); 71 | 72 | imagePaint.setAntiAlias(true); 73 | imagePaint.setFilterBitmap(true); 74 | 75 | pathPaint.setAlpha(0); 76 | pathPaint.setStyle(Paint.Style.STROKE); 77 | pathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 78 | pathPaint.setAntiAlias(true); 79 | 80 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 81 | } 82 | 83 | public void setPlaceholderColor(@Nullable String placeholderColor) { 84 | if (placeholderColor != null) { 85 | try { 86 | this.placeholderColor = Color.parseColor(placeholderColor); 87 | } catch (Exception ex) { 88 | ex.printStackTrace(); 89 | } 90 | } 91 | } 92 | 93 | public void setThreshold(float threshold) { 94 | this.threshold = threshold; 95 | } 96 | 97 | public void setBrushSize(float brushSize) { 98 | this.brushSize = brushSize * 3; 99 | } 100 | 101 | public void setImageUrl(String imageUrl) { 102 | this.imageUrl = imageUrl; 103 | } 104 | 105 | public void setResourceName(String resourceName) { 106 | this.resourceName = resourceName; 107 | } 108 | 109 | public void setResizeMode(String resizeMode) { 110 | if (resizeMode != null) { 111 | this.resizeMode = resizeMode.toLowerCase(); 112 | } 113 | } 114 | 115 | private void loadImage() { 116 | path = null; 117 | if (imageUrl != null) { 118 | Thread thread = new Thread(new Runnable() { 119 | @Override 120 | public void run() { 121 | try { 122 | InputStream is = (InputStream) new URL(imageUrl).getContent(); 123 | image = BitmapFactory.decodeStream(is).copy(Bitmap.Config.ARGB_8888, true); 124 | reportImageLoadFinished(true); 125 | invalidate(); 126 | 127 | } catch (Exception e) { 128 | reportImageLoadFinished(false); 129 | e.printStackTrace(); 130 | } 131 | } 132 | }); 133 | thread.start(); 134 | } else if (resourceName != null) { 135 | int imageResourceId = getResources().getIdentifier(resourceName, "drawable", getContext().getPackageName()); 136 | image = BitmapFactory.decodeResource(getContext().getResources(), imageResourceId); 137 | reportImageLoadFinished(true); 138 | invalidate(); 139 | } 140 | } 141 | 142 | public void reset() { 143 | minDimension = getWidth() > getHeight() ? getHeight() : getWidth(); 144 | brushSize = brushSize > 0 ? brushSize : (minDimension / 10.0f); 145 | brushSize = Math.max(1, Math.min(100, brushSize)); 146 | threshold = threshold > 0 ? threshold : 50; 147 | 148 | loadImage(); 149 | initGrid(); 150 | reportScratchProgress(); 151 | reportScratchState(); 152 | } 153 | 154 | public void initGrid() { 155 | gridSize = (float) Math.max(Math.min(Math.ceil(minDimension / brushSize), 29), 9); 156 | 157 | grid = new ArrayList(); 158 | for (int x = 0; x < gridSize; x++) { 159 | grid.add(new ArrayList()); 160 | for (int y = 0; y < gridSize; y++) { 161 | grid.get(x).add(true); 162 | } 163 | } 164 | clearPointsCounter = 0; 165 | cleared = false; 166 | scratchProgress = 0; 167 | } 168 | 169 | public void updateGrid(int x, int y) { 170 | float viewWidth = getWidth(); 171 | float viewHeight = getHeight(); 172 | int pointInGridX = Math.round((Math.max(Math.min(x, viewWidth), 0) / viewWidth) * (gridSize - 1.0f)); 173 | int pointInGridY = Math.round((Math.max(Math.min(y, viewHeight), 0) / viewHeight) * (gridSize - 1.0f)); 174 | if (grid.get(pointInGridX).get(pointInGridY) == true) { 175 | grid.get(pointInGridX).set(pointInGridY, false); 176 | clearPointsCounter++; 177 | scratchProgress = ((float) clearPointsCounter) / (gridSize * gridSize) * 100.0f; 178 | reportScratchProgress(); 179 | if (!cleared && scratchProgress > threshold) { 180 | cleared = true; 181 | reportScratchState(); 182 | } 183 | } 184 | } 185 | 186 | public void reportImageLoadFinished(boolean success) { 187 | final Context context = getContext(); 188 | if (context instanceof ReactContext) { 189 | WritableMap event = Arguments.createMap(); 190 | event.putBoolean("success", success); 191 | ((ReactContext) context).getJSModule(RCTEventEmitter.class).receiveEvent(getId(), 192 | RNTScratchViewManager.EVENT_IMAGE_LOAD, event); 193 | } 194 | } 195 | 196 | public void reportTouchState(boolean state) { 197 | final Context context = getContext(); 198 | if (context instanceof ReactContext) { 199 | WritableMap event = Arguments.createMap(); 200 | event.putBoolean("touchState", state); 201 | ((ReactContext) context).getJSModule(RCTEventEmitter.class).receiveEvent(getId(), 202 | RNTScratchViewManager.EVENT_TOUCH_STATE_CHANGED, event); 203 | } 204 | } 205 | 206 | public void reportScratchProgress() { 207 | final Context context = getContext(); 208 | if (context instanceof ReactContext) { 209 | WritableMap event = Arguments.createMap(); 210 | event.putDouble("progressValue", Math.round(scratchProgress * 100.0f) / 100.0); 211 | ((ReactContext) context).getJSModule(RCTEventEmitter.class).receiveEvent(getId(), 212 | RNTScratchViewManager.EVENT_SCRATCH_PROGRESS_CHANGED, event); 213 | } 214 | } 215 | 216 | public void reportScratchState() { 217 | final Context context = getContext(); 218 | if (context instanceof ReactContext) { 219 | WritableMap event = Arguments.createMap(); 220 | event.putBoolean("isScratchDone", cleared); 221 | ((ReactContext) context).getJSModule(RCTEventEmitter.class).receiveEvent(getId(), 222 | RNTScratchViewManager.EVENT_SCRATCH_DONE, event); 223 | } 224 | } 225 | 226 | @Override 227 | protected void onDraw(Canvas canvas) { 228 | if (!inited && getWidth() > 0) { 229 | inited = true; 230 | reset(); 231 | } 232 | 233 | if (!imageTakenFromView) { 234 | canvas.drawColor(this.placeholderColor != -1 ? this.placeholderColor : Color.GRAY); 235 | } 236 | 237 | if (image == null) { 238 | return; 239 | } 240 | 241 | if (imageRect == null) { 242 | int offsetX = 0; 243 | int offsetY = 0; 244 | float viewWidth = (float) getWidth(); 245 | float viewHeight = (float) getHeight(); 246 | float imageAspect = (float) image.getWidth() / (float) image.getHeight(); 247 | float viewAspect = viewWidth / viewHeight; 248 | switch (resizeMode) { 249 | case "cover": 250 | if (imageAspect > viewAspect) { 251 | offsetX = (int) (((viewHeight * imageAspect) - viewWidth) / 2.0f); 252 | } else { 253 | offsetY = (int) (((viewWidth / imageAspect) - viewHeight) / 2.0f); 254 | } 255 | break; 256 | case "contain": 257 | if (imageAspect < viewAspect) { 258 | offsetX = (int) (((viewHeight * imageAspect) - viewWidth) / 2.0f); 259 | } else { 260 | offsetY = (int) (((viewWidth / imageAspect) - viewHeight) / 2.0f); 261 | } 262 | break; 263 | } 264 | imageRect = new Rect(-offsetX, -offsetY, getWidth() + offsetX, getHeight() + offsetY); 265 | } 266 | 267 | canvas.drawBitmap(image, new Rect(0, 0, image.getWidth(), image.getHeight()), imageRect, imagePaint); 268 | 269 | if (path != null) { 270 | canvas.drawPath(path, pathPaint); 271 | } 272 | } 273 | 274 | @Override 275 | public boolean onTouch(View view, MotionEvent motionEvent) { 276 | int x = (int) motionEvent.getX(); 277 | int y = (int) motionEvent.getY(); 278 | 279 | switch (motionEvent.getAction()) { 280 | case MotionEvent.ACTION_DOWN: 281 | image = createBitmapFromView(); 282 | reportTouchState(true); 283 | float strokeWidth = brushSize > 0 ? brushSize 284 | : ((getHeight() < getWidth() ? getHeight() : getWidth()) / 10f); 285 | imageRect = new Rect(0, 0, getWidth(), getHeight()); 286 | pathPaint.setStrokeWidth(strokeWidth); 287 | path = new Path(); 288 | path.moveTo(x, y); 289 | break; 290 | case MotionEvent.ACTION_MOVE: 291 | if (path != null) { 292 | path.lineTo(x, y); 293 | updateGrid(x, y); 294 | } 295 | break; 296 | case MotionEvent.ACTION_CANCEL: 297 | case MotionEvent.ACTION_UP: 298 | reportTouchState(false); 299 | image = createBitmapFromView(); 300 | path = null; 301 | break; 302 | } 303 | invalidate(); 304 | return true; 305 | } 306 | 307 | public Bitmap createBitmapFromView() { 308 | Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 309 | Canvas c = new Canvas(bitmap); 310 | draw(c); 311 | imageTakenFromView = true; 312 | return bitmap; 313 | } 314 | } -------------------------------------------------------------------------------- /android/src/main/java/com/reactlibrary/ScratchViewPackage.java: -------------------------------------------------------------------------------- 1 | package com.como.RNTScratchView; 2 | 3 | import android.app.Activity; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.JavaScriptModule; 7 | import com.facebook.react.bridge.NativeModule; 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.facebook.react.uimanager.ViewManager; 10 | 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | public class ScratchViewPackage implements ReactPackage { 16 | 17 | @Override 18 | public List createNativeModules(ReactApplicationContext reactContext) { 19 | return Arrays.asList(); 20 | } 21 | 22 | @Override 23 | public List createViewManagers(ReactApplicationContext reactContext) { 24 | return Arrays.asList(new RNTScratchViewManager()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ScratchView = require('./src/ScratchView'); 2 | 3 | module.exports = ScratchView; 4 | -------------------------------------------------------------------------------- /ios/RNScratch.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2946D6D022413E0A00BB98A4 /* RNTScratchViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2946D6C922413E0A00BB98A4 /* RNTScratchViewManager.m */; }; 11 | 2946D6D122413E0A00BB98A4 /* ScratchViewTools.m in Sources */ = {isa = PBXBuildFile; fileRef = 2946D6CB22413E0A00BB98A4 /* ScratchViewTools.m */; }; 12 | 2946D6D222413E0A00BB98A4 /* ScratchView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2946D6CE22413E0A00BB98A4 /* ScratchView.m */; }; 13 | 2946D72322414D5700BB98A4 /* libRNScratch.a in Sources */ = {isa = PBXBuildFile; fileRef = 134814201AA4EA6300B7C361 /* libRNScratch.a */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXCopyFilesBuildPhase section */ 17 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 18 | isa = PBXCopyFilesBuildPhase; 19 | buildActionMask = 2147483647; 20 | dstPath = "include/$(PRODUCT_NAME)"; 21 | dstSubfolderSpec = 16; 22 | files = ( 23 | ); 24 | runOnlyForDeploymentPostprocessing = 0; 25 | }; 26 | /* End PBXCopyFilesBuildPhase section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 134814201AA4EA6300B7C361 /* libRNScratch.a */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = sourcecode.make; path = libRNScratch.a; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 2946D6C922413E0A00BB98A4 /* RNTScratchViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTScratchViewManager.m; sourceTree = ""; }; 31 | 2946D6CA22413E0A00BB98A4 /* ScratchViewTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScratchViewTools.h; sourceTree = ""; }; 32 | 2946D6CB22413E0A00BB98A4 /* ScratchViewTools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScratchViewTools.m; sourceTree = ""; }; 33 | 2946D6CC22413E0A00BB98A4 /* RNTScratchViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNTScratchViewManager.h; sourceTree = ""; }; 34 | 2946D6CD22413E0A00BB98A4 /* ScratchView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScratchView.h; sourceTree = ""; }; 35 | 2946D6CE22413E0A00BB98A4 /* ScratchView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScratchView.m; sourceTree = ""; }; 36 | 2946D6CF22413E0A00BB98A4 /* ScratchViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScratchViewDelegate.h; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 134814211AA4EA7D00B7C361 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 134814201AA4EA6300B7C361 /* libRNScratch.a */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 58B511D21A9E6C8500147676 = { 59 | isa = PBXGroup; 60 | children = ( 61 | 2946D6CC22413E0A00BB98A4 /* RNTScratchViewManager.h */, 62 | 2946D6C922413E0A00BB98A4 /* RNTScratchViewManager.m */, 63 | 2946D6CD22413E0A00BB98A4 /* ScratchView.h */, 64 | 2946D6CE22413E0A00BB98A4 /* ScratchView.m */, 65 | 2946D6CF22413E0A00BB98A4 /* ScratchViewDelegate.h */, 66 | 2946D6CA22413E0A00BB98A4 /* ScratchViewTools.h */, 67 | 2946D6CB22413E0A00BB98A4 /* ScratchViewTools.m */, 68 | 134814211AA4EA7D00B7C361 /* Products */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | /* End PBXGroup section */ 73 | 74 | /* Begin PBXNativeTarget section */ 75 | 58B511DA1A9E6C8500147676 /* RNScratch */ = { 76 | isa = PBXNativeTarget; 77 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNScratch" */; 78 | buildPhases = ( 79 | 58B511D71A9E6C8500147676 /* Sources */, 80 | 58B511D81A9E6C8500147676 /* Frameworks */, 81 | 58B511D91A9E6C8500147676 /* CopyFiles */, 82 | ); 83 | buildRules = ( 84 | ); 85 | dependencies = ( 86 | ); 87 | name = RNScratch; 88 | productName = RCTDataManager; 89 | productReference = 134814201AA4EA6300B7C361 /* libRNScratch.a */; 90 | productType = "com.apple.product-type.library.static"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | 58B511D31A9E6C8500147676 /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | LastUpgradeCheck = 0830; 99 | ORGANIZATIONNAME = Facebook; 100 | TargetAttributes = { 101 | 58B511DA1A9E6C8500147676 = { 102 | CreatedOnToolsVersion = 6.1.1; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNScratch" */; 107 | compatibilityVersion = "Xcode 3.2"; 108 | developmentRegion = English; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | ); 113 | mainGroup = 58B511D21A9E6C8500147676; 114 | productRefGroup = 58B511D21A9E6C8500147676; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 58B511DA1A9E6C8500147676 /* RNScratch */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXSourcesBuildPhase section */ 124 | 58B511D71A9E6C8500147676 /* Sources */ = { 125 | isa = PBXSourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | 2946D72322414D5700BB98A4 /* libRNScratch.a in Sources */, 129 | 2946D6D122413E0A00BB98A4 /* ScratchViewTools.m in Sources */, 130 | 2946D6D022413E0A00BB98A4 /* RNTScratchViewManager.m in Sources */, 131 | 2946D6D222413E0A00BB98A4 /* ScratchView.m in Sources */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXSourcesBuildPhase section */ 136 | 137 | /* Begin XCBuildConfiguration section */ 138 | 58B511ED1A9E6C8500147676 /* Debug */ = { 139 | isa = XCBuildConfiguration; 140 | buildSettings = { 141 | ALWAYS_SEARCH_USER_PATHS = NO; 142 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 143 | CLANG_CXX_LIBRARY = "libc++"; 144 | CLANG_ENABLE_MODULES = YES; 145 | CLANG_ENABLE_OBJC_ARC = YES; 146 | CLANG_WARN_BOOL_CONVERSION = YES; 147 | CLANG_WARN_CONSTANT_CONVERSION = YES; 148 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 149 | CLANG_WARN_EMPTY_BODY = YES; 150 | CLANG_WARN_ENUM_CONVERSION = YES; 151 | CLANG_WARN_INFINITE_RECURSION = YES; 152 | CLANG_WARN_INT_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 155 | CLANG_WARN_UNREACHABLE_CODE = YES; 156 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 157 | COPY_PHASE_STRIP = NO; 158 | ENABLE_STRICT_OBJC_MSGSEND = YES; 159 | ENABLE_TESTABILITY = YES; 160 | GCC_C_LANGUAGE_STANDARD = gnu99; 161 | GCC_DYNAMIC_NO_PIC = NO; 162 | GCC_NO_COMMON_BLOCKS = YES; 163 | GCC_OPTIMIZATION_LEVEL = 0; 164 | GCC_PREPROCESSOR_DEFINITIONS = ( 165 | "DEBUG=1", 166 | "$(inherited)", 167 | ); 168 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 169 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 170 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 171 | GCC_WARN_UNDECLARED_SELECTOR = YES; 172 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 173 | GCC_WARN_UNUSED_FUNCTION = YES; 174 | GCC_WARN_UNUSED_VARIABLE = YES; 175 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 176 | MTL_ENABLE_DEBUG_INFO = YES; 177 | ONLY_ACTIVE_ARCH = YES; 178 | SDKROOT = iphoneos; 179 | }; 180 | name = Debug; 181 | }; 182 | 58B511EE1A9E6C8500147676 /* Release */ = { 183 | isa = XCBuildConfiguration; 184 | buildSettings = { 185 | ALWAYS_SEARCH_USER_PATHS = NO; 186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 187 | CLANG_CXX_LIBRARY = "libc++"; 188 | CLANG_ENABLE_MODULES = YES; 189 | CLANG_ENABLE_OBJC_ARC = YES; 190 | CLANG_WARN_BOOL_CONVERSION = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INFINITE_RECURSION = YES; 196 | CLANG_WARN_INT_CONVERSION = YES; 197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | COPY_PHASE_STRIP = YES; 202 | ENABLE_NS_ASSERTIONS = NO; 203 | ENABLE_STRICT_OBJC_MSGSEND = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu99; 205 | GCC_NO_COMMON_BLOCKS = YES; 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 213 | MTL_ENABLE_DEBUG_INFO = NO; 214 | SDKROOT = iphoneos; 215 | VALIDATE_PRODUCT = YES; 216 | }; 217 | name = Release; 218 | }; 219 | 58B511F01A9E6C8500147676 /* Debug */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | HEADER_SEARCH_PATHS = ( 223 | "$(inherited)", 224 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 225 | "$(SRCROOT)/../../../React/**", 226 | "$(SRCROOT)/../../react-native/React/**", 227 | ); 228 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 229 | OTHER_LDFLAGS = "-ObjC"; 230 | PRODUCT_NAME = RNScratch; 231 | SKIP_INSTALL = YES; 232 | }; 233 | name = Debug; 234 | }; 235 | 58B511F11A9E6C8500147676 /* Release */ = { 236 | isa = XCBuildConfiguration; 237 | buildSettings = { 238 | HEADER_SEARCH_PATHS = ( 239 | "$(inherited)", 240 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 241 | "$(SRCROOT)/../../../React/**", 242 | "$(SRCROOT)/../../react-native/React/**", 243 | ); 244 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 245 | OTHER_LDFLAGS = "-ObjC"; 246 | PRODUCT_NAME = RNScratch; 247 | SKIP_INSTALL = YES; 248 | }; 249 | name = Release; 250 | }; 251 | /* End XCBuildConfiguration section */ 252 | 253 | /* Begin XCConfigurationList section */ 254 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNScratch" */ = { 255 | isa = XCConfigurationList; 256 | buildConfigurations = ( 257 | 58B511ED1A9E6C8500147676 /* Debug */, 258 | 58B511EE1A9E6C8500147676 /* Release */, 259 | ); 260 | defaultConfigurationIsVisible = 0; 261 | defaultConfigurationName = Release; 262 | }; 263 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNScratch" */ = { 264 | isa = XCConfigurationList; 265 | buildConfigurations = ( 266 | 58B511F01A9E6C8500147676 /* Debug */, 267 | 58B511F11A9E6C8500147676 /* Release */, 268 | ); 269 | defaultConfigurationIsVisible = 0; 270 | defaultConfigurationName = Release; 271 | }; 272 | /* End XCConfigurationList section */ 273 | }; 274 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 275 | } 276 | -------------------------------------------------------------------------------- /ios/RNScratch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/RNScratch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/RNTScratchViewManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "ScratchViewDelegate.h" 3 | 4 | @interface RNTScratchViewManager : RCTViewManager 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/RNTScratchViewManager.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import "RNTScratchViewManager.h" 7 | #import "ScratchView.h" 8 | 9 | @implementation RNTScratchViewManager 10 | 11 | @synthesize bridge = _bridge; 12 | 13 | RCT_EXPORT_MODULE(); 14 | RCT_EXPORT_VIEW_PROPERTY(placeholderColor, NSString) 15 | RCT_EXPORT_VIEW_PROPERTY(threshold, float) 16 | RCT_EXPORT_VIEW_PROPERTY(brushSize, float) 17 | RCT_EXPORT_VIEW_PROPERTY(imageUrl, NSString); 18 | RCT_EXPORT_VIEW_PROPERTY(resourceName, NSString); 19 | RCT_EXPORT_VIEW_PROPERTY(localImageName, NSString); // deprecated 20 | RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); 21 | RCT_EXPORT_VIEW_PROPERTY(onImageLoadFinished, RCTBubblingEventBlock); 22 | RCT_EXPORT_VIEW_PROPERTY(onTouchStateChanged, RCTBubblingEventBlock); 23 | RCT_EXPORT_VIEW_PROPERTY(onScratchProgressChanged, RCTBubblingEventBlock); 24 | RCT_EXPORT_VIEW_PROPERTY(onScratchDone, RCTBubblingEventBlock); 25 | 26 | RCT_EXPORT_METHOD(reset:(nonnull NSNumber *)reactTag) 27 | { 28 | [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { 29 | ScratchView *scratchView = (ScratchView*)viewRegistry[reactTag]; 30 | if ([scratchView isKindOfClass:[ScratchView class]]) { 31 | [scratchView reset]; 32 | } 33 | }]; 34 | } 35 | 36 | -(ScratchView *) view 37 | { 38 | ScratchView *scratchView = [[ScratchView alloc] init]; 39 | scratchView._delegate = self; 40 | return scratchView; 41 | } 42 | 43 | - (void)onImageLoadFinished:(ScratchView *)sender successState:(BOOL)success { 44 | if (sender.onImageLoadFinished) { 45 | sender.onImageLoadFinished(@{@"success": success ? @"true" : @"false"}); 46 | } 47 | } 48 | 49 | - (void)onScratchProgressChanged:(ScratchView *)sender didChangeProgress:(CGFloat)scratchProgress { 50 | NSString* formattedScratchProgress = [NSString stringWithFormat:@"%.02f", scratchProgress]; 51 | if (sender.onScratchProgressChanged) { 52 | sender.onScratchProgressChanged(@{@"progressValue": formattedScratchProgress}); 53 | } 54 | } 55 | 56 | - (void)onScratchDone:(ScratchView *)sender isScratchDone:(BOOL)isDone { 57 | if (sender.onScratchDone) { 58 | sender.onScratchDone(@{@"isScratchDone": isDone ? @"true" : @"false"}); 59 | } 60 | } 61 | 62 | - (void)onTouchStateChanged:(ScratchView *)sender touchState:(BOOL)state { 63 | if (sender.onTouchStateChanged) { 64 | sender.onTouchStateChanged(@{@"touchState": state ? @"true" : @"false"}); 65 | } 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /ios/ScratchView.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import "ScratchViewDelegate.h" 5 | 6 | @interface ScratchView : UIImageView 7 | { 8 | NSString *imageUrl; 9 | NSString *resourceName; 10 | NSString *resizeMode; 11 | CGRect imageRect; 12 | UIColor *placeholderColor; 13 | float threshold; 14 | float brushSize; 15 | UIImage *backgroundColorImage; 16 | UIImage *image; 17 | UIBezierPath *path; 18 | float minDimension; 19 | float gridSize; 20 | NSMutableArray *grid; 21 | bool cleared; 22 | int clearPointsCounter; 23 | float scratchProgress; 24 | bool imageTakenFromView; 25 | 26 | id _delegate; 27 | } 28 | 29 | @property(nonatomic, assign) id _delegate; 30 | 31 | @property(nonatomic, copy) RCTBubblingEventBlock onImageLoadFinished; 32 | @property(nonatomic, copy) RCTBubblingEventBlock onTouchStateChanged; 33 | @property(nonatomic, copy) RCTBubblingEventBlock onScratchProgressChanged; 34 | @property(nonatomic, copy) RCTBubblingEventBlock onScratchDone; 35 | 36 | - (id)init; 37 | - (void)reset; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /ios/ScratchView.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "ScratchViewTools.h" 5 | #import "ScratchView.h" 6 | 7 | @implementation ScratchView 8 | 9 | -(id)init 10 | { 11 | self = [super init]; 12 | self.userInteractionEnabled = true; 13 | self.exclusiveTouch = true; 14 | return self; 15 | } 16 | 17 | -(id) initWithFrame:(CGRect)frame 18 | { 19 | if (self = [super initWithFrame:frame]) { 20 | self.multipleTouchEnabled = NO; 21 | self.userInteractionEnabled = true; 22 | self.exclusiveTouch = true; 23 | } 24 | return self; 25 | } 26 | 27 | 28 | - (void)layoutSubviews { 29 | [self reset]; 30 | [super layoutSubviews]; 31 | } 32 | 33 | -(void) setPlaceholderColor:(NSString *)colorString 34 | { 35 | @try { 36 | self->placeholderColor = [ScratchViewTools colorFromHexString:colorString]; 37 | } 38 | @catch (NSException *exception) { 39 | NSLog(@"placeholderColor error: %@", exception.reason); 40 | } 41 | } 42 | 43 | -(void) setImageUrl:(NSString *)url 44 | { 45 | imageUrl = url; 46 | } 47 | 48 | // Deprecated 49 | -(void) setLocalImageName: (NSString *)imageName 50 | { 51 | resourceName = imageName; 52 | } 53 | 54 | -(void) setResourceName: (NSString *)resourceName 55 | { 56 | self->resourceName = resourceName; 57 | } 58 | 59 | -(void) setResizeMode: (NSString * )resizeMode 60 | { 61 | if (resizeMode == nil) { 62 | return; 63 | } 64 | self->resizeMode = [resizeMode lowercaseString]; 65 | self.layer.masksToBounds = YES; 66 | } 67 | 68 | -(void) setThreshold: (float)value 69 | { 70 | threshold = value; 71 | } 72 | 73 | -(void) setBrushSize: (float)value 74 | { 75 | brushSize = value; 76 | } 77 | 78 | -(void)loadImage 79 | { 80 | UIColor *backgroundColor = placeholderColor != nil ? placeholderColor : [UIColor grayColor]; 81 | self->backgroundColorImage = [ScratchViewTools createImageFromColor:backgroundColor]; 82 | [self setImage:backgroundColorImage]; 83 | if (imageUrl != nil) { 84 | NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString: imageUrl] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { 85 | if (data) { 86 | self->image = [UIImage imageWithData:data]; 87 | } else { 88 | image = backgroundColorImage; 89 | } 90 | dispatch_sync(dispatch_get_main_queue(), ^{ 91 | [self drawImageStart]; 92 | [self drawImageEnd]; 93 | [self reportImageLoadFinished: data ? true : false]; 94 | }); 95 | }]; 96 | [task resume]; 97 | } else if (resourceName != nil) { 98 | image = [UIImage imageNamed:resourceName]; 99 | if (image == nil) { 100 | image = backgroundColorImage; 101 | } 102 | [self drawImageStart]; 103 | [self drawImageEnd]; 104 | [self reportImageLoadFinished: true]; 105 | } else { 106 | image = backgroundColorImage; 107 | [self drawImageStart]; 108 | [self drawImageEnd]; 109 | [self reportImageLoadFinished: true]; 110 | } 111 | } 112 | 113 | -(void) reset { 114 | minDimension = self.frame.size.width > self.frame.size.height ? self.frame.size.height: self.frame.size.width; 115 | brushSize = brushSize > 0 ? brushSize : minDimension / 10.0f; 116 | brushSize = MAX(1, MIN(100, brushSize)); 117 | threshold = threshold > 0 ? threshold : 50; 118 | threshold = MAX(1, MIN(100, threshold)); 119 | path = nil; 120 | [self loadImage]; 121 | [self initGrid]; 122 | [self reportScratchProgress]; 123 | [self reportScratchState]; 124 | } 125 | 126 | -(void) initGrid 127 | { 128 | gridSize = MAX(MIN(ceil(minDimension / brushSize), 29), 9); 129 | grid = [[NSMutableArray alloc] initWithCapacity: gridSize]; 130 | for (int x = 0; x < gridSize; x++) 131 | { 132 | [grid insertObject:[[NSMutableArray alloc] init] atIndex:x]; 133 | for (int y = 0; y < gridSize; y++) 134 | { 135 | [[grid objectAtIndex:x] addObject:@(YES)]; 136 | } 137 | } 138 | clearPointsCounter = 0; 139 | cleared = false; 140 | scratchProgress = 0; 141 | } 142 | 143 | -(void) updateGrid: (CGPoint)point 144 | { 145 | float viewWidth = self.frame.size.width; 146 | float viewHeight = self.frame.size.height; 147 | int pointInGridX = roundf((MAX(MIN(point.x, viewWidth), 0) / viewWidth) * (gridSize - 1.0f)); 148 | int pointInGridY = roundf((MAX(MIN(point.y, viewHeight), 0) / viewHeight) * (gridSize - 1.0f)); 149 | if ([[[grid objectAtIndex:pointInGridX] objectAtIndex: pointInGridY] boolValue]) { 150 | [[grid objectAtIndex:pointInGridX] replaceObjectAtIndex: pointInGridY withObject: @(NO)]; 151 | clearPointsCounter++; 152 | scratchProgress = ((float)clearPointsCounter) / (gridSize*gridSize) * 100.0f; 153 | [self reportScratchProgress]; 154 | } 155 | } 156 | 157 | -(void) drawImageStart { 158 | CGSize selfSize = self.frame.size; 159 | CGSize imgSize = image.size; 160 | CGFloat scale = image.scale; 161 | UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, scale); 162 | 163 | if (!imageTakenFromView) { 164 | [backgroundColorImage drawInRect:CGRectMake(0, 0, selfSize.width, selfSize.height)]; 165 | int offsetX = 0; 166 | int offsetY = 0; 167 | float imageAspect = imgSize.width / imgSize.height; 168 | float viewAspect = selfSize.width / selfSize.height; 169 | if ([resizeMode isEqualToString:@"cover"]) { 170 | if (imageAspect > viewAspect) { 171 | offsetX = (int) (((selfSize.height * imageAspect) - selfSize.width) / 2.0f); 172 | } else { 173 | offsetY = (int) (((selfSize.width / imageAspect) - selfSize.height) / 2.0f); 174 | } 175 | } else if ([resizeMode isEqualToString:@"contain"]) { 176 | if (imageAspect < viewAspect) { 177 | offsetX = (int) (((selfSize.height * imageAspect) - selfSize.width) / 2.0f); 178 | } else { 179 | offsetY = (int) (((selfSize.width / imageAspect) - selfSize.height) / 2.0f); 180 | } 181 | } else { 182 | } 183 | imageRect = CGRectMake(-offsetX, -offsetY, selfSize.width + (offsetX * 2), selfSize.height + (offsetY * 2)); 184 | } 185 | else { 186 | imageRect = CGRectMake(0, 0, selfSize.width, selfSize.height); 187 | } 188 | 189 | if (image == nil) { 190 | return; 191 | } 192 | [image drawInRect:imageRect]; 193 | } 194 | 195 | - (UIImage *) drawImage 196 | { 197 | if (path != nil) { 198 | [path strokeWithBlendMode:kCGBlendModeClear alpha:0]; 199 | } 200 | UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); 201 | [self setImage:newImage]; 202 | return newImage; 203 | } 204 | 205 | -(void) drawImageEnd { 206 | if (image == nil) { 207 | return; 208 | } 209 | imageTakenFromView = YES; 210 | image = [self drawImage]; 211 | UIGraphicsEndImageContext(); 212 | path = nil; 213 | } 214 | 215 | -(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 216 | { 217 | [self reportTouchState:true]; 218 | UITouch *touch = [touches anyObject]; 219 | path = [UIBezierPath bezierPath]; 220 | path.lineWidth = brushSize; 221 | 222 | CGPoint point = [touch locationInView:self]; 223 | [path moveToPoint:point]; 224 | [self drawImageStart]; 225 | } 226 | 227 | -(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 228 | { 229 | UITouch *touch = [touches anyObject]; 230 | CGPoint point = [touch locationInView:self]; 231 | [path addLineToPoint:point]; 232 | [self updateGrid: point]; 233 | if (!cleared && scratchProgress > threshold) { 234 | cleared = true; 235 | [self reportScratchState]; 236 | } 237 | [self drawImage]; 238 | } 239 | 240 | -(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 241 | { 242 | if (path == nil) 243 | { 244 | return; 245 | } 246 | [self reportTouchState:false]; 247 | [self drawImageEnd]; 248 | } 249 | 250 | -(void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 251 | { 252 | if (path == nil) 253 | { 254 | return; 255 | } 256 | [self reportTouchState:false]; 257 | [self drawImageEnd]; 258 | } 259 | 260 | -(void) reportImageLoadFinished:(BOOL)success { 261 | [self._delegate onImageLoadFinished:self successState:success]; 262 | } 263 | 264 | -(void) reportTouchState:(BOOL)state { 265 | [self._delegate onTouchStateChanged:self touchState:state]; 266 | } 267 | 268 | -(void) reportScratchProgress 269 | { 270 | [self._delegate onScratchProgressChanged:self didChangeProgress:scratchProgress]; 271 | } 272 | 273 | -(void) reportScratchState { 274 | [self._delegate onScratchDone:self isScratchDone:cleared]; 275 | } 276 | 277 | @end 278 | -------------------------------------------------------------------------------- /ios/ScratchViewDelegate.h: -------------------------------------------------------------------------------- 1 | @class ScratchView; 2 | 3 | @protocol ScratchViewDelegate 4 | @required 5 | -(void) onImageLoadFinished:(ScratchView *)sender successState:(BOOL)state; 6 | -(void) onTouchStateChanged:(ScratchView *)sender touchState:(BOOL)state; 7 | -(void) onScratchProgressChanged:(ScratchView *)sender didChangeProgress:(CGFloat)scratchProgress; 8 | -(void) onScratchDone:(ScratchView *)sender isScratchDone:(BOOL)isDone; 9 | @end 10 | 11 | -------------------------------------------------------------------------------- /ios/ScratchViewTools.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ScratchViewTools : NSObject 4 | + (UIColor *)colorFromHexString:(NSString *)hexString; 5 | + (UIImage *)createImageFromColor:(UIColor *)color; 6 | @end 7 | -------------------------------------------------------------------------------- /ios/ScratchViewTools.m: -------------------------------------------------------------------------------- 1 | #import "ScratchViewTools.h" 2 | 3 | @implementation ScratchViewTools 4 | 5 | + (UIColor *)colorFromHexString:(NSString *)hexString { 6 | unsigned rgbValue = 0; 7 | NSScanner *scanner = [NSScanner scannerWithString:hexString]; 8 | [scanner setScanLocation:1]; // bypass '#' character 9 | [scanner scanHexInt:&rgbValue]; 10 | return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; 11 | } 12 | 13 | + (UIImage *)createImageFromColor:(UIColor *)color { 14 | CGRect rect = CGRectMake(0, 0, 1, 1); 15 | UIGraphicsBeginImageContext(rect.size); 16 | CGContextRef context = UIGraphicsGetCurrentContext(); 17 | CGContextSetFillColorWithColor(context, [color CGColor]); 18 | CGContextFillRect(context, rect); 19 | UIImage *colorImage = UIGraphicsGetImageFromCurrentImageContext(); 20 | UIGraphicsEndImageContext(); 21 | return colorImage; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-scratch", 3 | "version": "1.2.1", 4 | "description": "Scratch view for react native", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ConduitMobileRND/react-native-scratch" 12 | }, 13 | "keywords": [ 14 | "react-native", 15 | "react-native-scratch", 16 | "react-native-scratch-view", 17 | "react-native-scratch-card", 18 | "react-native-scratch-image", 19 | "scratch", 20 | "scratch-card", 21 | "scratch-view", 22 | "scratch-image" 23 | ], 24 | "author": "Roy Ben-Sasson", 25 | "license": "MIT", 26 | "peerDependencies": { 27 | "react-native": "0.51.0" 28 | } 29 | } -------------------------------------------------------------------------------- /src/ScratchView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { StyleSheet, Animated, requireNativeComponent } from 'react-native'; 3 | 4 | const RNTScratchView = requireNativeComponent('RNTScratchView', ScratchView); 5 | 6 | const AnimatedScratchView = RNTScratchView && Animated.createAnimatedComponent(RNTScratchView); 7 | 8 | class ScratchView extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | animatedValue: new Animated.Value(1), 14 | isScratchDone: false, 15 | visible: true, 16 | }; 17 | 18 | this.scratchOpacity = { 19 | opacity: this.state.animatedValue.interpolate({ 20 | inputRange: [0, 1], 21 | outputRange: [1, 0], 22 | }), 23 | }; 24 | } 25 | 26 | _onImageLoadFinished = (e) => { 27 | const { id, onImageLoadFinished } = this.props; 28 | const success = JSON.parse(e.nativeEvent.success); 29 | onImageLoadFinished && onImageLoadFinished({ id, success }); 30 | } 31 | 32 | _onTouchStateChanged = (e) => { 33 | const { id, onTouchStateChanged } = this.props; 34 | const touchState = JSON.parse(e.nativeEvent.touchState); 35 | const { isScratchDone } = this.state; 36 | 37 | onTouchStateChanged && onTouchStateChanged({ id, touchState }); 38 | if (!touchState && isScratchDone && !this.hideTimeout && this.props.fadeOut !== false) { 39 | const that = this; 40 | this.hideTimeout = setTimeout(() => { 41 | that.setState({ visible: false }); 42 | }, 300); 43 | } 44 | } 45 | 46 | _onScratchProgressChanged = (e) => { 47 | const { id, onScratchProgressChanged } = this.props; 48 | const { progressValue } = e.nativeEvent; 49 | onScratchProgressChanged && onScratchProgressChanged({ id, value: parseFloat(progressValue) }); 50 | } 51 | 52 | _onScratchDone = (e) => { 53 | const { id, onScratchDone } = this.props; 54 | const isScratchDone = JSON.parse(e.nativeEvent.isScratchDone); 55 | if (isScratchDone) { 56 | this.setState({ 57 | isScratchDone, 58 | }, () => { 59 | this.fadeOut(() => { 60 | onScratchDone && onScratchDone({ id, isScratchDone }); 61 | }); 62 | }); 63 | } 64 | } 65 | 66 | fadeOut(postAction) { 67 | if (this.props.fadeOut === false) { 68 | postAction && postAction(); 69 | } else { 70 | this.state.animatedValue.setValue(1); 71 | Animated.timing(this.state.animatedValue, { 72 | toValue: 0, 73 | duration: 300, 74 | useNativeDriver: true, 75 | }).start(postAction); 76 | } 77 | } 78 | 79 | render() { 80 | if (AnimatedScratchView && this.state.visible) { 81 | return ( 82 | 90 | ); 91 | } 92 | return null; 93 | } 94 | } 95 | 96 | 97 | const styles = StyleSheet.create({ 98 | container: { 99 | position: 'absolute', 100 | width: '100%', 101 | height: '100%' 102 | }, 103 | }); 104 | 105 | export default ScratchView --------------------------------------------------------------------------------