capacitor-card-scanner 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -3
- package/android/build.gradle +2 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardDetector.java +283 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScanner.java +267 -195
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScannerPlugin.java +33 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/DetectionOverlay.java +82 -0
- package/dist/docs.json +300 -0
- package/dist/esm/definitions.d.ts +83 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -0
- package/dist/esm/web.js +6 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +6 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +6 -0
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,195 +1,267 @@
|
|
|
1
|
-
package pt.tcgclassifier.cardscanner;
|
|
2
|
-
|
|
3
|
-
import android.graphics.Bitmap;
|
|
4
|
-
import android.graphics.BitmapFactory;
|
|
5
|
-
import android.graphics.Color;
|
|
6
|
-
import android.graphics.Matrix;
|
|
7
|
-
import android.util.Base64;
|
|
8
|
-
import android.
|
|
9
|
-
import android.
|
|
10
|
-
import
|
|
11
|
-
import androidx.
|
|
12
|
-
import androidx.
|
|
13
|
-
import androidx.camera.core.
|
|
14
|
-
import androidx.camera.core.
|
|
15
|
-
import androidx.camera.core.
|
|
16
|
-
import androidx.camera.core.
|
|
17
|
-
import androidx.camera.core.
|
|
18
|
-
import androidx.camera.core.
|
|
19
|
-
import androidx.camera.
|
|
20
|
-
import androidx.camera.
|
|
21
|
-
import androidx.core.
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
package pt.tcgclassifier.cardscanner;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.BitmapFactory;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.graphics.Matrix;
|
|
7
|
+
import android.util.Base64;
|
|
8
|
+
import android.util.Size;
|
|
9
|
+
import android.view.ViewGroup;
|
|
10
|
+
import android.webkit.WebView;
|
|
11
|
+
import androidx.annotation.NonNull;
|
|
12
|
+
import androidx.appcompat.app.AppCompatActivity;
|
|
13
|
+
import androidx.camera.core.Camera;
|
|
14
|
+
import androidx.camera.core.CameraSelector;
|
|
15
|
+
import androidx.camera.core.ImageAnalysis;
|
|
16
|
+
import androidx.camera.core.ImageCapture;
|
|
17
|
+
import androidx.camera.core.ImageCaptureException;
|
|
18
|
+
import androidx.camera.core.ImageProxy;
|
|
19
|
+
import androidx.camera.core.Preview;
|
|
20
|
+
import androidx.camera.core.UseCase;
|
|
21
|
+
import androidx.camera.core.ZoomState;
|
|
22
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
|
23
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy;
|
|
24
|
+
import androidx.camera.lifecycle.ProcessCameraProvider;
|
|
25
|
+
import androidx.camera.view.PreviewView;
|
|
26
|
+
import androidx.core.content.ContextCompat;
|
|
27
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
28
|
+
import java.io.ByteArrayOutputStream;
|
|
29
|
+
import java.nio.ByteBuffer;
|
|
30
|
+
import java.util.ArrayList;
|
|
31
|
+
import java.util.List;
|
|
32
|
+
import java.util.concurrent.ExecutorService;
|
|
33
|
+
import java.util.concurrent.Executors;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Places a CameraX preview behind the Capacitor WebView and makes the WebView
|
|
37
|
+
* transparent, so the HTML UI is drawn on top of the camera feed.
|
|
38
|
+
*/
|
|
39
|
+
public class CardScanner {
|
|
40
|
+
|
|
41
|
+
public interface Callback<T> {
|
|
42
|
+
void onSuccess(T result);
|
|
43
|
+
void onError(String message, Exception e);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static class Photo {
|
|
47
|
+
|
|
48
|
+
public final String base64;
|
|
49
|
+
public final int width;
|
|
50
|
+
public final int height;
|
|
51
|
+
|
|
52
|
+
Photo(String base64, int width, int height) {
|
|
53
|
+
this.base64 = base64;
|
|
54
|
+
this.width = width;
|
|
55
|
+
this.height = height;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private final AppCompatActivity activity;
|
|
60
|
+
private final WebView webView;
|
|
61
|
+
|
|
62
|
+
private PreviewView previewView;
|
|
63
|
+
private DetectionOverlay overlay;
|
|
64
|
+
private ProcessCameraProvider cameraProvider;
|
|
65
|
+
private ImageCapture imageCapture;
|
|
66
|
+
private Camera camera;
|
|
67
|
+
private boolean useFront = false;
|
|
68
|
+
|
|
69
|
+
private final ExecutorService analysisExecutor = Executors.newSingleThreadExecutor();
|
|
70
|
+
private CardDetector detector;
|
|
71
|
+
|
|
72
|
+
public CardScanner(AppCompatActivity activity, WebView webView) {
|
|
73
|
+
this.activity = activity;
|
|
74
|
+
this.webView = webView;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public boolean isRunning() {
|
|
78
|
+
return camera != null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public boolean isFront() {
|
|
82
|
+
return useFront;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Must be called on the UI thread. */
|
|
86
|
+
public void start(boolean front, Callback<Void> callback) {
|
|
87
|
+
useFront = front;
|
|
88
|
+
|
|
89
|
+
if (previewView == null) {
|
|
90
|
+
previewView = new PreviewView(activity);
|
|
91
|
+
// TextureView-based mode composes correctly underneath a transparent WebView.
|
|
92
|
+
previewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
|
|
93
|
+
previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
|
|
94
|
+
ViewGroup parent = (ViewGroup) webView.getParent();
|
|
95
|
+
parent.addView(previewView, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
96
|
+
|
|
97
|
+
// Between the preview and the WebView, so HTML controls stay on top of it.
|
|
98
|
+
overlay = new DetectionOverlay(activity);
|
|
99
|
+
parent.addView(overlay, 1, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
100
|
+
}
|
|
101
|
+
overlay.setMirror(front);
|
|
102
|
+
|
|
103
|
+
webView.setBackgroundColor(Color.TRANSPARENT);
|
|
104
|
+
|
|
105
|
+
ListenableFuture<ProcessCameraProvider> future = ProcessCameraProvider.getInstance(activity);
|
|
106
|
+
future.addListener(
|
|
107
|
+
() -> {
|
|
108
|
+
try {
|
|
109
|
+
cameraProvider = future.get();
|
|
110
|
+
bind();
|
|
111
|
+
callback.onSuccess(null);
|
|
112
|
+
} catch (Exception e) {
|
|
113
|
+
callback.onError("Failed to start camera: " + e.getMessage(), e);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
ContextCompat.getMainExecutor(activity)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private void bind() {
|
|
121
|
+
cameraProvider.unbindAll();
|
|
122
|
+
|
|
123
|
+
Preview preview = new Preview.Builder().build();
|
|
124
|
+
preview.setSurfaceProvider(previewView.getSurfaceProvider());
|
|
125
|
+
|
|
126
|
+
imageCapture = new ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).build();
|
|
127
|
+
|
|
128
|
+
List<UseCase> useCases = new ArrayList<>();
|
|
129
|
+
useCases.add(preview);
|
|
130
|
+
useCases.add(imageCapture);
|
|
131
|
+
if (detector != null) {
|
|
132
|
+
// Collector numbers are tiny, so OCR needs a fairly high resolution.
|
|
133
|
+
ResolutionSelector resolution = new ResolutionSelector.Builder()
|
|
134
|
+
.setResolutionStrategy(new ResolutionStrategy(new Size(1920, 1440), ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER))
|
|
135
|
+
.build();
|
|
136
|
+
ImageAnalysis analysis = new ImageAnalysis.Builder()
|
|
137
|
+
.setResolutionSelector(resolution)
|
|
138
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
139
|
+
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
|
140
|
+
.build();
|
|
141
|
+
analysis.setAnalyzer(analysisExecutor, detector);
|
|
142
|
+
useCases.add(analysis);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
CameraSelector selector = useFront ? CameraSelector.DEFAULT_FRONT_CAMERA : CameraSelector.DEFAULT_BACK_CAMERA;
|
|
146
|
+
camera = cameraProvider.bindToLifecycle(activity, selector, useCases.toArray(new UseCase[0]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Must be called on the UI thread, with the camera running. */
|
|
150
|
+
public void startDetection(CardDetector newDetector) {
|
|
151
|
+
closeDetector();
|
|
152
|
+
detector = newDetector;
|
|
153
|
+
DetectionOverlay target = overlay;
|
|
154
|
+
detector.setBoxListener((box, frameW, frameH) ->
|
|
155
|
+
target.post(() -> {
|
|
156
|
+
// Ignore frames that arrive after detection was stopped or restarted.
|
|
157
|
+
if (detector == newDetector) target.setBox(box, frameW, frameH);
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
bind();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Must be called on the UI thread. */
|
|
164
|
+
public void stopDetection() {
|
|
165
|
+
if (detector == null) return;
|
|
166
|
+
closeDetector();
|
|
167
|
+
if (isRunning()) bind();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private void closeDetector() {
|
|
171
|
+
if (detector != null) {
|
|
172
|
+
CardDetector old = detector;
|
|
173
|
+
detector = null;
|
|
174
|
+
old.setBoxListener(null);
|
|
175
|
+
if (overlay != null) overlay.clear();
|
|
176
|
+
// Let an in-flight analysis finish before releasing the recognizer.
|
|
177
|
+
analysisExecutor.execute(old::close);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Must be called on the UI thread. */
|
|
182
|
+
public void stop() {
|
|
183
|
+
closeDetector();
|
|
184
|
+
if (cameraProvider != null) {
|
|
185
|
+
cameraProvider.unbindAll();
|
|
186
|
+
}
|
|
187
|
+
camera = null;
|
|
188
|
+
imageCapture = null;
|
|
189
|
+
if (previewView != null) {
|
|
190
|
+
((ViewGroup) previewView.getParent()).removeView(previewView);
|
|
191
|
+
previewView = null;
|
|
192
|
+
((ViewGroup) overlay.getParent()).removeView(overlay);
|
|
193
|
+
overlay = null;
|
|
194
|
+
}
|
|
195
|
+
webView.setBackgroundColor(Color.WHITE);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Must be called on the UI thread. */
|
|
199
|
+
public void flip(Callback<Void> callback) {
|
|
200
|
+
useFront = !useFront;
|
|
201
|
+
overlay.setMirror(useFront);
|
|
202
|
+
overlay.clear();
|
|
203
|
+
try {
|
|
204
|
+
bind();
|
|
205
|
+
callback.onSuccess(null);
|
|
206
|
+
} catch (Exception e) {
|
|
207
|
+
callback.onError("Failed to flip camera: " + e.getMessage(), e);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public void setTorch(boolean enabled) {
|
|
212
|
+
camera.getCameraControl().enableTorch(enabled);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public void setZoom(float ratio) {
|
|
216
|
+
ZoomState state = camera.getCameraInfo().getZoomState().getValue();
|
|
217
|
+
if (state != null) {
|
|
218
|
+
ratio = Math.max(state.getMinZoomRatio(), Math.min(state.getMaxZoomRatio(), ratio));
|
|
219
|
+
}
|
|
220
|
+
camera.getCameraControl().setZoomRatio(ratio);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public void capture(int quality, Callback<Photo> callback) {
|
|
224
|
+
imageCapture.takePicture(
|
|
225
|
+
ContextCompat.getMainExecutor(activity),
|
|
226
|
+
new ImageCapture.OnImageCapturedCallback() {
|
|
227
|
+
@Override
|
|
228
|
+
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
229
|
+
try {
|
|
230
|
+
callback.onSuccess(toPhoto(image, quality));
|
|
231
|
+
} catch (Exception e) {
|
|
232
|
+
callback.onError("Failed to process image: " + e.getMessage(), e);
|
|
233
|
+
} finally {
|
|
234
|
+
image.close();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@Override
|
|
239
|
+
public void onError(@NonNull ImageCaptureException e) {
|
|
240
|
+
callback.onError("Capture failed: " + e.getMessage(), e);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private Photo toPhoto(ImageProxy image, int quality) {
|
|
247
|
+
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
|
|
248
|
+
byte[] jpeg = new byte[buffer.remaining()];
|
|
249
|
+
buffer.get(jpeg);
|
|
250
|
+
|
|
251
|
+
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length);
|
|
252
|
+
int rotation = image.getImageInfo().getRotationDegrees();
|
|
253
|
+
if (rotation != 0) {
|
|
254
|
+
Matrix matrix = new Matrix();
|
|
255
|
+
matrix.postRotate(rotation);
|
|
256
|
+
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
|
257
|
+
bitmap.recycle();
|
|
258
|
+
bitmap = rotated;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
262
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out);
|
|
263
|
+
Photo photo = new Photo(Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP), bitmap.getWidth(), bitmap.getHeight());
|
|
264
|
+
bitmap.recycle();
|
|
265
|
+
return photo;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -88,6 +88,39 @@ public class CardScannerPlugin extends Plugin {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
@PluginMethod
|
|
92
|
+
public void startDetection(PluginCall call) {
|
|
93
|
+
if (!ensureRunning(call)) return;
|
|
94
|
+
CardDetector.Options options = new CardDetector.Options();
|
|
95
|
+
options.intervalMs = call.getInt("intervalMs", (int) options.intervalMs);
|
|
96
|
+
options.stableFrames = call.getInt("stableFrames", options.stableFrames);
|
|
97
|
+
options.includeImage = call.getBoolean("includeImage", options.includeImage);
|
|
98
|
+
options.cardAspectRatio = call.getFloat("cardAspectRatio", options.cardAspectRatio);
|
|
99
|
+
options.aspectTolerance = call.getFloat("aspectTolerance", options.aspectTolerance);
|
|
100
|
+
options.minCardArea = call.getFloat("minCardArea", options.minCardArea);
|
|
101
|
+
options.maxCardArea = call.getFloat("maxCardArea", options.maxCardArea);
|
|
102
|
+
options.requireCard = call.getBoolean("requireCard", options.requireCard);
|
|
103
|
+
|
|
104
|
+
CardDetector detector = new CardDetector(options, result -> notifyListeners("cardDetected", result));
|
|
105
|
+
getActivity().runOnUiThread(() -> {
|
|
106
|
+
try {
|
|
107
|
+
implementation.startDetection(detector);
|
|
108
|
+
call.resolve();
|
|
109
|
+
} catch (Exception e) {
|
|
110
|
+
detector.close();
|
|
111
|
+
call.reject("Failed to start detection: " + e.getMessage(), e);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@PluginMethod
|
|
117
|
+
public void stopDetection(PluginCall call) {
|
|
118
|
+
getActivity().runOnUiThread(() -> {
|
|
119
|
+
implementation.stopDetection();
|
|
120
|
+
call.resolve();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
91
124
|
@PluginMethod
|
|
92
125
|
public void setTorch(PluginCall call) {
|
|
93
126
|
if (!ensureRunning(call)) return;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package pt.tcgclassifier.cardscanner;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.graphics.Paint;
|
|
7
|
+
import android.graphics.RectF;
|
|
8
|
+
import android.os.SystemClock;
|
|
9
|
+
import android.view.View;
|
|
10
|
+
import androidx.annotation.Nullable;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Draws a red rectangle around the detected card. Sits between the camera
|
|
14
|
+
* preview and the (transparent) WebView, and maps frame coordinates the same
|
|
15
|
+
* way PreviewView's FILL_CENTER does.
|
|
16
|
+
*/
|
|
17
|
+
public class DetectionOverlay extends View {
|
|
18
|
+
|
|
19
|
+
/** Keep the rectangle this long after the card was last seen, to avoid flicker. */
|
|
20
|
+
private static final long HIDE_AFTER_MS = 300;
|
|
21
|
+
|
|
22
|
+
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
23
|
+
private final float radius;
|
|
24
|
+
private final RectF drawRect = new RectF();
|
|
25
|
+
|
|
26
|
+
@Nullable
|
|
27
|
+
private RectF box;
|
|
28
|
+
private int frameWidth;
|
|
29
|
+
private int frameHeight;
|
|
30
|
+
private boolean mirror;
|
|
31
|
+
private long lastBoxAt;
|
|
32
|
+
|
|
33
|
+
public DetectionOverlay(Context context) {
|
|
34
|
+
super(context);
|
|
35
|
+
float density = context.getResources().getDisplayMetrics().density;
|
|
36
|
+
radius = 8 * density;
|
|
37
|
+
paint.setColor(Color.RED);
|
|
38
|
+
paint.setStyle(Paint.Style.STROKE);
|
|
39
|
+
paint.setStrokeWidth(3 * density);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public void setMirror(boolean mirror) {
|
|
43
|
+
this.mirror = mirror;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Must be called on the UI thread. */
|
|
47
|
+
public void setBox(@Nullable RectF box, int frameWidth, int frameHeight) {
|
|
48
|
+
long now = SystemClock.uptimeMillis();
|
|
49
|
+
if (box == null) {
|
|
50
|
+
if (this.box == null || now - lastBoxAt < HIDE_AFTER_MS) return;
|
|
51
|
+
} else {
|
|
52
|
+
lastBoxAt = now;
|
|
53
|
+
}
|
|
54
|
+
this.box = box;
|
|
55
|
+
this.frameWidth = frameWidth;
|
|
56
|
+
this.frameHeight = frameHeight;
|
|
57
|
+
invalidate();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public void clear() {
|
|
61
|
+
box = null;
|
|
62
|
+
invalidate();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Override
|
|
66
|
+
protected void onDraw(Canvas canvas) {
|
|
67
|
+
if (box == null || frameWidth == 0 || frameHeight == 0) return;
|
|
68
|
+
|
|
69
|
+
float vw = getWidth();
|
|
70
|
+
float vh = getHeight();
|
|
71
|
+
float scale = Math.max(vw / frameWidth, vh / frameHeight);
|
|
72
|
+
float sw = frameWidth * scale;
|
|
73
|
+
float sh = frameHeight * scale;
|
|
74
|
+
float dx = (vw - sw) / 2f;
|
|
75
|
+
float dy = (vh - sh) / 2f;
|
|
76
|
+
|
|
77
|
+
float left = mirror ? 1f - box.right : box.left;
|
|
78
|
+
float right = mirror ? 1f - box.left : box.right;
|
|
79
|
+
drawRect.set(dx + left * sw, dy + box.top * sh, dx + right * sw, dy + box.bottom * sh);
|
|
80
|
+
canvas.drawRoundRect(drawRect, radius, radius, paint);
|
|
81
|
+
}
|
|
82
|
+
}
|