Feature/overlays (#502)
* Overlays (#421) * get overlay working * fix overlay drawing * allow disabling overlay in pictures or videos * Fix picture snapshot colors when there is an overlay * Bug fixes * Update example with watermark * Fix bug * Fix overlay orientation in pictures * Fix overlay orientation in videos * Fix overlay when changing preview size * Fix bug * Experiment * Refactor EglViewport * Refactor SnapshotPictureRecorder * Use single EglViewport * Refactor SnapshotVideoRecorder * Bug fix * fix some of the requested changes * clean adding View to OverlayLayout * Specify where to draw the overlay * Refactor * Remove unnecessary variable from CameraPreview * Use mWithOverlay in SnapshotVideoRecorder * Use multiple OverlayLayout * Add explanation for OverlayLayoutManager * override removeView * Remove DisableOverlayFor * Reorder to overlay package * Address issues * Draw selectively on preview, picture or video * Use single Overlay with three targets * Fix picture snapshots * Add demo app control * Fix video snapshot rotation for Camera2 * Fix video snapshot overlay rotation for Camera2 only * Fix tests, improve performance * Add animating watermark * Add tests in CameraViewTest * Add integration tests * Fix race condition * Improve README * Remove isOverlay * Remove isOverlay from docs * Add documentation empty page * Add documentation links * Add real documentation * Remove isOverlay from attrs * Add doc links to main README * Fix tests and logs * Small changes in AudioMediaEncoder * Add changelog linepull/513/head
parent
7d87d4af61
commit
ea952d1497
@ -0,0 +1,197 @@ |
||||
package com.otaliastudios.cameraview.overlay; |
||||
|
||||
|
||||
import android.content.res.Resources; |
||||
import android.content.res.TypedArray; |
||||
import android.content.res.XmlResourceParser; |
||||
import android.graphics.Canvas; |
||||
import android.util.AttributeSet; |
||||
import android.util.Xml; |
||||
import android.view.Gravity; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.widget.FrameLayout; |
||||
|
||||
import androidx.annotation.NonNull; |
||||
import androidx.test.annotation.UiThreadTest; |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4; |
||||
import androidx.test.filters.SmallTest; |
||||
|
||||
import com.otaliastudios.cameraview.BaseTest; |
||||
import com.otaliastudios.cameraview.R; |
||||
|
||||
import org.junit.After; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.ArgumentCaptor; |
||||
import org.mockito.ArgumentMatcher; |
||||
import org.mockito.invocation.InvocationOnMock; |
||||
import org.mockito.stubbing.Answer; |
||||
import org.w3c.dom.Attr; |
||||
import org.xmlpull.v1.XmlPullParser; |
||||
import org.xmlpull.v1.XmlPullParserException; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertNotEquals; |
||||
import static org.junit.Assert.assertNotNull; |
||||
import static org.junit.Assert.assertTrue; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.ArgumentMatchers.anyFloat; |
||||
import static org.mockito.ArgumentMatchers.anyLong; |
||||
import static org.mockito.ArgumentMatchers.argThat; |
||||
import static org.mockito.ArgumentMatchers.eq; |
||||
import static org.mockito.ArgumentMatchers.notNull; |
||||
import static org.mockito.Mockito.doNothing; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.never; |
||||
import static org.mockito.Mockito.reset; |
||||
import static org.mockito.Mockito.spy; |
||||
import static org.mockito.Mockito.times; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
@RunWith(AndroidJUnit4.class) |
||||
@SmallTest |
||||
public class OverlayLayoutTest extends BaseTest { |
||||
|
||||
private OverlayLayout overlayLayout; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
overlayLayout = spy(new OverlayLayout(context())); |
||||
} |
||||
|
||||
@After |
||||
public void tearDown() { |
||||
overlayLayout = null; |
||||
} |
||||
|
||||
@Test |
||||
public void testIsOverlay_LayoutParams() { |
||||
ViewGroup.LayoutParams params; |
||||
|
||||
params = new ViewGroup.LayoutParams(10, 10); |
||||
assertFalse(overlayLayout.isOverlay(params)); |
||||
|
||||
params = new OverlayLayout.LayoutParams(10, 10); |
||||
assertTrue(overlayLayout.isOverlay(params)); |
||||
} |
||||
|
||||
@Test |
||||
public void testIsOverlay_attributeSet() throws Exception { |
||||
int layout1 = com.otaliastudios.cameraview.test.R.layout.overlay; |
||||
int layout2 = com.otaliastudios.cameraview.test.R.layout.not_overlay; |
||||
|
||||
AttributeSet set1 = getAttributeSet(layout1); |
||||
assertTrue(overlayLayout.isOverlay(set1)); |
||||
|
||||
AttributeSet set2 = getAttributeSet(layout2); |
||||
assertFalse(overlayLayout.isOverlay(set2)); |
||||
} |
||||
|
||||
@NonNull |
||||
private AttributeSet getAttributeSet(int layout) throws Exception { |
||||
// Get the attribute set in the correct state: use a parser and move to START_TAG
|
||||
XmlResourceParser parser = context().getResources().getLayout(layout); |
||||
//noinspection StatementWithEmptyBody
|
||||
while (parser.next() != XmlResourceParser.START_TAG) {} |
||||
return Xml.asAttributeSet(parser); |
||||
} |
||||
|
||||
@Test |
||||
public void testLayoutParams_drawsOn() { |
||||
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10); |
||||
|
||||
assertFalse(params.drawsOn(Overlay.Target.PREVIEW)); |
||||
assertFalse(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT)); |
||||
assertFalse(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT)); |
||||
|
||||
params.drawOnPreview = true; |
||||
assertTrue(params.drawsOn(Overlay.Target.PREVIEW)); |
||||
params.drawOnPictureSnapshot = true; |
||||
assertTrue(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT)); |
||||
params.drawOnVideoSnapshot = true; |
||||
assertTrue(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT)); |
||||
} |
||||
|
||||
@Test |
||||
public void testLayoutParams_toString() { |
||||
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10); |
||||
String string = params.toString(); |
||||
assertTrue(string.contains("drawOnPreview")); |
||||
assertTrue(string.contains("drawOnPictureSnapshot")); |
||||
assertTrue(string.contains("drawOnVideoSnapshot")); |
||||
} |
||||
|
||||
@Test |
||||
public void testDrawChild() { |
||||
Canvas canvas = new Canvas(); |
||||
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10); |
||||
View child = new View(context()); |
||||
child.setLayoutParams(params); |
||||
when(overlayLayout.doDrawChild(canvas, child, 0)).thenReturn(true); |
||||
|
||||
overlayLayout.currentTarget = Overlay.Target.PREVIEW; |
||||
assertFalse(overlayLayout.drawChild(canvas, child, 0)); |
||||
params.drawOnPreview = true; |
||||
assertTrue(overlayLayout.drawChild(canvas, child, 0)); |
||||
|
||||
overlayLayout.currentTarget = Overlay.Target.PICTURE_SNAPSHOT; |
||||
assertFalse(overlayLayout.drawChild(canvas, child, 0)); |
||||
params.drawOnPictureSnapshot = true; |
||||
assertTrue(overlayLayout.drawChild(canvas, child, 0)); |
||||
|
||||
overlayLayout.currentTarget = Overlay.Target.VIDEO_SNAPSHOT; |
||||
assertFalse(overlayLayout.drawChild(canvas, child, 0)); |
||||
params.drawOnVideoSnapshot = true; |
||||
assertTrue(overlayLayout.drawChild(canvas, child, 0)); |
||||
} |
||||
|
||||
@UiThreadTest |
||||
@Test |
||||
public void testDraw() { |
||||
Canvas canvas = new Canvas(); |
||||
when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(false); |
||||
overlayLayout.draw(canvas); |
||||
verify(overlayLayout, never()).drawOn(Overlay.Target.PREVIEW, canvas); |
||||
|
||||
when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(true); |
||||
overlayLayout.draw(canvas); |
||||
verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas); |
||||
} |
||||
|
||||
@UiThreadTest |
||||
@Test |
||||
public void testDrawOn() { |
||||
Canvas canvas = spy(new Canvas()); |
||||
View child = new View(context()); |
||||
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10); |
||||
params.drawOnPreview = true; |
||||
params.drawOnPictureSnapshot = true; |
||||
params.drawOnVideoSnapshot = true; |
||||
overlayLayout.addView(child, params); |
||||
|
||||
overlayLayout.drawOn(Overlay.Target.PREVIEW, canvas); |
||||
verify(canvas, never()).scale(anyFloat(), anyFloat()); |
||||
verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong()); |
||||
reset(canvas); |
||||
reset(overlayLayout); |
||||
|
||||
overlayLayout.drawOn(Overlay.Target.PICTURE_SNAPSHOT, canvas); |
||||
verify(canvas, times(1)).scale(anyFloat(), anyFloat()); |
||||
verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong()); |
||||
reset(canvas); |
||||
reset(overlayLayout); |
||||
|
||||
overlayLayout.drawOn(Overlay.Target.VIDEO_SNAPSHOT, canvas); |
||||
verify(canvas, times(1)).scale(anyFloat(), anyFloat()); |
||||
verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong()); |
||||
reset(canvas); |
||||
reset(overlayLayout); |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"/> |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
app:layout_drawOnPreview="true" |
||||
app:layout_drawOnPictureSnapshot="true" |
||||
app:layout_drawOnVideoSnapshot="true" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"/> |
@ -0,0 +1,33 @@ |
||||
package com.otaliastudios.cameraview.overlay; |
||||
|
||||
import android.graphics.Canvas; |
||||
|
||||
import androidx.annotation.NonNull; |
||||
|
||||
/** |
||||
* Base interface for overlays. |
||||
*/ |
||||
public interface Overlay { |
||||
|
||||
enum Target { |
||||
PREVIEW, PICTURE_SNAPSHOT, VIDEO_SNAPSHOT |
||||
} |
||||
|
||||
/** |
||||
* Called for this overlay to draw itself on the specified target and canvas. |
||||
* |
||||
* @param target target |
||||
* @param canvas target canvas |
||||
*/ |
||||
void drawOn(@NonNull Target target, @NonNull Canvas canvas); |
||||
|
||||
/** |
||||
* Called to understand if this overlay would like to draw onto the given |
||||
* target or not. If true is returned, {@link #drawOn(Target, Canvas)} can be |
||||
* called at a future time. |
||||
* |
||||
* @param target the target |
||||
* @return true to draw on it |
||||
*/ |
||||
boolean drawsOn(@NonNull Target target); |
||||
} |
@ -0,0 +1,211 @@ |
||||
package com.otaliastudios.cameraview.overlay; |
||||
|
||||
|
||||
import android.annotation.SuppressLint; |
||||
import android.content.Context; |
||||
import android.content.res.TypedArray; |
||||
import android.graphics.Canvas; |
||||
import android.util.AttributeSet; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.widget.FrameLayout; |
||||
|
||||
import androidx.annotation.NonNull; |
||||
import androidx.annotation.Nullable; |
||||
import androidx.annotation.VisibleForTesting; |
||||
|
||||
import com.otaliastudios.cameraview.CameraLogger; |
||||
import com.otaliastudios.cameraview.R; |
||||
|
||||
|
||||
@SuppressLint("CustomViewStyleable") |
||||
public class OverlayLayout extends FrameLayout implements Overlay { |
||||
|
||||
private static final String TAG = OverlayLayout.class.getSimpleName(); |
||||
private static final CameraLogger LOG = CameraLogger.create(TAG); |
||||
|
||||
@VisibleForTesting Target currentTarget = Target.PREVIEW; |
||||
|
||||
/** |
||||
* We set {@link #setWillNotDraw(boolean)} to false even if we don't draw anything. |
||||
* This ensures that the View system will call {@link #draw(Canvas)} on us instead |
||||
* of short-circuiting to {@link #dispatchDraw(Canvas)}. |
||||
* |
||||
* That would be a problem for us since we use {@link #draw(Canvas)} to understand if |
||||
* we are currently drawing on the preview or not. |
||||
* |
||||
* @param context a context |
||||
*/ |
||||
public OverlayLayout(@NonNull Context context) { |
||||
super(context); |
||||
setWillNotDraw(false); |
||||
} |
||||
|
||||
/** |
||||
* Returns true if this {@link AttributeSet} belongs to an overlay. |
||||
* @param set an attribute set |
||||
* @return true if overlay |
||||
*/ |
||||
public boolean isOverlay(@Nullable AttributeSet set) { |
||||
if (set == null) return false; |
||||
TypedArray a = getContext().obtainStyledAttributes(set, R.styleable.CameraView_Layout); |
||||
boolean isOverlay = |
||||
a.hasValue(R.styleable.CameraView_Layout_layout_drawOnPreview) |
||||
|| a.hasValue(R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot) |
||||
|| a.hasValue(R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot); |
||||
a.recycle(); |
||||
return isOverlay; |
||||
} |
||||
|
||||
/** |
||||
* Returns true if this {@link ViewGroup.LayoutParams} belongs to an overlay. |
||||
* @param params a layout params |
||||
* @return true if overlay |
||||
*/ |
||||
public boolean isOverlay(@NonNull ViewGroup.LayoutParams params) { |
||||
return params instanceof LayoutParams; |
||||
} |
||||
|
||||
/** |
||||
* Generates our own overlay layout params. |
||||
* @param attrs input attrs |
||||
* @return our params |
||||
*/ |
||||
@Override |
||||
public OverlayLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { |
||||
return new LayoutParams(getContext(), attrs); |
||||
} |
||||
|
||||
/** |
||||
* This is called by the View hierarchy, so at this point we are |
||||
* likely drawing on the preview. |
||||
* @param canvas View canvas |
||||
*/ |
||||
@SuppressLint("MissingSuperCall") |
||||
@Override |
||||
public void draw(Canvas canvas) { |
||||
LOG.i("normal draw called."); |
||||
if (drawsOn(Target.PREVIEW)) { |
||||
drawOn(Target.PREVIEW, canvas); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean drawsOn(@NonNull Target target) { |
||||
for (int i = 0; i < getChildCount(); i++) { |
||||
LayoutParams params = (LayoutParams) getChildAt(i).getLayoutParams(); |
||||
if (params.drawsOn(target)) return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* For {@link Target#PREVIEW}, this method is called by the View hierarchy. We will |
||||
* just forward the call to super. |
||||
* |
||||
* For {@link Target#PICTURE_SNAPSHOT} and {@link Target#VIDEO_SNAPSHOT}, |
||||
* this method is called by the overlay drawer. We call {@link #dispatchDraw(Canvas)} |
||||
* to draw our children only. |
||||
* |
||||
* @param target the draw target |
||||
* @param canvas the canvas |
||||
*/ |
||||
@Override |
||||
public void drawOn(@NonNull Target target, @NonNull Canvas canvas) { |
||||
synchronized (this) { |
||||
currentTarget = target; |
||||
switch (target) { |
||||
case PREVIEW: |
||||
super.draw(canvas); |
||||
break; |
||||
case VIDEO_SNAPSHOT: |
||||
case PICTURE_SNAPSHOT: |
||||
canvas.save(); |
||||
// The input canvas size is that of the preview stream, cropped to match
|
||||
// the view aspect ratio (this op is done by picture & video recorder).
|
||||
// So the aspect ratio is guaranteed to be the same, but we might have
|
||||
// to apply some scale (typically > 1).
|
||||
float widthScale = canvas.getWidth() / (float) getWidth(); |
||||
float heightScale = canvas.getHeight() / (float) getHeight(); |
||||
LOG.i("draw", |
||||
"target:", target, |
||||
"canvas:", canvas.getWidth() + "x" + canvas.getHeight(), |
||||
"view:", getWidth() + "x" + getHeight(), |
||||
"widthScale:", widthScale, |
||||
"heightScale:", heightScale |
||||
); |
||||
canvas.scale(widthScale, heightScale); |
||||
dispatchDraw(canvas); |
||||
canvas.restore(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* We end up here in all three cases, and should filter out |
||||
* views that are not meant to be drawn on that specific surface. |
||||
*/ |
||||
@Override |
||||
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { |
||||
LayoutParams params = (LayoutParams) child.getLayoutParams(); |
||||
if (params.drawsOn(currentTarget)) { |
||||
LOG.v("Performing drawing for view:", child.getClass().getSimpleName(), |
||||
"target:", currentTarget, |
||||
"params:", params); |
||||
return doDrawChild(canvas, child, drawingTime); |
||||
} else { |
||||
LOG.v("Skipping drawing for view:", child.getClass().getSimpleName(), |
||||
"target:", currentTarget, |
||||
"params:", params); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
boolean doDrawChild(Canvas canvas, View child, long drawingTime) { |
||||
return super.drawChild(canvas, child, drawingTime); |
||||
} |
||||
|
||||
@SuppressWarnings("WeakerAccess") |
||||
public static class LayoutParams extends FrameLayout.LayoutParams { |
||||
|
||||
@SuppressWarnings("unused") |
||||
public boolean drawOnPreview = false; |
||||
public boolean drawOnPictureSnapshot = false; |
||||
public boolean drawOnVideoSnapshot = false; |
||||
|
||||
public LayoutParams(int width, int height) { |
||||
super(width, height); |
||||
} |
||||
|
||||
public LayoutParams(@NonNull Context context, @NonNull AttributeSet attrs) { |
||||
super(context, attrs); |
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView_Layout); |
||||
try { |
||||
drawOnPreview = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPreview, false); |
||||
drawOnPictureSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot, false); |
||||
drawOnVideoSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot, false); |
||||
} finally { |
||||
a.recycle(); |
||||
} |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
boolean drawsOn(@NonNull Target target) { |
||||
return ((target == Target.PREVIEW && drawOnPreview) |
||||
|| (target == Target.VIDEO_SNAPSHOT && drawOnVideoSnapshot) |
||||
|| (target == Target.PICTURE_SNAPSHOT && drawOnPictureSnapshot)); |
||||
} |
||||
|
||||
@NonNull |
||||
@Override |
||||
public String toString() { |
||||
return getClass().getName() + "[" |
||||
+ "drawOnPreview:" + drawOnPreview |
||||
+ ",drawOnPictureSnapshot:" + drawOnPictureSnapshot |
||||
+ ",drawOnVideoSnapshot:" + drawOnVideoSnapshot |
||||
+ "]"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,73 @@ |
||||
--- |
||||
layout: page |
||||
title: "Watermarks and Overlays" |
||||
subtitle: "Static and animated overlays" |
||||
description: "Static and animated overlays" |
||||
category: docs |
||||
order: 10 |
||||
date: 2019-07-14 20:14:31 |
||||
disqus: 1 |
||||
--- |
||||
|
||||
CameraView offers a simple yet powerful framework for watermarks and overlays of any kind. |
||||
These overlays can be shown on the live camera preview, plus they appear on the media results |
||||
taken with `takePictureSnapshot()` or `takeVideoSnapshot()`. |
||||
|
||||
### Simple Usage |
||||
|
||||
```xml |
||||
<com.otaliastudios.cameraview.CameraView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<!-- Watermark in bottom/end corner --> |
||||
<ImageView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_gravity="bottom|end" |
||||
android:src="@drawable/watermark" |
||||
app:layout_drawOnPreview="true|false" |
||||
app:layout_drawOnPictureSnapshot="true|false" |
||||
app:layout_drawOnVideoSnapshot="true|false"/> |
||||
|
||||
<!-- More overlays here... --> |
||||
|
||||
</com.otaliastudios.cameraview.CameraView> |
||||
``` |
||||
|
||||
As you can see, the overlay system is View-based - each overlay is just a real `View` attached |
||||
into the hierarchy. This is a powerful and creative tool. You can, for instance, retrieve the |
||||
overlay with `findViewById` and: |
||||
|
||||
- Animate it! |
||||
- Change its visibility |
||||
- Change its position or appearance |
||||
- Do so while video is being recorded |
||||
|
||||
Any changes in the overlay appearance will be recorded in real-time in the picture snapshot |
||||
or video snapshot that you are capturing. |
||||
|
||||
As you can see in the example, you can also selectively choose, for each overlay, whether it |
||||
will draw on the preview (`layout_drawOnPreview`), on picture snapshots (`layout_drawOnPreview`), |
||||
on video snapshots (`layout_drawOnPreview`). |
||||
|
||||
### Advanced Usage |
||||
|
||||
If you need to change these flags at runtime, you should cast the overlay `LayoutParams` as follows: |
||||
|
||||
```java |
||||
// Cast to OverlayLayout.LayoutParams |
||||
View overlay = findViewById(R.id.watermark); |
||||
OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams(); |
||||
|
||||
// Perform changes |
||||
params.drawOnPreview = true; // draw on preview |
||||
params.drawOnPreview = false; // do not draw on preview |
||||
params.drawOnPictureSnapshot = true; // draw on picture snapshots |
||||
params.drawOnPictureSnapshot = false; // do not draw on picture snapshots |
||||
params.drawOnVideoSnapshot = true; // draw on video snapshots |
||||
params.drawOnVideoSnapshot = false; // do not draw on video snapshots |
||||
|
||||
// When done, apply |
||||
overlay.setLayoutParams(params); |
||||
``` |
Loading…
Reference in new issue