Some bug fixes (#953)

* Kotlin 1.4.0, fixes #940

* Fix multifilter bug, fixes #875

* Fix arithmetic exception, fixes #895

* Update dependencies

* Demo app in Kotlin

* Avoid stackoverflows on zoom/ev, fixes #856

* Remove Gemfile.lock

* Try to fix build
pull/954/head
Mattia Iavarone 4 years ago committed by GitHub
parent daf7a0bf44
commit 8207e67679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/workflows/build.yml
  2. 6
      build.gradle.kts
  3. 22
      cameraview/build.gradle.kts
  4. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  5. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  6. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  7. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java
  8. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java
  9. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java
  10. 12
      demo/build.gradle.kts
  11. 417
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  12. 46
      demo/src/main/java/com/otaliastudios/cameraview/demo/MessageView.java
  13. 605
      demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java
  14. 97
      demo/src/main/java/com/otaliastudios/cameraview/demo/OptionView.java
  15. 136
      demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java
  16. 136
      demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java
  17. 344
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt
  18. 34
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/MessageView.kt
  19. 296
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/Option.kt
  20. 76
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/OptionView.kt
  21. 105
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/PicturePreviewActivity.kt
  22. 107
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt
  23. 1
      docs/.gitignore
  24. 248
      docs/Gemfile.lock

@ -18,7 +18,7 @@ jobs:
with:
java-version: 1.8
- name: Perform base checks
run: ./gradlew demo:assembleDebug cameraview:publishToDirectory
run: ./gradlew demo:assembleDebug cameraview:publishToDirectory --stacktrace
ANDROID_UNIT_TESTS:
name: Unit Tests
runs-on: ubuntu-latest
@ -28,7 +28,7 @@ jobs:
with:
java-version: 1.8
- name: Execute unit tests
run: ./gradlew cameraview:runUnitTests
run: ./gradlew cameraview:runUnitTests --stacktrace
- name: Upload unit tests artifact
uses: actions/upload-artifact@v1
with:

@ -4,7 +4,6 @@ buildscript {
extra["minSdkVersion"] = 15
extra["compileSdkVersion"] = 29
extra["targetSdkVersion"] = 29
extra["kotlinVersion"] = "1.3.72"
repositories {
google()
@ -13,10 +12,9 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:4.0.0")
classpath("com.android.tools.build:gradle:4.0.1")
classpath("com.otaliastudios.tools:publisher:0.3.3")
val kotlinVersion = property("kotlinVersion") as String
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0")
}
}

@ -9,10 +9,10 @@ plugins {
}
android {
setCompileSdkVersion(rootProject.property("compileSdkVersion") as Int)
setCompileSdkVersion(property("compileSdkVersion") as Int)
defaultConfig {
setMinSdkVersion(rootProject.property("minSdkVersion") as Int)
setTargetSdkVersion(rootProject.property("targetSdkVersion") as Int)
setMinSdkVersion(property("minSdkVersion") as Int)
setTargetSdkVersion(property("targetSdkVersion") as Int)
versionCode = 1
versionName = "2.6.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -25,20 +25,20 @@ android {
}
dependencies {
testImplementation("junit:junit:4.12")
testImplementation("junit:junit:4.13")
testImplementation("org.mockito:mockito-inline:2.28.2")
androidTestImplementation("androidx.test:runner:1.2.0")
androidTestImplementation("androidx.test:rules:1.2.0")
androidTestImplementation("androidx.test:runner:1.3.0")
androidTestImplementation("androidx.test:rules:1.3.0")
androidTestImplementation("androidx.test.ext:junit:1.1.1")
androidTestImplementation("org.mockito:mockito-android:2.28.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0")
api("androidx.exifinterface:exifinterface:1.1.0")
api("androidx.lifecycle:lifecycle-common:2.1.0")
api("com.google.android.gms:play-services-tasks:17.0.0")
api("androidx.exifinterface:exifinterface:1.2.0")
api("androidx.lifecycle:lifecycle-common:2.2.0")
api("com.google.android.gms:play-services-tasks:17.2.0")
implementation("androidx.annotation:annotation:1.1.0")
implementation("com.otaliastudios.opengl:egloo:0.5.2")
implementation("com.otaliastudios.opengl:egloo:0.5.3")
}
// Publishing
@ -93,7 +93,7 @@ tasks.register("runAndroidTests") { // changing name? change github workflow
}
// Merge the two with a jacoco task.
jacoco { toolVersion = "0.8.1" }
jacoco { toolVersion = "0.8.5" }
tasks.register("computeCoverage", JacocoReport::class) {
dependsOn("compileDebugSources") // Compile sources, needed below
executionData.from(fileTree(coverageInputDir))

@ -394,12 +394,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// attached. That's why we instantiate the preview here.
doInstantiatePreview();
}
mOrientationHelper.enable();
}
@Override
protected void onDetachedFromWindow() {
if (!mInEditor) mOrientationHelper.disable();
mLastPreviewStreamSize = null;
super.onDetachedFromWindow();
}
@ -732,6 +730,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* Sets permissions flag if you want enable auto check permissions or disable it.
* @param requestPermissions - true: auto check permissions enabled, false: auto check permissions disabled.
*/
@SuppressWarnings("unused")
public void setRequestPermissions(boolean requestPermissions) {
mRequestPermissions = requestPermissions;
}
@ -1810,6 +1809,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @param fileDescriptor a file descriptor where the video will be saved
* @param durationMillis recording max duration
*/
@SuppressWarnings("unused")
public void takeVideo(@NonNull FileDescriptor fileDescriptor, int durationMillis) {
takeVideo(null, fileDescriptor, durationMillis);
}

@ -623,7 +623,10 @@ public class Camera1Engine extends CameraBaseEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue;
mZoomValue = zoom;
mZoomTask = getOrchestrator().scheduleStateful("zoom (" + zoom + ")",
// Zoom requests can be high frequency (e.g. linked to touch events), so
// we remove the task before scheduling to avoid stack overflows in orchestrator.
getOrchestrator().remove("zoom");
mZoomTask = getOrchestrator().scheduleStateful("zoom",
CameraState.ENGINE,
new Runnable() {
@Override
@ -655,8 +658,11 @@ public class Camera1Engine extends CameraBaseEngine implements
@Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue;
// EV requests can be high frequency (e.g. linked to touch events), so
// we remove the task before scheduling to avoid stack overflows in orchestrator.
getOrchestrator().remove("exposure correction");
mExposureCorrectionTask = getOrchestrator().scheduleStateful(
"exposure correction (" + EVvalue + ")",
"exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override

@ -1246,8 +1246,11 @@ public class Camera2Engine extends CameraBaseEngine implements
public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
final float old = mZoomValue;
mZoomValue = zoom;
// Zoom requests can be high frequency (e.g. linked to touch events), so
// we remove the task before scheduling to avoid stack overflows in orchestrator.
getOrchestrator().remove("zoom");
mZoomTask = getOrchestrator().scheduleStateful(
"zoom (" + zoom + ")",
"zoom",
CameraState.ENGINE,
new Runnable() {
@Override
@ -1302,8 +1305,11 @@ public class Camera2Engine extends CameraBaseEngine implements
final boolean notify) {
final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue;
// EV requests can be high frequency (e.g. linked to touch events), so
// we remove the task before scheduling to avoid stack overflows in orchestrator.
getOrchestrator().remove("exposure correction");
mExposureCorrectionTask = getOrchestrator().scheduleStateful(
"exposure correction (" + EVvalue + ")",
"exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override

@ -48,6 +48,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
static class State {
@VisibleForTesting boolean isProgramCreated = false;
@VisibleForTesting boolean isFramebufferCreated = false;
private boolean sizeChanged = false;
@VisibleForTesting Size size = null;
private int programHandle = -1;
private GlFramebuffer outputFramebuffer = null;
@ -135,8 +136,17 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isFirst, boolean isLast) {
State state = states.get(filter);
if (isLast) {
//noinspection ConstantConditions
if (state.isFramebufferCreated || isLast) return;
state.sizeChanged = false;
return;
}
//noinspection ConstantConditions
if (state.sizeChanged) {
maybeDestroyFramebuffer(filter);
state.sizeChanged = false;
}
if (!state.isFramebufferCreated) {
state.isFramebufferCreated = true;
state.outputTexture = new GlTexture(GLES20.GL_TEXTURE0,
GLES20.GL_TEXTURE_2D,
@ -145,6 +155,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
state.outputFramebuffer = new GlFramebuffer();
state.outputFramebuffer.attach(state.outputTexture);
}
}
private void maybeDestroyFramebuffer(@NonNull Filter filter) {
State state = states.get(filter);
@ -157,11 +168,13 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
state.outputTexture = null;
}
// Any thread...
private void maybeSetSize(@NonNull Filter filter) {
State state = states.get(filter);
//noinspection ConstantConditions
if (size != null && !size.equals(state.size)) {
state.size = size;
state.sizeChanged = true;
filter.setSize(size.getWidth(), size.getHeight());
}
}

@ -7,6 +7,8 @@ import androidx.annotation.VisibleForTesting;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.Surface;
@ -27,6 +29,7 @@ public class OrientationHelper {
void onDisplayOffsetChanged(int displayOffset, boolean willRecreate);
}
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Context mContext;
private final Callback mCallback;
@ -99,16 +102,14 @@ public class OrientationHelper {
* Enables this listener.
*/
public void enable() {
if (mEnabled) {
//already enabled, will ignore call
return;
}
if (mEnabled) return;
mEnabled = true;
mDisplayOffset = findDisplayOffset();
if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager)
mContext.getSystemService(Context.DISPLAY_SERVICE);
manager.registerDisplayListener(mDisplayOffsetListener, null);
// Without the handler, this can crash if called from a thread without a looper
manager.registerDisplayListener(mDisplayOffsetListener, mHandler);
}
mDeviceOrientationListener.enable();
}

@ -33,8 +33,8 @@ public class AspectRatio implements Comparable<AspectRatio> {
@NonNull
public static AspectRatio of(int x, int y) {
int gcd = gcd(x, y);
x /= gcd;
y /= gcd;
if (gcd > 0) x /= gcd;
if (gcd > 0) y /= gcd;
String key = x + ":" + y;
AspectRatio cached = sCache.get(key);
if (cached == null) {
@ -58,8 +58,8 @@ public class AspectRatio implements Comparable<AspectRatio> {
if (parts.length != 2) {
throw new NumberFormatException("Illegal AspectRatio string. Must be x:y");
}
int x = Integer.valueOf(parts[0]);
int y = Integer.valueOf(parts[1]);
int x = Integer.parseInt(parts[0]);
int y = Integer.parseInt(parts[1]);
return of(x, y);
}
@ -80,14 +80,11 @@ public class AspectRatio implements Comparable<AspectRatio> {
}
public boolean matches(@NonNull Size size) {
int gcd = gcd(size.getWidth(), size.getHeight());
int x = size.getWidth() / gcd;
int y = size.getHeight() / gcd;
return mX == x && mY == y;
return equals(AspectRatio.of(size));
}
public boolean matches(@NonNull Size size, float tolerance) {
return Math.abs(toFloat() - (float) size.getWidth() / size.getHeight()) <= tolerance;
return Math.abs(toFloat() - AspectRatio.of(size).toFloat()) <= tolerance;
}
@Override
@ -99,8 +96,7 @@ public class AspectRatio implements Comparable<AspectRatio> {
return true;
}
if (o instanceof AspectRatio) {
AspectRatio ratio = (AspectRatio) o;
return mX == ratio.mX && mY == ratio.mY;
return toFloat() == ((AspectRatio) o).toFloat();
}
return false;
}
@ -117,17 +113,12 @@ public class AspectRatio implements Comparable<AspectRatio> {
@Override
public int hashCode() {
return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2)));
return Float.floatToIntBits(toFloat());
}
@Override
public int compareTo(@NonNull AspectRatio another) {
if (equals(another)) {
return 0;
} else if (toFloat() - another.toFloat() > 0) {
return 1;
}
return -1;
return Float.compare(toFloat(), another.toFloat());
}
/**
@ -140,6 +131,7 @@ public class AspectRatio implements Comparable<AspectRatio> {
return AspectRatio.of(mY, mX);
}
// Note: gcd(0,X) = gcd(X,0) = X (even for X=0)
private static int gcd(int a, int b) {
while (b != 0) {
int c = b;

@ -1,21 +1,23 @@
plugins {
id("com.android.application")
id("kotlin-android")
}
android {
setCompileSdkVersion(rootProject.property("compileSdkVersion") as Int)
setCompileSdkVersion(property("compileSdkVersion") as Int)
defaultConfig {
applicationId = "com.otaliastudios.cameraview.demo"
setMinSdkVersion(rootProject.property("minSdkVersion") as Int)
setTargetSdkVersion(rootProject.property("targetSdkVersion") as Int)
setMinSdkVersion(property("minSdkVersion") as Int)
setTargetSdkVersion(property("targetSdkVersion") as Int)
versionCode = 1
versionName = "1.0"
vectorDrawables.useSupportLibrary = true
}
sourceSets["main"].java.srcDir("src/main/kotlin")
}
dependencies {
implementation(project(":cameraview"))
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("com.google.android.material:material:1.1.0")
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("com.google.android.material:material:1.2.0")
}

@ -1,417 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.Toast;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraListener;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.MultiFilter;
import com.otaliastudios.cameraview.filters.BrightnessFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback {
private final static CameraLogger LOG = CameraLogger.create("DemoApp");
private final static boolean USE_FRAME_PROCESSOR = true;
private final static boolean DECODE_BITMAP = false;
private CameraView camera;
private ViewGroup controlPanel;
private long mCaptureTime;
private int mCurrentFilter = 0;
private final Filters[] mAllFilters = Filters.values();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
camera = findViewById(R.id.camera);
camera.setLifecycleOwner(this);
camera.addCameraListener(new Listener());
if (USE_FRAME_PROCESSOR) {
camera.addFrameProcessor(new FrameProcessor() {
private long lastTime = System.currentTimeMillis();
@Override
public void process(@NonNull Frame frame) {
long newTime = frame.getTime();
long delay = newTime - lastTime;
lastTime = newTime;
LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay);
if (DECODE_BITMAP) {
if (frame.getFormat() == ImageFormat.NV21
&& frame.getDataClass() == byte[].class) {
byte[] data = frame.getData();
YuvImage yuvImage = new YuvImage(data,
frame.getFormat(),
frame.getSize().getWidth(),
frame.getSize().getHeight(),
null);
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0,
frame.getSize().getWidth(),
frame.getSize().getHeight()), 100, jpegStream);
byte[] jpegByteArray = jpegStream.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray,
0, jpegByteArray.length);
//noinspection ResultOfMethodCallIgnored
bitmap.toString();
}
}
}
});
}
findViewById(R.id.edit).setOnClickListener(this);
findViewById(R.id.capturePicture).setOnClickListener(this);
findViewById(R.id.capturePictureSnapshot).setOnClickListener(this);
findViewById(R.id.captureVideo).setOnClickListener(this);
findViewById(R.id.captureVideoSnapshot).setOnClickListener(this);
findViewById(R.id.toggleCamera).setOnClickListener(this);
findViewById(R.id.changeFilter).setOnClickListener(this);
controlPanel = findViewById(R.id.controls);
ViewGroup group = (ViewGroup) controlPanel.getChildAt(0);
final View watermark = findViewById(R.id.watermark);
List<Option<?>> options = Arrays.asList(
// Layout
new Option.Width(), new Option.Height(),
// Engine and preview
new Option.Mode(), new Option.Engine(), new Option.Preview(),
// Some controls
new Option.Flash(), new Option.WhiteBalance(), new Option.Hdr(),
new Option.PictureMetering(), new Option.PictureSnapshotMetering(),
new Option.PictureFormat(),
// Video recording
new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(), new Option.AudioCodec(),
// Gestures
new Option.Pinch(), new Option.HorizontalScroll(), new Option.VerticalScroll(),
new Option.Tap(), new Option.LongTap(),
// Watermarks
new Option.OverlayInPreview(watermark),
new Option.OverlayInPictureSnapshot(watermark),
new Option.OverlayInVideoSnapshot(watermark),
// Frame Processing
new Option.FrameProcessingFormat(),
// Other
new Option.Grid(), new Option.GridColor(), new Option.UseDeviceOrientation()
);
List<Boolean> dividers = Arrays.asList(
// Layout
false, true,
// Engine and preview
false, false, true,
// Some controls
false, false, false, false, false, true,
// Video recording
false, false, false, true,
// Gestures
false, false, false, false, true,
// Watermarks
false, false, true,
// Frame Processing
true,
// Other
false, false, true
);
for (int i = 0; i < options.size(); i++) {
OptionView view = new OptionView(this);
//noinspection unchecked
view.setOption(options.get(i), this);
view.setHasDivider(dividers.get(i));
group.addView(view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
controlPanel.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_HIDDEN);
}
});
// Animate the watermark just to show we record the animation in video snapshots
ValueAnimator animator = ValueAnimator.ofFloat(1F, 0.8F);
animator.setDuration(300);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float scale = (float) animation.getAnimatedValue();
watermark.setScaleX(scale);
watermark.setScaleY(scale);
watermark.setRotation(watermark.getRotation() + 2);
}
});
animator.start();
}
private void message(@NonNull String content, boolean important) {
if (important) {
LOG.w(content);
Toast.makeText(this, content, Toast.LENGTH_LONG).show();
} else {
LOG.i(content);
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
}
}
private class Listener extends CameraListener {
@Override
public void onCameraOpened(@NonNull CameraOptions options) {
ViewGroup group = (ViewGroup) controlPanel.getChildAt(0);
for (int i = 0; i < group.getChildCount(); i++) {
OptionView view = (OptionView) group.getChildAt(i);
view.onCameraOpened(camera, options);
}
}
@Override
public void onCameraError(@NonNull CameraException exception) {
super.onCameraError(exception);
message("Got CameraException #" + exception.getReason(), true);
}
@Override
public void onPictureTaken(@NonNull PictureResult result) {
super.onPictureTaken(result);
if (camera.isTakingVideo()) {
message("Captured while taking video. Size=" + result.getSize(), false);
return;
}
// This can happen if picture was taken with a gesture.
long callbackTime = System.currentTimeMillis();
if (mCaptureTime == 0) mCaptureTime = callbackTime - 300;
LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - mCaptureTime);
PicturePreviewActivity.setPictureResult(result);
Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class);
intent.putExtra("delay", callbackTime - mCaptureTime);
startActivity(intent);
mCaptureTime = 0;
LOG.w("onPictureTaken called! Launched activity.");
}
@Override
public void onVideoTaken(@NonNull VideoResult result) {
super.onVideoTaken(result);
LOG.w("onVideoTaken called! Launching activity.");
VideoPreviewActivity.setVideoResult(result);
Intent intent = new Intent(CameraActivity.this, VideoPreviewActivity.class);
startActivity(intent);
LOG.w("onVideoTaken called! Launched activity.");
}
@Override
public void onVideoRecordingStart() {
super.onVideoRecordingStart();
LOG.w("onVideoRecordingStart!");
}
@Override
public void onVideoRecordingEnd() {
super.onVideoRecordingEnd();
message("Video taken. Processing...", false);
LOG.w("onVideoRecordingEnd!");
}
@Override
public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) {
super.onExposureCorrectionChanged(newValue, bounds, fingers);
message("Exposure correction:" + newValue, false);
}
@Override
public void onZoomChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) {
super.onZoomChanged(newValue, bounds, fingers);
message("Zoom:" + newValue, false);
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.edit: edit(); break;
case R.id.capturePicture: capturePicture(); break;
case R.id.capturePictureSnapshot: capturePictureSnapshot(); break;
case R.id.captureVideo: captureVideo(); break;
case R.id.captureVideoSnapshot: captureVideoSnapshot(); break;
case R.id.toggleCamera: toggleCamera(); break;
case R.id.changeFilter: changeCurrentFilter(); break;
}
}
@Override
public void onBackPressed() {
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
if (b.getState() != BottomSheetBehavior.STATE_HIDDEN) {
b.setState(BottomSheetBehavior.STATE_HIDDEN);
return;
}
super.onBackPressed();
}
private void edit() {
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
private void capturePicture() {
if (camera.getMode() == Mode.VIDEO) {
message("Can't take HQ pictures while in VIDEO mode.", false);
return;
}
if (camera.isTakingPicture()) return;
mCaptureTime = System.currentTimeMillis();
message("Capturing picture...", false);
camera.takePicture();
}
private void capturePictureSnapshot() {
if (camera.isTakingPicture()) return;
if (camera.getPreview() != Preview.GL_SURFACE) {
message("Picture snapshots are only allowed with the GL_SURFACE preview.", true);
return;
}
mCaptureTime = System.currentTimeMillis();
message("Capturing picture snapshot...", false);
camera.takePictureSnapshot();
}
private void captureVideo() {
if (camera.getMode() == Mode.PICTURE) {
message("Can't record HQ videos while in PICTURE mode.", false);
return;
}
if (camera.isTakingPicture() || camera.isTakingVideo()) return;
message("Recording for 5 seconds...", true);
camera.takeVideo(new File(getFilesDir(), "video.mp4"), 5000);
}
private void captureVideoSnapshot() {
if (camera.isTakingVideo()) {
message("Already taking video.", false);
return;
}
if (camera.getPreview() != Preview.GL_SURFACE) {
message("Video snapshots are only allowed with the GL_SURFACE preview.", true);
return;
}
message("Recording snapshot for 5 seconds...", true);
camera.takeVideoSnapshot(new File(getFilesDir(), "video.mp4"), 5000);
}
private void toggleCamera() {
if (camera.isTakingPicture() || camera.isTakingVideo()) return;
switch (camera.toggleFacing()) {
case BACK:
message("Switched to back camera!", false);
break;
case FRONT:
message("Switched to front camera!", false);
break;
}
}
private void changeCurrentFilter() {
if (camera.getPreview() != Preview.GL_SURFACE) {
message("Filters are supported only when preview is Preview.GL_SURFACE.", true);
return;
}
if (mCurrentFilter < mAllFilters.length - 1) {
mCurrentFilter++;
} else {
mCurrentFilter = 0;
}
Filters filter = mAllFilters[mCurrentFilter];
message(filter.toString(), false);
// Normal behavior:
camera.setFilter(filter.newInstance());
// To test MultiFilter:
// DuotoneFilter duotone = new DuotoneFilter();
// duotone.setFirstColor(Color.RED);
// duotone.setSecondColor(Color.GREEN);
// camera.setFilter(new MultiFilter(duotone, filter.newInstance()));
}
@Override
public <T> boolean onValueChanged(@NonNull Option<T> option, @NonNull T value, @NonNull String name) {
if ((option instanceof Option.Width || option instanceof Option.Height)) {
Preview preview = camera.getPreview();
boolean wrapContent = (Integer) value == ViewGroup.LayoutParams.WRAP_CONTENT;
if (preview == Preview.SURFACE && !wrapContent) {
message("The SurfaceView preview does not support width or height changes. " +
"The view will act as WRAP_CONTENT by default.", true);
return false;
}
}
option.set(camera, value);
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_HIDDEN);
message("Changed " + option.getName() + " to " + name, false);
return true;
}
//region Permissions
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean valid = true;
for (int grantResult : grantResults) {
valid = valid && grantResult == PackageManager.PERMISSION_GRANTED;
}
if (valid && !camera.isOpened()) {
camera.open();
}
}
//endregion
}

@ -1,46 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
public class MessageView extends LinearLayout {
private TextView message;
private TextView title;
public MessageView(Context context) {
this(context, null);
}
public MessageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MessageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
inflate(context, R.layout.option_view, this);
ViewGroup content = findViewById(R.id.content);
inflate(context, R.layout.spinner_text, content);
title = findViewById(R.id.title);
message = (TextView) content.getChildAt(0);
}
public void setTitleAndMessage(String title, String message) {
setTitle(title);
setMessage(message);
}
public void setTitle(String title) {
this.title.setText(title);
}
public void setMessage(String message) {
this.message.setText(message);
}
}

@ -1,605 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import android.graphics.ImageFormat;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.CameraListener;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.overlay.OverlayLayout;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Controls that we want to display in a ControlView.
*/
@SuppressWarnings("WeakerAccess")
public abstract class Option<T> {
private String name;
private Option(@NonNull String name) {
this.name = name;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public final String getName() {
return name;
}
@NonNull
public abstract T get(@NonNull CameraView view);
@NonNull
public abstract Collection<T> getAll(@NonNull CameraView view, @NonNull CameraOptions options);
public abstract void set(@NonNull CameraView view, @NonNull T value);
@NonNull
public String toString(@NonNull T value) {
return String.valueOf(value).replace("_", " ").toLowerCase();
}
public static class Width extends Option<Integer> {
public Width() {
super("Width");
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
View root = (View) view.getParent();
List<Integer> list = new ArrayList<>();
int boundary = root.getWidth();
if (boundary == 0) boundary = 1000;
int step = boundary / 10;
list.add(ViewGroup.LayoutParams.WRAP_CONTENT);
list.add(ViewGroup.LayoutParams.MATCH_PARENT);
for (int i = step; i < boundary; i += step) {
list.add(i);
}
return list;
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return view.getLayoutParams().width;
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.getLayoutParams().width = (int) value;
view.setLayoutParams(view.getLayoutParams());
}
@NonNull
@Override
public String toString(@NonNull Integer value) {
if (value == ViewGroup.LayoutParams.MATCH_PARENT) return "match parent";
if (value == ViewGroup.LayoutParams.WRAP_CONTENT) return "wrap content";
return super.toString(value);
}
}
public static class Height extends Option<Integer> {
public Height() {
super("Height");
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
View root = (View) view.getParent();
ArrayList<Integer> list = new ArrayList<>();
int boundary = root.getHeight();
if (boundary == 0) boundary = 1000;
int step = boundary / 10;
list.add(ViewGroup.LayoutParams.WRAP_CONTENT);
list.add(ViewGroup.LayoutParams.MATCH_PARENT);
for (int i = step; i < boundary; i += step) {
list.add(i);
}
return list;
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return view.getLayoutParams().height;
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.getLayoutParams().height = (int) value;
view.setLayoutParams(view.getLayoutParams());
}
@NonNull
@Override
public String toString(@NonNull Integer value) {
if (value == ViewGroup.LayoutParams.MATCH_PARENT) return "match parent";
if (value == ViewGroup.LayoutParams.WRAP_CONTENT) return "wrap content";
return super.toString(value);
}
}
private static abstract class ControlOption<T extends Control> extends Option<T> {
private final Class<T> controlClass;
ControlOption(@NonNull Class<T> controlClass, String name) {
super(name);
this.controlClass = controlClass;
}
@Override
public void set(@NonNull CameraView view, @NonNull T value) {
view.set(value);
}
@NonNull
@Override
public T get(@NonNull CameraView view) {
return view.get(controlClass);
}
@NonNull
@Override
public Collection<T> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return options.getSupportedControls(controlClass);
}
}
public static class Mode extends ControlOption<com.otaliastudios.cameraview.controls.Mode> {
public Mode() {
super(com.otaliastudios.cameraview.controls.Mode.class, "Mode");
}
}
public static class Engine extends ControlOption<com.otaliastudios.cameraview.controls.Engine> {
public Engine() {
super(com.otaliastudios.cameraview.controls.Engine.class, "Engine");
}
@Override
public void set(final @NonNull CameraView view, final @NonNull com.otaliastudios.cameraview.controls.Engine value) {
boolean started = view.isOpened();
if (started) {
view.addCameraListener(new CameraListener() {
@Override
public void onCameraClosed() {
super.onCameraClosed();
view.removeCameraListener(this);
view.setEngine(value);
view.open();
}
});
view.close();
} else {
view.setEngine(value);
}
}
}
public static class Preview extends ControlOption<com.otaliastudios.cameraview.controls.Preview> {
public Preview() {
super(com.otaliastudios.cameraview.controls.Preview.class, "Preview Surface");
}
@Override
public void set(final @NonNull CameraView view, final @NonNull com.otaliastudios.cameraview.controls.Preview value) {
boolean opened = view.isOpened();
if (opened) {
view.addCameraListener(new CameraListener() {
@Override
public void onCameraClosed() {
super.onCameraClosed();
view.removeCameraListener(this);
applyPreview(view, value);
view.open();
}
});
view.close();
} else {
applyPreview(view, value);
}
}
// This is really tricky since the preview can only be changed when not attached to window.
private void applyPreview(@NonNull CameraView cameraView,
@NonNull com.otaliastudios.cameraview.controls.Preview newPreview) {
ViewGroup.LayoutParams params = cameraView.getLayoutParams();
ViewGroup parent = (ViewGroup) cameraView.getParent();
int index = 0;
for (int i = 0; i < parent.getChildCount(); i++) {
if (parent.getChildAt(i) == cameraView) {
index = i;
break;
}
}
parent.removeView(cameraView);
cameraView.setPreview(newPreview);
parent.addView(cameraView, index, params);
}
}
public static class Flash extends ControlOption<com.otaliastudios.cameraview.controls.Flash> {
public Flash() {
super(com.otaliastudios.cameraview.controls.Flash.class, "Flash");
}
}
public static class WhiteBalance extends ControlOption<com.otaliastudios.cameraview.controls.WhiteBalance> {
public WhiteBalance() {
super(com.otaliastudios.cameraview.controls.WhiteBalance.class, "White Balance");
}
}
public static class Hdr extends ControlOption<com.otaliastudios.cameraview.controls.Hdr> {
public Hdr() {
super(com.otaliastudios.cameraview.controls.Hdr.class, "HDR");
}
}
public static class PictureMetering extends Option<Boolean> {
public PictureMetering() {
super("Picture Metering");
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
return view.getPictureMetering();
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
view.setPictureMetering(value);
}
}
public static class PictureSnapshotMetering extends Option<Boolean> {
public PictureSnapshotMetering() {
super("Picture Snapshot Metering");
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
return view.getPictureSnapshotMetering();
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
view.setPictureSnapshotMetering(value);
}
}
public static class VideoCodec extends ControlOption<com.otaliastudios.cameraview.controls.VideoCodec> {
public VideoCodec() {
super(com.otaliastudios.cameraview.controls.VideoCodec.class, "Video Codec");
}
}
public static class AudioCodec extends ControlOption<com.otaliastudios.cameraview.controls.AudioCodec> {
public AudioCodec() {
super(com.otaliastudios.cameraview.controls.AudioCodec.class, "Audio Codec");
}
}
public static class Audio extends ControlOption<com.otaliastudios.cameraview.controls.Audio> {
public Audio() {
super(com.otaliastudios.cameraview.controls.Audio.class, "Audio");
}
}
private static abstract class GestureOption extends Option<GestureAction> {
private final Gesture gesture;
private final GestureAction[] allActions = GestureAction.values();
GestureOption(@NonNull Gesture gesture, String name) {
super(name);
this.gesture = gesture;
}
@Override
public void set(@NonNull CameraView view, @NonNull GestureAction value) {
view.mapGesture(gesture, value);
}
@NonNull
@Override
public GestureAction get(@NonNull CameraView view) {
return view.getGestureAction(gesture);
}
@NonNull
@Override
public Collection<GestureAction> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
List<GestureAction> list = new ArrayList<>();
for (GestureAction action : allActions) {
if (gesture.isAssignableTo(action) && options.supports(action)) {
list.add(action);
}
}
return list;
}
}
public static class Pinch extends GestureOption {
public Pinch() {
super(Gesture.PINCH, "Pinch");
}
}
public static class HorizontalScroll extends GestureOption {
public HorizontalScroll() {
super(Gesture.SCROLL_HORIZONTAL, "Horizontal Scroll");
}
}
public static class VerticalScroll extends GestureOption {
public VerticalScroll() {
super(Gesture.SCROLL_VERTICAL, "Vertical Scroll");
}
}
public static class Tap extends GestureOption {
public Tap() {
super(Gesture.TAP, "Tap");
}
}
public static class LongTap extends GestureOption {
public LongTap() {
super(Gesture.LONG_TAP, "Long Tap");
}
}
private static abstract class OverlayOption extends Option<Boolean> {
private View overlay;
private Overlay.Target target;
OverlayOption(@NonNull Overlay.Target target, @NonNull String name, @NonNull View overlay) {
super(name);
this.overlay = overlay;
this.target = target;
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams();
switch (target) {
case PREVIEW: return params.drawOnPreview;
case PICTURE_SNAPSHOT: return params.drawOnPictureSnapshot;
case VIDEO_SNAPSHOT: return params.drawOnVideoSnapshot;
}
return false;
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams();
switch (target) {
case PREVIEW: params.drawOnPreview = value; break;
case PICTURE_SNAPSHOT: params.drawOnPictureSnapshot = value; break;
case VIDEO_SNAPSHOT: params.drawOnVideoSnapshot = value; break;
}
overlay.setLayoutParams(params);
}
}
public static class OverlayInPreview extends OverlayOption {
public OverlayInPreview(@NonNull View overlay) {
super(Overlay.Target.PREVIEW, "Overlay in Preview", overlay);
}
}
public static class OverlayInPictureSnapshot extends OverlayOption {
public OverlayInPictureSnapshot(@NonNull View overlay) {
super(Overlay.Target.PICTURE_SNAPSHOT, "Overlay in Picture Snapshot", overlay);
}
}
public static class OverlayInVideoSnapshot extends OverlayOption {
public OverlayInVideoSnapshot(@NonNull View overlay) {
super(Overlay.Target.VIDEO_SNAPSHOT, "Overlay in Video Snapshot", overlay);
}
}
public static class Grid extends ControlOption<com.otaliastudios.cameraview.controls.Grid> {
public Grid() {
super(com.otaliastudios.cameraview.controls.Grid.class, "Grid Lines");
}
}
public static class GridColor extends Option<Pair<Integer, String>> {
public GridColor() {
super("Grid Color");
}
private static final List<Pair<Integer, String>> ALL = Arrays.asList(
new Pair<>(Color.argb(160, 255, 255, 255), "default"),
new Pair<>(Color.WHITE, "white"),
new Pair<>(Color.BLACK, "black"),
new Pair<>(Color.YELLOW, "yellow")
);
@NonNull
@Override
public Collection<Pair<Integer, String>> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return ALL;
}
@NonNull
@Override
public Pair<Integer, String> get(@NonNull CameraView view) {
for (Pair<Integer, String> pair : ALL) {
//noinspection ConstantConditions
if (pair.first == view.getGridColor()) {
return pair;
}
}
throw new RuntimeException("Could not find grid color");
}
@Override
public void set(@NonNull CameraView view, @NonNull Pair<Integer, String> value) {
//noinspection ConstantConditions
view.setGridColor(value.first);
}
@NonNull
@Override
public String toString(@NonNull Pair<Integer, String> value) {
//noinspection ConstantConditions
return value.second;
}
}
public static class UseDeviceOrientation extends Option<Boolean> {
public UseDeviceOrientation() {
super("Use Device Orientation");
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
return view.getUseDeviceOrientation();
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
view.setUseDeviceOrientation(value);
}
}
public static class PreviewFrameRate extends Option<Integer> {
public PreviewFrameRate() {
super("Preview FPS");
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
float min = options.getPreviewFrameRateMinValue();
float max = options.getPreviewFrameRateMaxValue();
float delta = max - min;
List<Integer> result = new ArrayList<>();
if (min == 0F && max == 0F) {
return result; // empty list
} else if (delta < 0.005F) {
result.add(Math.round(min));
return result; // single value
} else {
final int steps = 3;
final float step = delta / steps;
for (int i = 0; i <= steps; i++) {
result.add(Math.round(min));
min += step;
}
return result;
}
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return Math.round(view.getPreviewFrameRate());
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.setPreviewFrameRate((float) value);
}
}
public static class PictureFormat extends ControlOption<com.otaliastudios.cameraview.controls.PictureFormat> {
public PictureFormat() {
super(com.otaliastudios.cameraview.controls.PictureFormat.class, "Picture Format");
}
}
public static class FrameProcessingFormat extends Option<Integer> {
FrameProcessingFormat() {
super("Frame Processing Format");
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.setFrameProcessingFormat(value);
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return view.getFrameProcessingFormat();
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return options.getSupportedFrameProcessingFormats();
}
@NonNull
@Override
public String toString(@NonNull Integer value) {
switch (value) {
case ImageFormat.NV21: return "NV21";
case ImageFormat.NV16: return "NV16";
case ImageFormat.JPEG: return "JPEG";
case ImageFormat.YUY2: return "YUY2";
case ImageFormat.YUV_420_888: return "YUV_420_888";
case ImageFormat.YUV_422_888: return "YUV_422_888";
case ImageFormat.YUV_444_888: return "YUV_444_888";
case ImageFormat.RAW10: return "RAW10";
case ImageFormat.RAW12: return "RAW12";
case ImageFormat.RAW_SENSOR: return "RAW_SENSOR";
}
return super.toString(value);
}
}
}

@ -1,97 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
import java.util.ArrayList;
@SuppressLint("ViewConstructor")
public class OptionView<Value> extends LinearLayout implements Spinner.OnItemSelectedListener {
interface Callback {
<T> boolean onValueChanged(@NonNull Option<T> option, @NonNull T value, @NonNull String name);
}
private Value value;
private ArrayList<Value> values;
private ArrayList<String> valuesStrings;
private Option option;
private Callback callback;
private Spinner spinner;
public OptionView(@NonNull Context context) {
super(context);
setOrientation(VERTICAL);
inflate(context, R.layout.option_view, this);
ViewGroup content = findViewById(R.id.content);
spinner = new Spinner(context, Spinner.MODE_DROPDOWN);
content.addView(spinner);
}
public void setHasDivider(boolean hasDivider) {
View divider = findViewById(R.id.divider);
divider.setVisibility(hasDivider ? View.VISIBLE : View.GONE);
}
public void setOption(@NonNull Option<Value> option, @NonNull Callback callback) {
this.option = option;
this.callback = callback;
TextView title = findViewById(R.id.title);
title.setText(option.getName());
}
@SuppressWarnings("all")
public void onCameraOpened(CameraView view, CameraOptions options) {
values = new ArrayList(option.getAll(view, options));
value = (Value) option.get(view);
valuesStrings = new ArrayList<>();
for (Value value : values) {
valuesStrings.add(option.toString(value));
}
if (values.isEmpty()) {
spinner.setOnItemSelectedListener(null);
spinner.setEnabled(false);
spinner.setAlpha(0.8f);
spinner.setAdapter(new ArrayAdapter(getContext(),
R.layout.spinner_text, new String[]{ "Not supported." }));
spinner.setSelection(0, false);
} else {
spinner.setEnabled(true);
spinner.setAlpha(1f);
spinner.setAdapter(new ArrayAdapter(getContext(),
R.layout.spinner_text, valuesStrings));
spinner.setSelection(values.indexOf(value), false);
spinner.setOnItemSelectedListener(this);
}
}
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
if (!values.get(i).equals(value)) {
Log.e("ControlView", "curr: " + value + " new: " + values.get(i));
if (!callback.onValueChanged(option, values.get(i), valuesStrings.get(i))) {
spinner.setSelection(values.indexOf(value)); // Go back.
} else {
value = values.get(i);
}
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {}
}

@ -1,136 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.Toast;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.FileCallback;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.BitmapCallback;
import com.otaliastudios.cameraview.PictureResult;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class PicturePreviewActivity extends AppCompatActivity {
private static PictureResult picture;
public static void setPictureResult(@Nullable PictureResult pictureResult) {
picture = pictureResult;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_picture_preview);
final PictureResult result = picture;
if (result == null) {
finish();
return;
}
final ImageView imageView = findViewById(R.id.image);
final MessageView captureResolution = findViewById(R.id.nativeCaptureResolution);
final MessageView captureLatency = findViewById(R.id.captureLatency);
final MessageView exifRotation = findViewById(R.id.exifRotation);
final long delay = getIntent().getLongExtra("delay", 0);
AspectRatio ratio = AspectRatio.of(result.getSize());
captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds");
captureResolution.setTitleAndMessage("Resolution", result.getSize() + " (" + ratio + ")");
exifRotation.setTitleAndMessage("EXIF rotation", result.getRotation() + "");
try {
result.toBitmap(1000, 1000, new BitmapCallback() {
@Override
public void onBitmapReady(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
});
} catch (UnsupportedOperationException e) {
imageView.setImageDrawable(new ColorDrawable(Color.GREEN));
Toast.makeText(this, "Can't preview this format: " + picture.getFormat(),
Toast.LENGTH_LONG).show();
}
if (result.isSnapshot()) {
// Log the real size for debugging reason.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(result.getData(), 0, result.getData().length, options);
if (result.getRotation() % 180 != 0) {
Log.e("PicturePreview", "The picture full size is " + result.getSize().getHeight() + "x" + result.getSize().getWidth());
} else {
Log.e("PicturePreview", "The picture full size is " + result.getSize().getWidth() + "x" + result.getSize().getHeight());
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
setPictureResult(null);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.share, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show();
String extension;
switch (picture.getFormat()) {
case JPEG: extension = "jpg"; break;
case DNG: extension = "dng"; break;
default: throw new RuntimeException("Unknown format.");
}
File file = new File(getFilesDir(), "picture." + extension);
CameraUtils.writeToFile(picture.getData(), file, new FileCallback() {
@Override
public void onFileReady(@Nullable File file) {
if (file != null) {
Context context = PicturePreviewActivity.this;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/*");
Uri uri = FileProvider.getUriForFile(context,
context.getPackageName() + ".provider",
file);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
} else {
Toast.makeText(PicturePreviewActivity.this,
"Error while writing file.",
Toast.LENGTH_SHORT).show();
}
}
});
return true;
}
return super.onOptionsItemSelected(item);
}
}

@ -1,136 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.MediaController;
import android.widget.Toast;
import android.widget.VideoView;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.FileCallback;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.size.AspectRatio;
import java.io.File;
public class VideoPreviewActivity extends AppCompatActivity {
private VideoView videoView;
private static VideoResult videoResult;
public static void setVideoResult(@Nullable VideoResult result) {
videoResult = result;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_preview);
final VideoResult result = videoResult;
if (result == null) {
finish();
return;
}
videoView = findViewById(R.id.video);
videoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
playVideo();
}
});
final MessageView actualResolution = findViewById(R.id.actualResolution);
final MessageView isSnapshot = findViewById(R.id.isSnapshot);
final MessageView rotation = findViewById(R.id.rotation);
final MessageView audio = findViewById(R.id.audio);
final MessageView audioBitRate = findViewById(R.id.audioBitRate);
final MessageView videoCodec = findViewById(R.id.videoCodec);
final MessageView audioCodec = findViewById(R.id.audioCodec);
final MessageView videoBitRate = findViewById(R.id.videoBitRate);
final MessageView videoFrameRate = findViewById(R.id.videoFrameRate);
AspectRatio ratio = AspectRatio.of(result.getSize());
actualResolution.setTitleAndMessage("Size", result.getSize() + " (" + ratio + ")");
isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot() + "");
rotation.setTitleAndMessage("Rotation", result.getRotation() + "");
audio.setTitleAndMessage("Audio", result.getAudio().name());
audioBitRate.setTitleAndMessage("Audio bit rate", result.getAudioBitRate() + " bits per sec.");
videoCodec.setTitleAndMessage("VideoCodec", result.getVideoCodec().name());
audioCodec.setTitleAndMessage("AudioCodec", result.getAudioCodec().name());
videoBitRate.setTitleAndMessage("Video bit rate", result.getVideoBitRate() + " bits per sec.");
videoFrameRate.setTitleAndMessage("Video frame rate", result.getVideoFrameRate() + " fps");
MediaController controller = new MediaController(this);
controller.setAnchorView(videoView);
controller.setMediaPlayer(videoView);
videoView.setMediaController(controller);
videoView.setVideoURI(Uri.fromFile(result.getFile()));
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
ViewGroup.LayoutParams lp = videoView.getLayoutParams();
float videoWidth = mp.getVideoWidth();
float videoHeight = mp.getVideoHeight();
float viewWidth = videoView.getWidth();
lp.height = (int) (viewWidth * (videoHeight / videoWidth));
videoView.setLayoutParams(lp);
playVideo();
if (result.isSnapshot()) {
// Log the real size for debugging reason.
Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight);
}
}
});
}
void playVideo() {
if (!videoView.isPlaying()) {
videoView.start();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
setVideoResult(null);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.share, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("video/*");
Uri uri = FileProvider.getUriForFile(this,
this.getPackageName() + ".provider",
videoResult.getFile());
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
}

@ -0,0 +1,344 @@
package com.otaliastudios.cameraview.demo
import android.animation.ValueAnimator
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.*
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.otaliastudios.cameraview.*
import com.otaliastudios.cameraview.controls.Facing
import com.otaliastudios.cameraview.controls.Mode
import com.otaliastudios.cameraview.controls.Preview
import com.otaliastudios.cameraview.filter.Filters
import com.otaliastudios.cameraview.frame.Frame
import com.otaliastudios.cameraview.frame.FrameProcessor
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*
class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Callback {
companion object {
private val LOG = CameraLogger.create("DemoApp")
private const val USE_FRAME_PROCESSOR = true
private const val DECODE_BITMAP = false
}
private val camera: CameraView by lazy { findViewById(R.id.camera) }
private val controlPanel: ViewGroup by lazy { findViewById(R.id.controls) }
private var captureTime: Long = 0
private var currentFilter = 0
private val allFilters = Filters.values()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE)
camera.setLifecycleOwner(this)
camera.addCameraListener(Listener())
if (USE_FRAME_PROCESSOR) {
camera.addFrameProcessor(object : FrameProcessor {
private var lastTime = System.currentTimeMillis()
override fun process(frame: Frame) {
val newTime = frame.time
val delay = newTime - lastTime
lastTime = newTime
LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay)
if (DECODE_BITMAP) {
if (frame.format == ImageFormat.NV21
&& frame.dataClass == ByteArray::class.java) {
val data = frame.getData<ByteArray>()
val yuvImage = YuvImage(data,
frame.format,
frame.size.width,
frame.size.height,
null)
val jpegStream = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0,
frame.size.width,
frame.size.height), 100, jpegStream)
val jpegByteArray = jpegStream.toByteArray()
val bitmap = BitmapFactory.decodeByteArray(jpegByteArray,
0, jpegByteArray.size)
bitmap.toString()
}
}
}
})
}
findViewById<View>(R.id.edit).setOnClickListener(this)
findViewById<View>(R.id.capturePicture).setOnClickListener(this)
findViewById<View>(R.id.capturePictureSnapshot).setOnClickListener(this)
findViewById<View>(R.id.captureVideo).setOnClickListener(this)
findViewById<View>(R.id.captureVideoSnapshot).setOnClickListener(this)
findViewById<View>(R.id.toggleCamera).setOnClickListener(this)
findViewById<View>(R.id.changeFilter).setOnClickListener(this)
val group = controlPanel.getChildAt(0) as ViewGroup
val watermark = findViewById<View>(R.id.watermark)
val options: List<Option<*>> = listOf(
// Layout
Option.Width(), Option.Height(),
// Engine and preview
Option.Mode(), Option.Engine(), Option.Preview(),
// Some controls
Option.Flash(), Option.WhiteBalance(), Option.Hdr(),
Option.PictureMetering(), Option.PictureSnapshotMetering(),
Option.PictureFormat(),
// Video recording
Option.PreviewFrameRate(), Option.VideoCodec(), Option.Audio(), Option.AudioCodec(),
// Gestures
Option.Pinch(), Option.HorizontalScroll(), Option.VerticalScroll(),
Option.Tap(), Option.LongTap(),
// Watermarks
Option.OverlayInPreview(watermark),
Option.OverlayInPictureSnapshot(watermark),
Option.OverlayInVideoSnapshot(watermark),
// Frame Processing
Option.FrameProcessingFormat(),
// Other
Option.Grid(), Option.GridColor(), Option.UseDeviceOrientation()
)
val dividers = listOf(
// Layout
false, true,
// Engine and preview
false, false, true,
// Some controls
false, false, false, false, false, true,
// Video recording
false, false, false, true,
// Gestures
false, false, false, false, true,
// Watermarks
false, false, true,
// Frame Processing
true,
// Other
false, false, true
)
for (i in options.indices) {
val view = OptionView<Any>(this)
view.setOption(options[i] as Option<Any>, this)
view.setHasDivider(dividers[i])
group.addView(view, MATCH_PARENT, WRAP_CONTENT)
}
controlPanel.viewTreeObserver.addOnGlobalLayoutListener {
BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_HIDDEN
}
// Animate the watermark just to show we record the animation in video snapshots
val animator = ValueAnimator.ofFloat(1f, 0.8f)
animator.duration = 300
animator.repeatCount = ValueAnimator.INFINITE
animator.repeatMode = ValueAnimator.REVERSE
animator.addUpdateListener { animation ->
val scale = animation.animatedValue as Float
watermark.scaleX = scale
watermark.scaleY = scale
watermark.rotation = watermark.rotation + 2
}
animator.start()
}
private fun message(content: String, important: Boolean) {
if (important) {
LOG.w(content)
Toast.makeText(this, content, Toast.LENGTH_LONG).show()
} else {
LOG.i(content)
Toast.makeText(this, content, Toast.LENGTH_SHORT).show()
}
}
private inner class Listener : CameraListener() {
override fun onCameraOpened(options: CameraOptions) {
val group = controlPanel.getChildAt(0) as ViewGroup
for (i in 0 until group.childCount) {
val view = group.getChildAt(i) as OptionView<*>
view.onCameraOpened(camera, options)
}
}
override fun onCameraError(exception: CameraException) {
super.onCameraError(exception)
message("Got CameraException #" + exception.reason, true)
}
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
if (camera.isTakingVideo) {
message("Captured while taking video. Size=" + result.size, false)
return
}
// This can happen if picture was taken with a gesture.
val callbackTime = System.currentTimeMillis()
if (captureTime == 0L) captureTime = callbackTime - 300
LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - captureTime)
PicturePreviewActivity.pictureResult = result
val intent = Intent(this@CameraActivity, PicturePreviewActivity::class.java)
intent.putExtra("delay", callbackTime - captureTime)
startActivity(intent)
captureTime = 0
LOG.w("onPictureTaken called! Launched activity.")
}
override fun onVideoTaken(result: VideoResult) {
super.onVideoTaken(result)
LOG.w("onVideoTaken called! Launching activity.")
VideoPreviewActivity.videoResult = result
val intent = Intent(this@CameraActivity, VideoPreviewActivity::class.java)
startActivity(intent)
LOG.w("onVideoTaken called! Launched activity.")
}
override fun onVideoRecordingStart() {
super.onVideoRecordingStart()
LOG.w("onVideoRecordingStart!")
}
override fun onVideoRecordingEnd() {
super.onVideoRecordingEnd()
message("Video taken. Processing...", false)
LOG.w("onVideoRecordingEnd!")
}
override fun onExposureCorrectionChanged(newValue: Float, bounds: FloatArray, fingers: Array<PointF>?) {
super.onExposureCorrectionChanged(newValue, bounds, fingers)
message("Exposure correction:$newValue", false)
}
override fun onZoomChanged(newValue: Float, bounds: FloatArray, fingers: Array<PointF>?) {
super.onZoomChanged(newValue, bounds, fingers)
message("Zoom:$newValue", false)
}
}
override fun onClick(view: View) {
when (view.id) {
R.id.edit -> edit()
R.id.capturePicture -> capturePicture()
R.id.capturePictureSnapshot -> capturePictureSnapshot()
R.id.captureVideo -> captureVideo()
R.id.captureVideoSnapshot -> captureVideoSnapshot()
R.id.toggleCamera -> toggleCamera()
R.id.changeFilter -> changeCurrentFilter()
}
}
override fun onBackPressed() {
val b = BottomSheetBehavior.from(controlPanel)
if (b.state != BottomSheetBehavior.STATE_HIDDEN) {
b.state = BottomSheetBehavior.STATE_HIDDEN
return
}
super.onBackPressed()
}
private fun edit() {
BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_COLLAPSED
}
private fun capturePicture() {
if (camera.mode == Mode.VIDEO) return run {
message("Can't take HQ pictures while in VIDEO mode.", false)
}
if (camera.isTakingPicture) return
captureTime = System.currentTimeMillis()
message("Capturing picture...", false)
camera.takePicture()
}
private fun capturePictureSnapshot() {
if (camera.isTakingPicture) return
if (camera.preview != Preview.GL_SURFACE) return run {
message("Picture snapshots are only allowed with the GL_SURFACE preview.", true)
}
captureTime = System.currentTimeMillis()
message("Capturing picture snapshot...", false)
camera.takePictureSnapshot()
}
private fun captureVideo() {
if (camera.mode == Mode.PICTURE) return run {
message("Can't record HQ videos while in PICTURE mode.", false)
}
if (camera.isTakingPicture || camera.isTakingVideo) return
message("Recording for 5 seconds...", true)
camera.takeVideo(File(filesDir, "video.mp4"), 5000)
}
private fun captureVideoSnapshot() {
if (camera.isTakingVideo) return run {
message("Already taking video.", false)
}
if (camera.preview != Preview.GL_SURFACE) return run {
message("Video snapshots are only allowed with the GL_SURFACE preview.", true)
}
message("Recording snapshot for 5 seconds...", true)
camera.takeVideoSnapshot(File(filesDir, "video.mp4"), 5000)
}
private fun toggleCamera() {
if (camera.isTakingPicture || camera.isTakingVideo) return
when (camera.toggleFacing()) {
Facing.BACK -> message("Switched to back camera!", false)
Facing.FRONT -> message("Switched to front camera!", false)
}
}
private fun changeCurrentFilter() {
if (camera.preview != Preview.GL_SURFACE) return run {
message("Filters are supported only when preview is Preview.GL_SURFACE.", true)
}
if (currentFilter < allFilters.size - 1) {
currentFilter++
} else {
currentFilter = 0
}
val filter = allFilters[currentFilter]
message(filter.toString(), false)
// Normal behavior:
camera.filter = filter.newInstance()
// To test MultiFilter:
// DuotoneFilter duotone = new DuotoneFilter();
// duotone.setFirstColor(Color.RED);
// duotone.setSecondColor(Color.GREEN);
// camera.setFilter(new MultiFilter(duotone, filter.newInstance()));
}
override fun <T : Any> onValueChanged(option: Option<T>, value: T, name: String): Boolean {
if (option is Option.Width || option is Option.Height) {
val preview = camera.preview
val wrapContent = value as Int == WRAP_CONTENT
if (preview == Preview.SURFACE && !wrapContent) {
message("The SurfaceView preview does not support width or height changes. " +
"The view will act as WRAP_CONTENT by default.", true)
return false
}
}
option.set(camera, value)
BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_HIDDEN
message("Changed " + option.name + " to " + name, false)
return true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val valid = grantResults.all { it == PERMISSION_GRANTED }
if (valid && !camera.isOpened) {
camera.open()
}
}
}

@ -0,0 +1,34 @@
package com.otaliastudios.cameraview.demo
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
class MessageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
init {
orientation = VERTICAL
View.inflate(context, R.layout.option_view, this)
val content = findViewById<ViewGroup>(R.id.content)
View.inflate(context, R.layout.spinner_text, content)
}
private val message: TextView = findViewById<ViewGroup>(R.id.content).getChildAt(0) as TextView
private val title: TextView = findViewById(R.id.title)
fun setMessage(message: String) { this.message.text = message }
fun setTitle(title: String) { this.title.text = title }
fun setTitleAndMessage(title: String, message: String) {
setTitle(title)
setMessage(message)
}
}

@ -0,0 +1,296 @@
package com.otaliastudios.cameraview.demo
import android.graphics.Color
import android.graphics.ImageFormat
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraOptions
import com.otaliastudios.cameraview.CameraView
import com.otaliastudios.cameraview.gesture.GestureAction
import com.otaliastudios.cameraview.overlay.OverlayLayout
import kotlin.math.roundToInt
import kotlin.reflect.KClass
/**
* Controls that we want to display in a ControlView.
*/
abstract class Option<T: Any>(val name: String) {
abstract fun get(view: CameraView): T
abstract fun getAll(view: CameraView, options: CameraOptions): Collection<T>
abstract fun set(view: CameraView, value: T)
open fun toString(value: T): String {
return "$value".replace("_", "_").toLowerCase()
}
class Width : Option<Int>("Width") {
override fun get(view: CameraView) = view.layoutParams.width
override fun set(view: CameraView, value: Int) {
view.layoutParams.width = value
view.layoutParams = view.layoutParams
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> {
val root = view.parent as View
val boundary = root.width.takeIf { it > 0 } ?: 1000
val step = boundary / 10
val list = mutableListOf(WRAP_CONTENT, MATCH_PARENT)
for (i in step until boundary step step) { list.add(i) }
return list
}
override fun toString(value: Int): String {
if (value == MATCH_PARENT) return "match parent"
if (value == WRAP_CONTENT) return "wrap content"
return super.toString(value)
}
}
class Height : Option<Int>("Height") {
override fun get(view: CameraView) = view.layoutParams.height
override fun set(view: CameraView, value: Int) {
view.layoutParams.height = value
view.layoutParams = view.layoutParams
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> {
val root = view.parent as View
val boundary = root.height.takeIf { it > 0 } ?: 1000
val step = boundary / 10
val list = mutableListOf(WRAP_CONTENT, MATCH_PARENT)
for (i in step until boundary step step) { list.add(i) }
return list
}
override fun toString(value: Int): String {
if (value == MATCH_PARENT) return "match parent"
if (value == WRAP_CONTENT) return "wrap content"
return super.toString(value)
}
}
abstract class Control<T: com.otaliastudios.cameraview.controls.Control>(private val kclass: KClass<T>, name: String) : Option<T>(name) {
override fun set(view: CameraView, value: T) = view.set(value)
override fun get(view: CameraView) = view.get(kclass.java)
override fun getAll(view: CameraView, options: CameraOptions)
= options.getSupportedControls(kclass.java)
}
class Mode : Control<com.otaliastudios.cameraview.controls.Mode>(com.otaliastudios.cameraview.controls.Mode::class, "Mode")
class Engine : Control<com.otaliastudios.cameraview.controls.Engine>(com.otaliastudios.cameraview.controls.Engine::class, "Engine") {
override fun set(view: CameraView, value: com.otaliastudios.cameraview.controls.Engine) {
if (view.isOpened) {
view.addCameraListener(object : CameraListener() {
override fun onCameraClosed() {
super.onCameraClosed()
view.removeCameraListener(this)
view.engine = value
view.open()
}
})
view.close()
} else {
view.engine = value
}
}
}
class Preview : Control<com.otaliastudios.cameraview.controls.Preview>(com.otaliastudios.cameraview.controls.Preview::class, "Preview Surface") {
override fun set(view: CameraView, value: com.otaliastudios.cameraview.controls.Preview) {
if (view.isOpened) {
view.addCameraListener(object : CameraListener() {
override fun onCameraClosed() {
super.onCameraClosed()
view.removeCameraListener(this)
applyPreview(view, value)
view.open()
}
})
view.close()
} else {
applyPreview(view, value)
}
}
// This is really tricky since the preview can only be changed when not attached to window.
private fun applyPreview(view: CameraView, value: com.otaliastudios.cameraview.controls.Preview) {
val params = view.layoutParams
val parent = view.parent as ViewGroup
val index = (0 until parent.childCount).first { parent.getChildAt(it) === view }
parent.removeView(view)
view.preview = value
parent.addView(view, index, params)
}
}
class Flash : Control<com.otaliastudios.cameraview.controls.Flash>(com.otaliastudios.cameraview.controls.Flash::class, "Flash")
class WhiteBalance : Control<com.otaliastudios.cameraview.controls.WhiteBalance>(com.otaliastudios.cameraview.controls.WhiteBalance::class, "White Balance")
class Hdr : Control<com.otaliastudios.cameraview.controls.Hdr>(com.otaliastudios.cameraview.controls.Hdr::class, "HDR")
class PictureMetering : Option<Boolean>("Picture Metering") {
override fun get(view: CameraView) = view.pictureMetering
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun set(view: CameraView, value: Boolean) { view.pictureMetering = value }
}
class PictureSnapshotMetering : Option<Boolean>("Picture Snapshot Metering") {
override fun get(view: CameraView) = view.pictureSnapshotMetering
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun set(view: CameraView, value: Boolean) { view.pictureSnapshotMetering = value }
}
class VideoCodec : Control<com.otaliastudios.cameraview.controls.VideoCodec>(com.otaliastudios.cameraview.controls.VideoCodec::class, "Video Codec")
class AudioCodec : Control<com.otaliastudios.cameraview.controls.AudioCodec>(com.otaliastudios.cameraview.controls.AudioCodec::class, "Audio Codec")
class Audio : Control<com.otaliastudios.cameraview.controls.Audio>(com.otaliastudios.cameraview.controls.Audio::class, "Audio")
abstract class Gesture(val gesture: com.otaliastudios.cameraview.gesture.Gesture, name: String) : Option<GestureAction>(name) {
override fun set(view: CameraView, value: GestureAction) {
view.mapGesture(gesture, value)
}
override fun get(view: CameraView): GestureAction {
return view.getGestureAction(gesture)
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<GestureAction> {
return GestureAction.values().filter {
gesture.isAssignableTo(it) && options.supports(it)
}
}
}
class Pinch : Gesture(com.otaliastudios.cameraview.gesture.Gesture.PINCH, "Pinch")
class HorizontalScroll : Gesture(com.otaliastudios.cameraview.gesture.Gesture.SCROLL_HORIZONTAL, "Horizontal Scroll")
class VerticalScroll : Gesture(com.otaliastudios.cameraview.gesture.Gesture.SCROLL_VERTICAL, "Vertical Scroll")
class Tap : Gesture(com.otaliastudios.cameraview.gesture.Gesture.TAP, "Tap")
class LongTap : Gesture(com.otaliastudios.cameraview.gesture.Gesture.LONG_TAP, "Long Tap")
abstract class Overlay(private val overlay: View, private val target: com.otaliastudios.cameraview.overlay.Overlay.Target, name: String) : Option<Boolean>(name) {
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun get(view: CameraView): Boolean {
val params = overlay.layoutParams as OverlayLayout.LayoutParams
return when (target) {
com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW -> params.drawOnPreview
com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT -> params.drawOnPictureSnapshot
com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT -> params.drawOnVideoSnapshot
}
}
override fun set(view: CameraView, value: Boolean) {
val params = overlay.layoutParams as OverlayLayout.LayoutParams
when (target) {
com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW -> params.drawOnPreview = value
com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT -> params.drawOnPictureSnapshot = value
com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT -> params.drawOnVideoSnapshot = value
}
overlay.layoutParams = params
}
}
class OverlayInPreview(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW, "Overlay in Preview")
class OverlayInPictureSnapshot(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT, "Overlay in Picture Snapshot")
class OverlayInVideoSnapshot(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT, "Overlay in Video Snapshot")
class Grid : Control<com.otaliastudios.cameraview.controls.Grid>(com.otaliastudios.cameraview.controls.Grid::class, "Grid Lines")
class GridColor : Option<Pair<Int, String>>("Grid Color") {
private val all = listOf(
Color.argb(160, 255, 255, 255) to "default",
Color.WHITE to "white",
Color.BLACK to "black",
Color.YELLOW to "yellow"
)
override fun getAll(view: CameraView, options: CameraOptions) = all
override fun get(view: CameraView) = all.first { it.first == view.gridColor }
override fun set(view: CameraView, value: Pair<Int, String>) {
view.gridColor = value.first
}
}
class UseDeviceOrientation : Option<Boolean>("Use Device Orientation") {
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun get(view: CameraView) = view.useDeviceOrientation
override fun set(view: CameraView, value: Boolean) {
view.useDeviceOrientation = value
}
}
class PreviewFrameRate : Option<Int>("Preview FPS") {
override fun get(view: CameraView) = view.previewFrameRate.roundToInt()
override fun set(view: CameraView, value: Int) {
view.previewFrameRate = value.toFloat()
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> {
val min = options.previewFrameRateMinValue
val max = options.previewFrameRateMaxValue
val delta = max - min
return when {
min == 0F && max == 0F -> listOf()
delta < 0.005F -> listOf(min.roundToInt())
else -> {
val results = mutableListOf<Int>()
var value = min
for (i in 0 until 3) {
results.add(value.roundToInt())
value += delta / 3
}
results
}
}
}
}
class PictureFormat : Control<com.otaliastudios.cameraview.controls.PictureFormat>(com.otaliastudios.cameraview.controls.PictureFormat::class, "Picture Format")
class FrameProcessingFormat : Option<Int>("Frame Processing Format") {
override fun set(view: CameraView, value: Int) {
view.frameProcessingFormat = value
}
override fun get(view: CameraView) = view.frameProcessingFormat
override fun getAll(view: CameraView, options: CameraOptions)
= options.supportedFrameProcessingFormats
override fun toString(value: Int): String {
return when (value) {
ImageFormat.NV21 -> "NV21"
ImageFormat.NV16 -> "NV16"
ImageFormat.JPEG -> "JPEG"
ImageFormat.YUY2 -> "YUY2"
ImageFormat.YUV_420_888 -> "YUV_420_888"
ImageFormat.YUV_422_888 -> "YUV_422_888"
ImageFormat.YUV_444_888 -> "YUV_444_888"
ImageFormat.RAW10 -> "RAW10"
ImageFormat.RAW12 -> "RAW12"
ImageFormat.RAW_SENSOR -> "RAW_SENSOR"
else -> super.toString(value)
}
}
}
}

@ -0,0 +1,76 @@
package com.otaliastudios.cameraview.demo
import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.otaliastudios.cameraview.CameraOptions
import com.otaliastudios.cameraview.CameraView
class OptionView<Value: Any>(context: Context) : LinearLayout(context), AdapterView.OnItemSelectedListener {
interface Callback {
fun <T: Any> onValueChanged(option: Option<T>, value: T, name: String): Boolean
}
private lateinit var option: Option<Value>
private lateinit var callback: Callback
private lateinit var value: Value
private lateinit var values: List<Value>
private lateinit var valuesStrings: List<String>
init {
orientation = VERTICAL
View.inflate(context, R.layout.option_view, this)
}
val spinner = Spinner(context, Spinner.MODE_DROPDOWN).also {
val content = findViewById<ViewGroup>(R.id.content)
content.addView(it)
}
fun setHasDivider(hasDivider: Boolean) {
val divider = findViewById<View>(R.id.divider)
divider.visibility = if (hasDivider) View.VISIBLE else View.GONE
}
fun setOption(option: Option<Value>, callback: Callback) {
this.option = option
this.callback = callback
val title = findViewById<TextView>(R.id.title)
title.text = option.name
}
fun onCameraOpened(view: CameraView, options: CameraOptions) {
values = option.getAll(view, options).toList()
value = option.get(view)
valuesStrings = values.map { option.toString(it) }
if (values.isEmpty()) {
spinner.onItemSelectedListener = null
spinner.isEnabled = false
spinner.alpha = 0.8f
spinner.adapter = ArrayAdapter(context, R.layout.spinner_text, arrayOf("Not supported."))
spinner.setSelection(0, false)
} else {
spinner.isEnabled = true
spinner.alpha = 1f
spinner.adapter = ArrayAdapter(context, R.layout.spinner_text, valuesStrings)
spinner.setSelection(values.indexOf(value), false)
spinner.onItemSelectedListener = this
}
}
override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) {
if (values[i] != value) {
Log.e("ControlView", "curr: " + value + " new: " + values[i])
if (!callback.onValueChanged(option, values[i], valuesStrings[i])) {
spinner.setSelection(values.indexOf(value)) // Go back.
} else {
value = values[i]
}
}
}
override fun onNothingSelected(adapterView: AdapterView<*>?) {}
}

@ -0,0 +1,105 @@
package com.otaliastudios.cameraview.demo
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.otaliastudios.cameraview.CameraUtils
import com.otaliastudios.cameraview.PictureResult
import com.otaliastudios.cameraview.controls.PictureFormat
import com.otaliastudios.cameraview.size.AspectRatio
import java.io.File
class PicturePreviewActivity : AppCompatActivity() {
companion object {
var pictureResult: PictureResult? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_picture_preview)
val result = pictureResult ?: run {
finish()
return
}
val imageView = findViewById<ImageView>(R.id.image)
val captureResolution = findViewById<MessageView>(R.id.nativeCaptureResolution)
val captureLatency = findViewById<MessageView>(R.id.captureLatency)
val exifRotation = findViewById<MessageView>(R.id.exifRotation)
val delay = intent.getLongExtra("delay", 0)
val ratio = AspectRatio.of(result.size)
captureLatency.setTitleAndMessage("Approx. latency", "$delay milliseconds")
captureResolution.setTitleAndMessage("Resolution", "${result.size} ($ratio)")
exifRotation.setTitleAndMessage("EXIF rotation", result.rotation.toString())
try {
result.toBitmap(1000, 1000) { bitmap -> imageView.setImageBitmap(bitmap) }
} catch (e: UnsupportedOperationException) {
imageView.setImageDrawable(ColorDrawable(Color.GREEN))
Toast.makeText(this, "Can't preview this format: " + result.getFormat(), Toast.LENGTH_LONG).show()
}
if (result.isSnapshot) {
// Log the real size for debugging reason.
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeByteArray(result.data, 0, result.data.size, options)
if (result.rotation % 180 != 0) {
Log.e("PicturePreview", "The picture full size is ${result.size.height}x${result.size.width}")
} else {
Log.e("PicturePreview", "The picture full size is ${result.size.width}x${result.size.height}")
}
}
}
override fun onDestroy() {
super.onDestroy()
if (!isChangingConfigurations) {
pictureResult = null
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.share, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show()
val extension = when (pictureResult!!.format) {
PictureFormat.JPEG -> "jpg"
PictureFormat.DNG -> "dng"
else -> throw RuntimeException("Unknown format.")
}
val file = File(filesDir, "picture.$extension")
CameraUtils.writeToFile(pictureResult!!.data, file) { file ->
if (file != null) {
val context = this@PicturePreviewActivity
val intent = Intent(Intent.ACTION_SEND)
intent.type = "image/*"
val uri = FileProvider.getUriForFile(context,
context.packageName + ".provider", file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
} else {
Toast.makeText(this@PicturePreviewActivity,
"Error while writing file.",
Toast.LENGTH_SHORT).show()
}
}
return true
}
return super.onOptionsItemSelected(item)
}
}

@ -0,0 +1,107 @@
package com.otaliastudios.cameraview.demo
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.MediaController
import android.widget.Toast
import android.widget.VideoView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.otaliastudios.cameraview.VideoResult
import com.otaliastudios.cameraview.size.AspectRatio
class VideoPreviewActivity : AppCompatActivity() {
companion object {
var videoResult: VideoResult? = null
}
private val videoView: VideoView by lazy { findViewById<VideoView>(R.id.video) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_preview)
val result = videoResult ?: run {
finish()
return
}
videoView.setOnClickListener { playVideo() }
val actualResolution = findViewById<MessageView>(R.id.actualResolution)
val isSnapshot = findViewById<MessageView>(R.id.isSnapshot)
val rotation = findViewById<MessageView>(R.id.rotation)
val audio = findViewById<MessageView>(R.id.audio)
val audioBitRate = findViewById<MessageView>(R.id.audioBitRate)
val videoCodec = findViewById<MessageView>(R.id.videoCodec)
val audioCodec = findViewById<MessageView>(R.id.audioCodec)
val videoBitRate = findViewById<MessageView>(R.id.videoBitRate)
val videoFrameRate = findViewById<MessageView>(R.id.videoFrameRate)
val ratio = AspectRatio.of(result.size)
actualResolution.setTitleAndMessage("Size", "${result.size} ($ratio)")
isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot.toString())
rotation.setTitleAndMessage("Rotation", result.rotation.toString())
audio.setTitleAndMessage("Audio", result.audio.name)
audioBitRate.setTitleAndMessage("Audio bit rate", "${result.audioBitRate} bits per sec.")
videoCodec.setTitleAndMessage("VideoCodec", result.videoCodec.name)
audioCodec.setTitleAndMessage("AudioCodec", result.audioCodec.name)
videoBitRate.setTitleAndMessage("Video bit rate", "${result.videoBitRate} bits per sec.")
videoFrameRate.setTitleAndMessage("Video frame rate", "${result.videoFrameRate} fps")
val controller = MediaController(this)
controller.setAnchorView(videoView)
controller.setMediaPlayer(videoView)
videoView.setMediaController(controller)
videoView.setVideoURI(Uri.fromFile(result.file))
videoView.setOnPreparedListener { mp ->
val lp = videoView.layoutParams
val videoWidth = mp.videoWidth.toFloat()
val videoHeight = mp.videoHeight.toFloat()
val viewWidth = videoView.width.toFloat()
lp.height = (viewWidth * (videoHeight / videoWidth)).toInt()
videoView.layoutParams = lp
playVideo()
if (result.isSnapshot) {
// Log the real size for debugging reason.
Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight)
}
}
}
fun playVideo() {
if (!videoView.isPlaying) {
videoView.start()
}
}
override fun onDestroy() {
super.onDestroy()
if (!isChangingConfigurations) {
videoResult = null
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.share, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show()
val intent = Intent(Intent.ACTION_SEND)
intent.type = "video/*"
val uri = FileProvider.getUriForFile(this,
this.packageName + ".provider",
videoResult!!.file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
return true
}
return super.onOptionsItemSelected(item)
}
}

1
docs/.gitignore vendored

@ -3,3 +3,4 @@ _pages
*.sw?
.sass-cache
.jekyll-metadata
Gemfile.lock

@ -1,248 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.10)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.4)
dnsruby (1.61.2)
addressable (~> 2.5)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.11.0)
ffi (>= 1.3.0)
eventmachine (1.2.7)
execjs (2.7.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
ffi (1.9.25)
forwardable-extended (2.6.0)
gemoji (3.0.0)
github-pages (193)
activesupport (= 4.2.10)
github-pages-health-check (= 1.8.1)
jekyll (= 3.7.4)
jekyll-avatar (= 0.6.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.5)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.11.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.9.4)
jekyll-mentions (= 1.4.1)
jekyll-optional-front-matter (= 0.3.0)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.2.0)
jekyll-redirect-from (= 0.14.0)
jekyll-relative-links (= 0.5.3)
jekyll-remote-theme (= 0.3.1)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.5.0)
jekyll-sitemap (= 1.2.0)
jekyll-swiss (= 0.4.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.1)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.3)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.1)
jemoji (= 0.10.1)
kramdown (= 1.17.0)
liquid (= 4.0.0)
listen (= 3.1.5)
mercenary (~> 0.3)
minima (= 2.5.0)
nokogiri (>= 1.8.2, < 2.0)
rouge (= 2.2.1)
terminal-table (~> 1.4)
github-pages-health-check (1.8.1)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (~> 2.0)
typhoeus (~> 1.3)
html-pipeline (2.9.1)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.7.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (~> 1.14)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.6.0)
jekyll (~> 3.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.2.0)
commonmarker (~> 0.14)
jekyll (>= 3.0, < 4.0)
jekyll-commonmark-ghpages (0.1.5)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1)
rouge (~> 2)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.11.0)
jekyll (~> 3.3)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.9.4)
jekyll (~> 3.1)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.4.1)
html-pipeline (~> 2.3)
jekyll (~> 3.0)
jekyll-optional-front-matter (0.3.0)
jekyll (~> 3.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.2.0)
jekyll (~> 3.0)
jekyll-redirect-from (0.14.0)
jekyll (~> 3.3)
jekyll-relative-links (0.5.3)
jekyll (~> 3.3)
jekyll-remote-theme (0.3.1)
jekyll (~> 3.5)
rubyzip (>= 1.2.1, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.5.0)
jekyll (~> 3.3)
jekyll-sitemap (1.2.0)
jekyll (~> 3.3)
jekyll-swiss (0.4.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.3)
jekyll (~> 3.5)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.1)
jekyll (~> 3.3)
jekyll-watch (2.1.2)
listen (~> 3.0)
jemoji (0.10.1)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (~> 3.0)
kramdown (1.17.0)
liquid (4.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minima (2.5.0)
jekyll (~> 3.5)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.11.3)
multipart-post (2.0.0)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
octokit (4.13.0)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (2.0.5)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rouge (2.2.1)
ruby-enum (0.7.2)
i18n
ruby_dep (1.5.0)
rubyzip (2.0.0)
safe_yaml (1.0.4)
sass (3.7.2)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.4.0)
PLATFORMS
ruby
DEPENDENCIES
github-pages
BUNDLED WITH
1.17.2
Loading…
Cancel
Save