├── .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 |
7 |
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
--------------------------------------------------------------------------------