diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 462b0847..9a3bd6d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/build.gradle.kts b/build.gradle.kts index 4e783fc8..5b73b143 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } } diff --git a/cameraview/build.gradle.kts b/cameraview/build.gradle.kts index 3b84109b..91fb6cc3 100644 --- a/cameraview/build.gradle.kts +++ b/cameraview/build.gradle.kts @@ -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)) diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 0f4d82c5..21967289 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -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); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index b5161c78..922da7b0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -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 diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 37f75628..2fa7f385 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -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 diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java index 5742336c..fbe43559 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java @@ -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,15 +136,25 @@ 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 + state.sizeChanged = false; + return; + } //noinspection ConstantConditions - if (state.isFramebufferCreated || isLast) return; - state.isFramebufferCreated = true; - state.outputTexture = new GlTexture(GLES20.GL_TEXTURE0, - GLES20.GL_TEXTURE_2D, - state.size.getWidth(), - state.size.getHeight()); - state.outputFramebuffer = new GlFramebuffer(); - state.outputFramebuffer.attach(state.outputTexture); + 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, + state.size.getWidth(), + state.size.getHeight()); + state.outputFramebuffer = new GlFramebuffer(); + state.outputFramebuffer.attach(state.outputTexture); + } } private void maybeDestroyFramebuffer(@NonNull Filter 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()); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java index b3d592a7..11db5681 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java @@ -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(); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java b/cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java index 7cf211d1..83a1af3d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java @@ -33,8 +33,8 @@ public class AspectRatio implements Comparable { @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 { 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 { } 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 { 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 { @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 { 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; diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index f65862d1..ece186ac 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -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") } diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java deleted file mode 100644 index e70994a0..00000000 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ /dev/null @@ -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> 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 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 boolean onValueChanged(@NonNull Option 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 -} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/MessageView.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/MessageView.java deleted file mode 100644 index 3e318a40..00000000 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/MessageView.java +++ /dev/null @@ -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); - } -} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java deleted file mode 100644 index 9f01bc1d..00000000 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java +++ /dev/null @@ -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 { - - 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 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 { - public Width() { - super("Width"); - } - - @NonNull - @Override - public Collection getAll(@NonNull CameraView view, @NonNull CameraOptions options) { - View root = (View) view.getParent(); - List 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 { - public Height() { - super("Height"); - } - - @NonNull - @Override - public Collection getAll(@NonNull CameraView view, @NonNull CameraOptions options) { - View root = (View) view.getParent(); - ArrayList 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 extends Option { - private final Class controlClass; - - ControlOption(@NonNull Class 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 getAll(@NonNull CameraView view, @NonNull CameraOptions options) { - return options.getSupportedControls(controlClass); - } - } - - public static class Mode extends ControlOption { - public Mode() { - super(com.otaliastudios.cameraview.controls.Mode.class, "Mode"); - } - } - - public static class Engine extends ControlOption { - 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 { - 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 { - public Flash() { - super(com.otaliastudios.cameraview.controls.Flash.class, "Flash"); - } - } - - public static class WhiteBalance extends ControlOption { - public WhiteBalance() { - super(com.otaliastudios.cameraview.controls.WhiteBalance.class, "White Balance"); - } - } - - public static class Hdr extends ControlOption { - public Hdr() { - super(com.otaliastudios.cameraview.controls.Hdr.class, "HDR"); - } - } - - public static class PictureMetering extends Option { - - public PictureMetering() { - super("Picture Metering"); - } - - @NonNull - @Override - public Boolean get(@NonNull CameraView view) { - return view.getPictureMetering(); - } - - @NonNull - @Override - public Collection 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 { - - public PictureSnapshotMetering() { - super("Picture Snapshot Metering"); - } - - @NonNull - @Override - public Boolean get(@NonNull CameraView view) { - return view.getPictureSnapshotMetering(); - } - - @NonNull - @Override - public Collection 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 { - public VideoCodec() { - super(com.otaliastudios.cameraview.controls.VideoCodec.class, "Video Codec"); - } - } - - public static class AudioCodec extends ControlOption { - public AudioCodec() { - super(com.otaliastudios.cameraview.controls.AudioCodec.class, "Audio Codec"); - } - } - - public static class Audio extends ControlOption { - public Audio() { - super(com.otaliastudios.cameraview.controls.Audio.class, "Audio"); - } - } - - private static abstract class GestureOption extends Option { - 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 getAll(@NonNull CameraView view, @NonNull CameraOptions options) { - List 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 { - 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 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 { - public Grid() { - super(com.otaliastudios.cameraview.controls.Grid.class, "Grid Lines"); - } - } - - public static class GridColor extends Option> { - - public GridColor() { - super("Grid Color"); - } - - private static final List> 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> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { - return ALL; - } - - @NonNull - @Override - public Pair get(@NonNull CameraView view) { - for (Pair 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 value) { - //noinspection ConstantConditions - view.setGridColor(value.first); - } - - @NonNull - @Override - public String toString(@NonNull Pair value) { - //noinspection ConstantConditions - return value.second; - } - } - - public static class UseDeviceOrientation extends Option { - public UseDeviceOrientation() { - super("Use Device Orientation"); - } - - @NonNull - @Override - public Collection 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 { - public PreviewFrameRate() { - super("Preview FPS"); - } - - @NonNull - @Override - public Collection getAll(@NonNull CameraView view, @NonNull CameraOptions options) { - float min = options.getPreviewFrameRateMinValue(); - float max = options.getPreviewFrameRateMaxValue(); - float delta = max - min; - List 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 { - public PictureFormat() { - super(com.otaliastudios.cameraview.controls.PictureFormat.class, "Picture Format"); - } - } - - public static class FrameProcessingFormat extends Option { - - 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 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); - } - } -} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/OptionView.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/OptionView.java deleted file mode 100644 index ea4bbd04..00000000 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/OptionView.java +++ /dev/null @@ -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 extends LinearLayout implements Spinner.OnItemSelectedListener { - - interface Callback { - boolean onValueChanged(@NonNull Option option, @NonNull T value, @NonNull String name); - } - - private Value value; - private ArrayList values; - private ArrayList 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 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) {} -} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java deleted file mode 100644 index 18321413..00000000 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java +++ /dev/null @@ -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); - } -} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java deleted file mode 100644 index aac9984c..00000000 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java +++ /dev/null @@ -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); - } -} diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt new file mode 100644 index 00000000..2e245562 --- /dev/null +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt @@ -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() + 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(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) + val group = controlPanel.getChildAt(0) as ViewGroup + val watermark = findViewById(R.id.watermark) + val options: List> = 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(this) + view.setOption(options[i] as Option, 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?) { + super.onExposureCorrectionChanged(newValue, bounds, fingers) + message("Exposure correction:$newValue", false) + } + + override fun onZoomChanged(newValue: Float, bounds: FloatArray, fingers: Array?) { + 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 onValueChanged(option: Option, 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, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + val valid = grantResults.all { it == PERMISSION_GRANTED } + if (valid && !camera.isOpened) { + camera.open() + } + } +} diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/MessageView.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/MessageView.kt new file mode 100644 index 00000000..9736d7c2 --- /dev/null +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/MessageView.kt @@ -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(R.id.content) + View.inflate(context, R.layout.spinner_text, content) + } + + private val message: TextView = findViewById(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) + } +} diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/Option.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/Option.kt new file mode 100644 index 00000000..ddd174c0 --- /dev/null +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/Option.kt @@ -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(val name: String) { + + abstract fun get(view: CameraView): T + + abstract fun getAll(view: CameraView, options: CameraOptions): Collection + + abstract fun set(view: CameraView, value: T) + + open fun toString(value: T): String { + return "$value".replace("_", "_").toLowerCase() + } + + class Width : Option("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 { + 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("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 { + 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(private val kclass: KClass, name: String) : Option(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::class, "Mode") + + class Engine : Control(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::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::class, "Flash") + + class WhiteBalance : Control(com.otaliastudios.cameraview.controls.WhiteBalance::class, "White Balance") + + class Hdr : Control(com.otaliastudios.cameraview.controls.Hdr::class, "HDR") + + class PictureMetering : Option("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("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::class, "Video Codec") + + class AudioCodec : Control(com.otaliastudios.cameraview.controls.AudioCodec::class, "Audio Codec") + + class Audio : Control(com.otaliastudios.cameraview.controls.Audio::class, "Audio") + + abstract class Gesture(val gesture: com.otaliastudios.cameraview.gesture.Gesture, name: String) : Option(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 { + 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(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::class, "Grid Lines") + + class GridColor : Option>("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) { + view.gridColor = value.first + } + } + + class UseDeviceOrientation : Option("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("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 { + 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() + 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::class, "Picture Format") + + class FrameProcessingFormat : Option("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) + } + } + } +} diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/OptionView.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/OptionView.kt new file mode 100644 index 00000000..4bca9d06 --- /dev/null +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/OptionView.kt @@ -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(context: Context) : LinearLayout(context), AdapterView.OnItemSelectedListener { + + interface Callback { + fun onValueChanged(option: Option, value: T, name: String): Boolean + } + + private lateinit var option: Option + private lateinit var callback: Callback + private lateinit var value: Value + private lateinit var values: List + private lateinit var valuesStrings: List + + init { + orientation = VERTICAL + View.inflate(context, R.layout.option_view, this) + } + + val spinner = Spinner(context, Spinner.MODE_DROPDOWN).also { + val content = findViewById(R.id.content) + content.addView(it) + } + + fun setHasDivider(hasDivider: Boolean) { + val divider = findViewById(R.id.divider) + divider.visibility = if (hasDivider) View.VISIBLE else View.GONE + } + + fun setOption(option: Option, callback: Callback) { + this.option = option + this.callback = callback + val title = findViewById(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<*>?) {} +} diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/PicturePreviewActivity.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/PicturePreviewActivity.kt new file mode 100644 index 00000000..12cb37ef --- /dev/null +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/PicturePreviewActivity.kt @@ -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(R.id.image) + val captureResolution = findViewById(R.id.nativeCaptureResolution) + val captureLatency = findViewById(R.id.captureLatency) + val exifRotation = findViewById(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) + } +} \ No newline at end of file diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt new file mode 100644 index 00000000..c105d4dd --- /dev/null +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt @@ -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(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(R.id.actualResolution) + val isSnapshot = findViewById(R.id.isSnapshot) + val rotation = findViewById(R.id.rotation) + val audio = findViewById(R.id.audio) + val audioBitRate = findViewById(R.id.audioBitRate) + val videoCodec = findViewById(R.id.videoCodec) + val audioCodec = findViewById(R.id.audioCodec) + val videoBitRate = findViewById(R.id.videoBitRate) + val videoFrameRate = findViewById(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) + } + +} \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore index d8a620e8..928a70a0 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,3 +3,4 @@ _pages *.sw? .sass-cache .jekyll-metadata +Gemfile.lock diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index e26c1aa1..00000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -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