Some bug fixes (#953)
	
		
	
				
					
				
			* Kotlin 1.4.0, fixes #940 * Fix multifilter bug, fixes #875 * Fix arithmetic exception, fixes #895 * Update dependencies * Demo app in Kotlin * Avoid stackoverflows on zoom/ev, fixes #856 * Remove Gemfile.lock * Try to fix buildpull/954/head
							parent
							
								
									daf7a0bf44
								
							
						
					
					
						commit
						8207e67679
					
				| @ -1,21 +1,23 @@ | ||||
| plugins { | ||||
|     id("com.android.application") | ||||
|     id("kotlin-android") | ||||
| } | ||||
| 
 | ||||
| android { | ||||
|     setCompileSdkVersion(rootProject.property("compileSdkVersion") as Int) | ||||
|     setCompileSdkVersion(property("compileSdkVersion") as Int) | ||||
|     defaultConfig { | ||||
|         applicationId = "com.otaliastudios.cameraview.demo" | ||||
|         setMinSdkVersion(rootProject.property("minSdkVersion") as Int) | ||||
|         setTargetSdkVersion(rootProject.property("targetSdkVersion") as Int) | ||||
|         setMinSdkVersion(property("minSdkVersion") as Int) | ||||
|         setTargetSdkVersion(property("targetSdkVersion") as Int) | ||||
|         versionCode = 1 | ||||
|         versionName = "1.0" | ||||
|         vectorDrawables.useSupportLibrary = true | ||||
|     } | ||||
|     sourceSets["main"].java.srcDir("src/main/kotlin") | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation(project(":cameraview")) | ||||
|     implementation("androidx.appcompat:appcompat:1.1.0") | ||||
|     implementation("com.google.android.material:material:1.1.0") | ||||
|     implementation("androidx.appcompat:appcompat:1.2.0") | ||||
|     implementation("com.google.android.material:material:1.2.0") | ||||
| } | ||||
|  | ||||
| @ -1,417 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.demo; | ||||
| 
 | ||||
| import android.animation.ValueAnimator; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.graphics.Color; | ||||
| import android.graphics.ImageFormat; | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.YuvImage; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.NonNull; | ||||
| import com.google.android.material.bottomsheet.BottomSheetBehavior; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| 
 | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.ViewTreeObserver; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraException; | ||||
| import com.otaliastudios.cameraview.CameraListener; | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.CameraOptions; | ||||
| import com.otaliastudios.cameraview.CameraView; | ||||
| import com.otaliastudios.cameraview.PictureResult; | ||||
| import com.otaliastudios.cameraview.controls.Mode; | ||||
| import com.otaliastudios.cameraview.VideoResult; | ||||
| import com.otaliastudios.cameraview.controls.Preview; | ||||
| import com.otaliastudios.cameraview.filter.Filters; | ||||
| import com.otaliastudios.cameraview.filter.MultiFilter; | ||||
| import com.otaliastudios.cameraview.filters.BrightnessFilter; | ||||
| import com.otaliastudios.cameraview.filters.DuotoneFilter; | ||||
| import com.otaliastudios.cameraview.frame.Frame; | ||||
| import com.otaliastudios.cameraview.frame.FrameProcessor; | ||||
| 
 | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.File; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| 
 | ||||
| public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback { | ||||
| 
 | ||||
|     private final static CameraLogger LOG = CameraLogger.create("DemoApp"); | ||||
|     private final static boolean USE_FRAME_PROCESSOR = true; | ||||
|     private final static boolean DECODE_BITMAP = false; | ||||
| 
 | ||||
|     private CameraView camera; | ||||
|     private ViewGroup controlPanel; | ||||
|     private long mCaptureTime; | ||||
| 
 | ||||
|     private int mCurrentFilter = 0; | ||||
|     private final Filters[] mAllFilters = Filters.values(); | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_camera); | ||||
|         CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE); | ||||
| 
 | ||||
|         camera = findViewById(R.id.camera); | ||||
|         camera.setLifecycleOwner(this); | ||||
|         camera.addCameraListener(new Listener()); | ||||
| 
 | ||||
|         if (USE_FRAME_PROCESSOR) { | ||||
|             camera.addFrameProcessor(new FrameProcessor() { | ||||
|                 private long lastTime = System.currentTimeMillis(); | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void process(@NonNull Frame frame) { | ||||
|                     long newTime = frame.getTime(); | ||||
|                     long delay = newTime - lastTime; | ||||
|                     lastTime = newTime; | ||||
|                     LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay); | ||||
|                     if (DECODE_BITMAP) { | ||||
|                         if (frame.getFormat() == ImageFormat.NV21 | ||||
|                                 && frame.getDataClass() == byte[].class) { | ||||
|                             byte[] data = frame.getData(); | ||||
|                             YuvImage yuvImage = new YuvImage(data, | ||||
|                                     frame.getFormat(), | ||||
|                                     frame.getSize().getWidth(), | ||||
|                                     frame.getSize().getHeight(), | ||||
|                                     null); | ||||
|                             ByteArrayOutputStream jpegStream = new ByteArrayOutputStream(); | ||||
|                             yuvImage.compressToJpeg(new Rect(0, 0, | ||||
|                                     frame.getSize().getWidth(), | ||||
|                                     frame.getSize().getHeight()), 100, jpegStream); | ||||
|                             byte[] jpegByteArray = jpegStream.toByteArray(); | ||||
|                             Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, | ||||
|                                     0, jpegByteArray.length); | ||||
|                             //noinspection ResultOfMethodCallIgnored
 | ||||
|                             bitmap.toString(); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         findViewById(R.id.edit).setOnClickListener(this); | ||||
|         findViewById(R.id.capturePicture).setOnClickListener(this); | ||||
|         findViewById(R.id.capturePictureSnapshot).setOnClickListener(this); | ||||
|         findViewById(R.id.captureVideo).setOnClickListener(this); | ||||
|         findViewById(R.id.captureVideoSnapshot).setOnClickListener(this); | ||||
|         findViewById(R.id.toggleCamera).setOnClickListener(this); | ||||
|         findViewById(R.id.changeFilter).setOnClickListener(this); | ||||
| 
 | ||||
|         controlPanel = findViewById(R.id.controls); | ||||
|         ViewGroup group = (ViewGroup) controlPanel.getChildAt(0); | ||||
|         final View watermark = findViewById(R.id.watermark); | ||||
| 
 | ||||
|         List<Option<?>> options = Arrays.asList( | ||||
|                 // Layout
 | ||||
|                 new Option.Width(), new Option.Height(), | ||||
|                 // Engine and preview
 | ||||
|                 new Option.Mode(), new Option.Engine(), new Option.Preview(), | ||||
|                 // Some controls
 | ||||
|                 new Option.Flash(), new Option.WhiteBalance(), new Option.Hdr(), | ||||
|                 new Option.PictureMetering(), new Option.PictureSnapshotMetering(), | ||||
|                 new Option.PictureFormat(), | ||||
|                 // Video recording
 | ||||
|                 new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(), new Option.AudioCodec(), | ||||
|                 // Gestures
 | ||||
|                 new Option.Pinch(), new Option.HorizontalScroll(), new Option.VerticalScroll(), | ||||
|                 new Option.Tap(), new Option.LongTap(), | ||||
|                 // Watermarks
 | ||||
|                 new Option.OverlayInPreview(watermark), | ||||
|                 new Option.OverlayInPictureSnapshot(watermark), | ||||
|                 new Option.OverlayInVideoSnapshot(watermark), | ||||
|                 // Frame Processing
 | ||||
|                 new Option.FrameProcessingFormat(), | ||||
|                 // Other
 | ||||
|                 new Option.Grid(), new Option.GridColor(), new Option.UseDeviceOrientation() | ||||
|         ); | ||||
|         List<Boolean> dividers = Arrays.asList( | ||||
|                 // Layout
 | ||||
|                 false, true, | ||||
|                 // Engine and preview
 | ||||
|                 false, false, true, | ||||
|                 // Some controls
 | ||||
|                 false, false, false, false, false, true, | ||||
|                 // Video recording
 | ||||
|                 false, false, false, true, | ||||
|                 // Gestures
 | ||||
|                 false, false, false, false, true, | ||||
|                 // Watermarks
 | ||||
|                 false, false, true, | ||||
|                 // Frame Processing
 | ||||
|                 true, | ||||
|                 // Other
 | ||||
|                 false, false, true | ||||
|         ); | ||||
|         for (int i = 0; i < options.size(); i++) { | ||||
|             OptionView view = new OptionView(this); | ||||
|             //noinspection unchecked
 | ||||
|             view.setOption(options.get(i), this); | ||||
|             view.setHasDivider(dividers.get(i)); | ||||
|             group.addView(view, | ||||
|                     ViewGroup.LayoutParams.MATCH_PARENT, | ||||
|                     ViewGroup.LayoutParams.WRAP_CONTENT); | ||||
|         } | ||||
| 
 | ||||
|         controlPanel.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { | ||||
|             @Override | ||||
|             public void onGlobalLayout() { | ||||
|                 BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel); | ||||
|                 b.setState(BottomSheetBehavior.STATE_HIDDEN); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Animate the watermark just to show we record the animation in video snapshots
 | ||||
|         ValueAnimator animator = ValueAnimator.ofFloat(1F, 0.8F); | ||||
|         animator.setDuration(300); | ||||
|         animator.setRepeatCount(ValueAnimator.INFINITE); | ||||
|         animator.setRepeatMode(ValueAnimator.REVERSE); | ||||
|         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { | ||||
|             @Override | ||||
|             public void onAnimationUpdate(ValueAnimator animation) { | ||||
|                 float scale = (float) animation.getAnimatedValue(); | ||||
|                 watermark.setScaleX(scale); | ||||
|                 watermark.setScaleY(scale); | ||||
|                 watermark.setRotation(watermark.getRotation() + 2); | ||||
|             } | ||||
|         }); | ||||
|         animator.start(); | ||||
|     } | ||||
| 
 | ||||
|     private void message(@NonNull String content, boolean important) { | ||||
|         if (important) { | ||||
|             LOG.w(content); | ||||
|             Toast.makeText(this, content, Toast.LENGTH_LONG).show(); | ||||
|         } else { | ||||
|             LOG.i(content); | ||||
|             Toast.makeText(this, content, Toast.LENGTH_SHORT).show(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private class Listener extends CameraListener { | ||||
| 
 | ||||
|         @Override | ||||
|         public void onCameraOpened(@NonNull CameraOptions options) { | ||||
|             ViewGroup group = (ViewGroup) controlPanel.getChildAt(0); | ||||
|             for (int i = 0; i < group.getChildCount(); i++) { | ||||
|                 OptionView view = (OptionView) group.getChildAt(i); | ||||
|                 view.onCameraOpened(camera, options); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onCameraError(@NonNull CameraException exception) { | ||||
|             super.onCameraError(exception); | ||||
|             message("Got CameraException #" + exception.getReason(), true); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onPictureTaken(@NonNull PictureResult result) { | ||||
|             super.onPictureTaken(result); | ||||
|             if (camera.isTakingVideo()) { | ||||
|                 message("Captured while taking video. Size=" + result.getSize(), false); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // This can happen if picture was taken with a gesture.
 | ||||
|             long callbackTime = System.currentTimeMillis(); | ||||
|             if (mCaptureTime == 0) mCaptureTime = callbackTime - 300; | ||||
|             LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - mCaptureTime); | ||||
|             PicturePreviewActivity.setPictureResult(result); | ||||
|             Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); | ||||
|             intent.putExtra("delay", callbackTime - mCaptureTime); | ||||
|             startActivity(intent); | ||||
|             mCaptureTime = 0; | ||||
|             LOG.w("onPictureTaken called! Launched activity."); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onVideoTaken(@NonNull VideoResult result) { | ||||
|             super.onVideoTaken(result); | ||||
|             LOG.w("onVideoTaken called! Launching activity."); | ||||
|             VideoPreviewActivity.setVideoResult(result); | ||||
|             Intent intent = new Intent(CameraActivity.this, VideoPreviewActivity.class); | ||||
|             startActivity(intent); | ||||
|             LOG.w("onVideoTaken called! Launched activity."); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onVideoRecordingStart() { | ||||
|             super.onVideoRecordingStart(); | ||||
|             LOG.w("onVideoRecordingStart!"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onVideoRecordingEnd() { | ||||
|             super.onVideoRecordingEnd(); | ||||
|             message("Video taken. Processing...", false); | ||||
|             LOG.w("onVideoRecordingEnd!"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { | ||||
|             super.onExposureCorrectionChanged(newValue, bounds, fingers); | ||||
|             message("Exposure correction:" + newValue, false); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onZoomChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { | ||||
|             super.onZoomChanged(newValue, bounds, fingers); | ||||
|             message("Zoom:" + newValue, false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onClick(View view) { | ||||
|         switch (view.getId()) { | ||||
|             case R.id.edit: edit(); break; | ||||
|             case R.id.capturePicture: capturePicture(); break; | ||||
|             case R.id.capturePictureSnapshot: capturePictureSnapshot(); break; | ||||
|             case R.id.captureVideo: captureVideo(); break; | ||||
|             case R.id.captureVideoSnapshot: captureVideoSnapshot(); break; | ||||
|             case R.id.toggleCamera: toggleCamera(); break; | ||||
|             case R.id.changeFilter: changeCurrentFilter(); break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel); | ||||
|         if (b.getState() != BottomSheetBehavior.STATE_HIDDEN) { | ||||
|             b.setState(BottomSheetBehavior.STATE_HIDDEN); | ||||
|             return; | ||||
|         } | ||||
|         super.onBackPressed(); | ||||
|     } | ||||
| 
 | ||||
|     private void edit() { | ||||
|         BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel); | ||||
|         b.setState(BottomSheetBehavior.STATE_COLLAPSED); | ||||
|     } | ||||
| 
 | ||||
|     private void capturePicture() { | ||||
|         if (camera.getMode() == Mode.VIDEO) { | ||||
|             message("Can't take HQ pictures while in VIDEO mode.", false); | ||||
|             return; | ||||
|         } | ||||
|         if (camera.isTakingPicture()) return; | ||||
|         mCaptureTime = System.currentTimeMillis(); | ||||
|         message("Capturing picture...", false); | ||||
|         camera.takePicture(); | ||||
|     } | ||||
| 
 | ||||
|     private void capturePictureSnapshot() { | ||||
|         if (camera.isTakingPicture()) return; | ||||
|         if (camera.getPreview() != Preview.GL_SURFACE) { | ||||
|             message("Picture snapshots are only allowed with the GL_SURFACE preview.", true); | ||||
|             return; | ||||
|         } | ||||
|         mCaptureTime = System.currentTimeMillis(); | ||||
|         message("Capturing picture snapshot...", false); | ||||
|         camera.takePictureSnapshot(); | ||||
|     } | ||||
| 
 | ||||
|     private void captureVideo() { | ||||
|         if (camera.getMode() == Mode.PICTURE) { | ||||
|             message("Can't record HQ videos while in PICTURE mode.", false); | ||||
|             return; | ||||
|         } | ||||
|         if (camera.isTakingPicture() || camera.isTakingVideo()) return; | ||||
|         message("Recording for 5 seconds...", true); | ||||
|         camera.takeVideo(new File(getFilesDir(), "video.mp4"), 5000); | ||||
|     } | ||||
| 
 | ||||
|     private void captureVideoSnapshot() { | ||||
|         if (camera.isTakingVideo()) { | ||||
|             message("Already taking video.", false); | ||||
|             return; | ||||
|         } | ||||
|         if (camera.getPreview() != Preview.GL_SURFACE) { | ||||
|             message("Video snapshots are only allowed with the GL_SURFACE preview.", true); | ||||
|             return; | ||||
|         } | ||||
|         message("Recording snapshot for 5 seconds...", true); | ||||
|         camera.takeVideoSnapshot(new File(getFilesDir(), "video.mp4"), 5000); | ||||
|     } | ||||
| 
 | ||||
|     private void toggleCamera() { | ||||
|         if (camera.isTakingPicture() || camera.isTakingVideo()) return; | ||||
|         switch (camera.toggleFacing()) { | ||||
|             case BACK: | ||||
|                 message("Switched to back camera!", false); | ||||
|                 break; | ||||
| 
 | ||||
|             case FRONT: | ||||
|                 message("Switched to front camera!", false); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void changeCurrentFilter() { | ||||
|         if (camera.getPreview() != Preview.GL_SURFACE) { | ||||
|             message("Filters are supported only when preview is Preview.GL_SURFACE.", true); | ||||
|             return; | ||||
|         } | ||||
|         if (mCurrentFilter < mAllFilters.length - 1) { | ||||
|             mCurrentFilter++; | ||||
|         } else { | ||||
|             mCurrentFilter = 0; | ||||
|         } | ||||
|         Filters filter = mAllFilters[mCurrentFilter]; | ||||
|         message(filter.toString(), false); | ||||
| 
 | ||||
|         // Normal behavior:
 | ||||
|         camera.setFilter(filter.newInstance()); | ||||
| 
 | ||||
|         // To test MultiFilter:
 | ||||
|         // DuotoneFilter duotone = new DuotoneFilter();
 | ||||
|         // duotone.setFirstColor(Color.RED);
 | ||||
|         // duotone.setSecondColor(Color.GREEN);
 | ||||
|         // camera.setFilter(new MultiFilter(duotone, filter.newInstance()));
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <T> boolean onValueChanged(@NonNull Option<T> option, @NonNull T value, @NonNull String name) { | ||||
|         if ((option instanceof Option.Width || option instanceof Option.Height)) { | ||||
|             Preview preview = camera.getPreview(); | ||||
|             boolean wrapContent = (Integer) value == ViewGroup.LayoutParams.WRAP_CONTENT; | ||||
|             if (preview == Preview.SURFACE && !wrapContent) { | ||||
|                 message("The SurfaceView preview does not support width or height changes. " + | ||||
|                         "The view will act as WRAP_CONTENT by default.", true); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         option.set(camera, value); | ||||
|         BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel); | ||||
|         b.setState(BottomSheetBehavior.STATE_HIDDEN); | ||||
|         message("Changed " + option.getName() + " to " + name, false); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     //region Permissions
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { | ||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||
|         boolean valid = true; | ||||
|         for (int grantResult : grantResults) { | ||||
|             valid = valid && grantResult == PackageManager.PERMISSION_GRANTED; | ||||
|         } | ||||
|         if (valid && !camera.isOpened()) { | ||||
|             camera.open(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     //endregion
 | ||||
| } | ||||
| @ -1,46 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.demo; | ||||
| 
 | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import androidx.annotation.Nullable; | ||||
| import android.util.AttributeSet; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| public class MessageView extends LinearLayout { | ||||
| 
 | ||||
|     private TextView message; | ||||
|     private TextView title; | ||||
| 
 | ||||
|     public MessageView(Context context) { | ||||
|         this(context, null); | ||||
|     } | ||||
| 
 | ||||
|     public MessageView(Context context, @Nullable AttributeSet attrs) { | ||||
|         this(context, attrs, 0); | ||||
|     } | ||||
| 
 | ||||
|     public MessageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { | ||||
|         super(context, attrs, defStyleAttr); | ||||
|         setOrientation(VERTICAL); | ||||
|         inflate(context, R.layout.option_view, this); | ||||
|         ViewGroup content = findViewById(R.id.content); | ||||
|         inflate(context, R.layout.spinner_text, content); | ||||
|         title = findViewById(R.id.title); | ||||
|         message = (TextView) content.getChildAt(0); | ||||
|     } | ||||
| 
 | ||||
|     public void setTitleAndMessage(String title, String message) { | ||||
|         setTitle(title); | ||||
|         setMessage(message); | ||||
|     } | ||||
| 
 | ||||
|     public void setTitle(String title) { | ||||
|         this.title.setText(title); | ||||
|     } | ||||
| 
 | ||||
|     public void setMessage(String message) { | ||||
|         this.message.setText(message); | ||||
|     } | ||||
| } | ||||
| @ -1,605 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.demo; | ||||
| 
 | ||||
| import android.graphics.Color; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.core.util.Pair; | ||||
| 
 | ||||
| import android.graphics.ImageFormat; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraListener; | ||||
| import com.otaliastudios.cameraview.CameraOptions; | ||||
| import com.otaliastudios.cameraview.CameraView; | ||||
| import com.otaliastudios.cameraview.controls.Control; | ||||
| import com.otaliastudios.cameraview.gesture.Gesture; | ||||
| import com.otaliastudios.cameraview.gesture.GestureAction; | ||||
| import com.otaliastudios.cameraview.overlay.Overlay; | ||||
| import com.otaliastudios.cameraview.overlay.OverlayLayout; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Controls that we want to display in a ControlView. | ||||
|  */ | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public abstract class Option<T> { | ||||
| 
 | ||||
|     private String name; | ||||
| 
 | ||||
|     private Option(@NonNull String name) { | ||||
|         this.name = name; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     @NonNull | ||||
|     public final String getName() { | ||||
|         return name; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract T get(@NonNull CameraView view); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract Collection<T> getAll(@NonNull CameraView view, @NonNull CameraOptions options); | ||||
| 
 | ||||
|     public abstract void set(@NonNull CameraView view, @NonNull T value); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public String toString(@NonNull T value) { | ||||
|         return String.valueOf(value).replace("_", " ").toLowerCase(); | ||||
|     } | ||||
| 
 | ||||
|     public static class Width extends Option<Integer> { | ||||
|         public Width() { | ||||
|             super("Width"); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             View root = (View) view.getParent(); | ||||
|             List<Integer> list = new ArrayList<>(); | ||||
|             int boundary = root.getWidth(); | ||||
|             if (boundary == 0) boundary = 1000; | ||||
|             int step = boundary / 10; | ||||
|             list.add(ViewGroup.LayoutParams.WRAP_CONTENT); | ||||
|             list.add(ViewGroup.LayoutParams.MATCH_PARENT); | ||||
|             for (int i = step; i < boundary; i += step) { | ||||
|                 list.add(i); | ||||
|             } | ||||
|             return list; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Integer get(@NonNull CameraView view) { | ||||
|             return view.getLayoutParams().width; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Integer value) { | ||||
|             view.getLayoutParams().width = (int) value; | ||||
|             view.setLayoutParams(view.getLayoutParams()); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public String toString(@NonNull Integer value) { | ||||
|             if (value == ViewGroup.LayoutParams.MATCH_PARENT) return "match parent"; | ||||
|             if (value == ViewGroup.LayoutParams.WRAP_CONTENT) return "wrap content"; | ||||
|             return super.toString(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Height extends Option<Integer> { | ||||
|         public Height() { | ||||
|             super("Height"); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             View root = (View) view.getParent(); | ||||
|             ArrayList<Integer> list = new ArrayList<>(); | ||||
|             int boundary = root.getHeight(); | ||||
|             if (boundary == 0) boundary = 1000; | ||||
|             int step = boundary / 10; | ||||
|             list.add(ViewGroup.LayoutParams.WRAP_CONTENT); | ||||
|             list.add(ViewGroup.LayoutParams.MATCH_PARENT); | ||||
|             for (int i = step; i < boundary; i += step) { | ||||
|                 list.add(i); | ||||
|             } | ||||
|             return list; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Integer get(@NonNull CameraView view) { | ||||
|             return view.getLayoutParams().height; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Integer value) { | ||||
|             view.getLayoutParams().height = (int) value; | ||||
|             view.setLayoutParams(view.getLayoutParams()); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public String toString(@NonNull Integer value) { | ||||
|             if (value == ViewGroup.LayoutParams.MATCH_PARENT) return "match parent"; | ||||
|             if (value == ViewGroup.LayoutParams.WRAP_CONTENT) return "wrap content"; | ||||
|             return super.toString(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static abstract class ControlOption<T extends Control> extends Option<T> { | ||||
|         private final Class<T> controlClass; | ||||
| 
 | ||||
|         ControlOption(@NonNull Class<T> controlClass, String name) { | ||||
|             super(name); | ||||
|             this.controlClass = controlClass; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull T value) { | ||||
|             view.set(value); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public T get(@NonNull CameraView view) { | ||||
|             return view.get(controlClass); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<T> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return options.getSupportedControls(controlClass); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Mode extends ControlOption<com.otaliastudios.cameraview.controls.Mode> { | ||||
|         public Mode() { | ||||
|             super(com.otaliastudios.cameraview.controls.Mode.class, "Mode"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Engine extends ControlOption<com.otaliastudios.cameraview.controls.Engine> { | ||||
|         public Engine() { | ||||
|             super(com.otaliastudios.cameraview.controls.Engine.class, "Engine"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(final @NonNull CameraView view, final @NonNull com.otaliastudios.cameraview.controls.Engine value) { | ||||
|             boolean started = view.isOpened(); | ||||
|             if (started) { | ||||
|                 view.addCameraListener(new CameraListener() { | ||||
|                     @Override | ||||
|                     public void onCameraClosed() { | ||||
|                         super.onCameraClosed(); | ||||
|                         view.removeCameraListener(this); | ||||
|                         view.setEngine(value); | ||||
|                         view.open(); | ||||
|                     } | ||||
|                 }); | ||||
|                 view.close(); | ||||
|             } else { | ||||
|                 view.setEngine(value); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Preview extends ControlOption<com.otaliastudios.cameraview.controls.Preview> { | ||||
|         public Preview() { | ||||
|             super(com.otaliastudios.cameraview.controls.Preview.class, "Preview Surface"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(final @NonNull CameraView view, final @NonNull com.otaliastudios.cameraview.controls.Preview value) { | ||||
|             boolean opened = view.isOpened(); | ||||
|             if (opened) { | ||||
|                 view.addCameraListener(new CameraListener() { | ||||
|                     @Override | ||||
|                     public void onCameraClosed() { | ||||
|                         super.onCameraClosed(); | ||||
|                         view.removeCameraListener(this); | ||||
|                         applyPreview(view, value); | ||||
|                         view.open(); | ||||
|                     } | ||||
|                 }); | ||||
|                 view.close(); | ||||
|             } else { | ||||
|                 applyPreview(view, value); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // This is really tricky since the preview can only be changed when not attached to window.
 | ||||
|         private void applyPreview(@NonNull CameraView cameraView, | ||||
|                                   @NonNull com.otaliastudios.cameraview.controls.Preview newPreview) { | ||||
|             ViewGroup.LayoutParams params = cameraView.getLayoutParams(); | ||||
|             ViewGroup parent = (ViewGroup) cameraView.getParent(); | ||||
|             int index = 0; | ||||
|             for (int i = 0; i < parent.getChildCount(); i++) { | ||||
|                 if (parent.getChildAt(i) == cameraView) { | ||||
|                     index = i; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             parent.removeView(cameraView); | ||||
|             cameraView.setPreview(newPreview); | ||||
|             parent.addView(cameraView, index, params); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Flash extends ControlOption<com.otaliastudios.cameraview.controls.Flash> { | ||||
|         public Flash() { | ||||
|             super(com.otaliastudios.cameraview.controls.Flash.class, "Flash"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class WhiteBalance extends ControlOption<com.otaliastudios.cameraview.controls.WhiteBalance> { | ||||
|         public WhiteBalance() { | ||||
|             super(com.otaliastudios.cameraview.controls.WhiteBalance.class, "White Balance"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Hdr extends ControlOption<com.otaliastudios.cameraview.controls.Hdr> { | ||||
|         public Hdr() { | ||||
|             super(com.otaliastudios.cameraview.controls.Hdr.class, "HDR"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class PictureMetering extends Option<Boolean> { | ||||
| 
 | ||||
|         public PictureMetering() { | ||||
|             super("Picture Metering"); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Boolean get(@NonNull CameraView view) { | ||||
|             return view.getPictureMetering(); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return Arrays.asList(true, false); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Boolean value) { | ||||
|             view.setPictureMetering(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class PictureSnapshotMetering extends Option<Boolean> { | ||||
| 
 | ||||
|         public PictureSnapshotMetering() { | ||||
|             super("Picture Snapshot Metering"); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Boolean get(@NonNull CameraView view) { | ||||
|             return view.getPictureSnapshotMetering(); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return Arrays.asList(true, false); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Boolean value) { | ||||
|             view.setPictureSnapshotMetering(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class VideoCodec extends ControlOption<com.otaliastudios.cameraview.controls.VideoCodec> { | ||||
|         public VideoCodec() { | ||||
|             super(com.otaliastudios.cameraview.controls.VideoCodec.class, "Video Codec"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class AudioCodec extends ControlOption<com.otaliastudios.cameraview.controls.AudioCodec> { | ||||
|         public AudioCodec() { | ||||
|             super(com.otaliastudios.cameraview.controls.AudioCodec.class, "Audio Codec"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Audio extends ControlOption<com.otaliastudios.cameraview.controls.Audio> { | ||||
|         public Audio() { | ||||
|             super(com.otaliastudios.cameraview.controls.Audio.class, "Audio"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static abstract class GestureOption extends Option<GestureAction> { | ||||
|         private final Gesture gesture; | ||||
|         private final GestureAction[] allActions = GestureAction.values(); | ||||
| 
 | ||||
|         GestureOption(@NonNull Gesture gesture, String name) { | ||||
|             super(name); | ||||
|             this.gesture = gesture; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull GestureAction value) { | ||||
|             view.mapGesture(gesture, value); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public GestureAction get(@NonNull CameraView view) { | ||||
|             return view.getGestureAction(gesture); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<GestureAction> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             List<GestureAction> list = new ArrayList<>(); | ||||
|             for (GestureAction action : allActions) { | ||||
|                 if (gesture.isAssignableTo(action) && options.supports(action)) { | ||||
|                     list.add(action); | ||||
|                 } | ||||
|             } | ||||
|             return list; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Pinch extends GestureOption { | ||||
|         public Pinch() { | ||||
|             super(Gesture.PINCH, "Pinch"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class HorizontalScroll extends GestureOption { | ||||
|         public HorizontalScroll() { | ||||
|             super(Gesture.SCROLL_HORIZONTAL, "Horizontal Scroll"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class VerticalScroll extends GestureOption { | ||||
|         public VerticalScroll() { | ||||
|             super(Gesture.SCROLL_VERTICAL, "Vertical Scroll"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Tap extends GestureOption { | ||||
|         public Tap() { | ||||
|             super(Gesture.TAP, "Tap"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class LongTap extends GestureOption { | ||||
|         public LongTap() { | ||||
|             super(Gesture.LONG_TAP, "Long Tap"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static abstract class OverlayOption extends Option<Boolean> { | ||||
|         private View overlay; | ||||
|         private Overlay.Target target; | ||||
| 
 | ||||
|         OverlayOption(@NonNull Overlay.Target target, @NonNull String name, @NonNull View overlay) { | ||||
|             super(name); | ||||
|             this.overlay = overlay; | ||||
|             this.target = target; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return Arrays.asList(true, false); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Boolean get(@NonNull CameraView view) { | ||||
|             OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams(); | ||||
|             switch (target) { | ||||
|                 case PREVIEW: return params.drawOnPreview; | ||||
|                 case PICTURE_SNAPSHOT: return params.drawOnPictureSnapshot; | ||||
|                 case VIDEO_SNAPSHOT: return params.drawOnVideoSnapshot; | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Boolean value) { | ||||
|             OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams(); | ||||
|             switch (target) { | ||||
|                 case PREVIEW: params.drawOnPreview = value; break; | ||||
|                 case PICTURE_SNAPSHOT: params.drawOnPictureSnapshot = value; break; | ||||
|                 case VIDEO_SNAPSHOT: params.drawOnVideoSnapshot = value; break; | ||||
|             } | ||||
|             overlay.setLayoutParams(params); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class OverlayInPreview extends OverlayOption { | ||||
|         public OverlayInPreview(@NonNull View overlay) { | ||||
|             super(Overlay.Target.PREVIEW, "Overlay in Preview", overlay); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class OverlayInPictureSnapshot extends OverlayOption { | ||||
|         public OverlayInPictureSnapshot(@NonNull View overlay) { | ||||
|             super(Overlay.Target.PICTURE_SNAPSHOT, "Overlay in Picture Snapshot", overlay); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class OverlayInVideoSnapshot extends OverlayOption { | ||||
|         public OverlayInVideoSnapshot(@NonNull View overlay) { | ||||
|             super(Overlay.Target.VIDEO_SNAPSHOT, "Overlay in Video Snapshot", overlay); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Grid extends ControlOption<com.otaliastudios.cameraview.controls.Grid> { | ||||
|         public Grid() { | ||||
|             super(com.otaliastudios.cameraview.controls.Grid.class, "Grid Lines"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class GridColor extends Option<Pair<Integer, String>> { | ||||
| 
 | ||||
|         public GridColor() { | ||||
|             super("Grid Color"); | ||||
|         } | ||||
| 
 | ||||
|         private static final List<Pair<Integer, String>> ALL = Arrays.asList( | ||||
|                 new Pair<>(Color.argb(160, 255, 255, 255), "default"), | ||||
|                 new Pair<>(Color.WHITE, "white"), | ||||
|                 new Pair<>(Color.BLACK, "black"), | ||||
|                 new Pair<>(Color.YELLOW, "yellow") | ||||
|         ); | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Pair<Integer, String>> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return ALL; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Pair<Integer, String> get(@NonNull CameraView view) { | ||||
|             for (Pair<Integer, String> pair : ALL) { | ||||
|                 //noinspection ConstantConditions
 | ||||
|                 if (pair.first == view.getGridColor()) { | ||||
|                     return pair; | ||||
|                 } | ||||
|             } | ||||
|             throw new RuntimeException("Could not find grid color"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Pair<Integer, String> value) { | ||||
|             //noinspection ConstantConditions
 | ||||
|             view.setGridColor(value.first); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public String toString(@NonNull Pair<Integer, String> value) { | ||||
|             //noinspection ConstantConditions
 | ||||
|             return value.second; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class UseDeviceOrientation extends Option<Boolean> { | ||||
|         public UseDeviceOrientation() { | ||||
|             super("Use Device Orientation"); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return Arrays.asList(true, false); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Boolean get(@NonNull CameraView view) { | ||||
|             return view.getUseDeviceOrientation(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Boolean value) { | ||||
|             view.setUseDeviceOrientation(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class PreviewFrameRate extends Option<Integer> { | ||||
|         public PreviewFrameRate() { | ||||
|             super("Preview FPS"); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             float min = options.getPreviewFrameRateMinValue(); | ||||
|             float max = options.getPreviewFrameRateMaxValue(); | ||||
|             float delta = max - min; | ||||
|             List<Integer> result = new ArrayList<>(); | ||||
|             if (min == 0F && max == 0F) { | ||||
|                 return result; // empty list
 | ||||
|             } else if (delta < 0.005F) { | ||||
|                 result.add(Math.round(min)); | ||||
|                 return result; // single value
 | ||||
|             } else { | ||||
|                 final int steps = 3; | ||||
|                 final float step = delta / steps; | ||||
|                 for (int i = 0; i <= steps; i++) { | ||||
|                     result.add(Math.round(min)); | ||||
|                     min += step; | ||||
|                 } | ||||
|                 return result; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Integer get(@NonNull CameraView view) { | ||||
|             return Math.round(view.getPreviewFrameRate()); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Integer value) { | ||||
|             view.setPreviewFrameRate((float) value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class PictureFormat extends ControlOption<com.otaliastudios.cameraview.controls.PictureFormat> { | ||||
|         public PictureFormat() { | ||||
|             super(com.otaliastudios.cameraview.controls.PictureFormat.class, "Picture Format"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class FrameProcessingFormat extends Option<Integer> { | ||||
| 
 | ||||
|         FrameProcessingFormat() { | ||||
|             super("Frame Processing Format"); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void set(@NonNull CameraView view, @NonNull Integer value) { | ||||
|             view.setFrameProcessingFormat(value); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Integer get(@NonNull CameraView view) { | ||||
|             return view.getFrameProcessingFormat(); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) { | ||||
|             return options.getSupportedFrameProcessingFormats(); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public String toString(@NonNull Integer value) { | ||||
|             switch (value) { | ||||
|                 case ImageFormat.NV21: return "NV21"; | ||||
|                 case ImageFormat.NV16: return "NV16"; | ||||
|                 case ImageFormat.JPEG: return "JPEG"; | ||||
|                 case ImageFormat.YUY2: return "YUY2"; | ||||
|                 case ImageFormat.YUV_420_888: return "YUV_420_888"; | ||||
|                 case ImageFormat.YUV_422_888: return "YUV_422_888"; | ||||
|                 case ImageFormat.YUV_444_888: return "YUV_444_888"; | ||||
|                 case ImageFormat.RAW10: return "RAW10"; | ||||
|                 case ImageFormat.RAW12: return "RAW12"; | ||||
|                 case ImageFormat.RAW_SENSOR: return "RAW_SENSOR"; | ||||
|             } | ||||
|             return super.toString(value); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,97 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.demo; | ||||
| 
 | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.AdapterView; | ||||
| import android.widget.ArrayAdapter; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.Spinner; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraOptions; | ||||
| import com.otaliastudios.cameraview.CameraView; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| 
 | ||||
| @SuppressLint("ViewConstructor") | ||||
| public class OptionView<Value> extends LinearLayout implements Spinner.OnItemSelectedListener { | ||||
| 
 | ||||
|     interface Callback { | ||||
|         <T> boolean onValueChanged(@NonNull Option<T> option, @NonNull T value, @NonNull String name); | ||||
|     } | ||||
| 
 | ||||
|     private Value value; | ||||
|     private ArrayList<Value> values; | ||||
|     private ArrayList<String> valuesStrings; | ||||
|     private Option option; | ||||
|     private Callback callback; | ||||
|     private Spinner spinner; | ||||
| 
 | ||||
|     public OptionView(@NonNull Context context) { | ||||
|         super(context); | ||||
|         setOrientation(VERTICAL); | ||||
|         inflate(context, R.layout.option_view, this); | ||||
|         ViewGroup content = findViewById(R.id.content); | ||||
|         spinner = new Spinner(context, Spinner.MODE_DROPDOWN); | ||||
|         content.addView(spinner); | ||||
|     } | ||||
| 
 | ||||
|     public void setHasDivider(boolean hasDivider) { | ||||
|         View divider = findViewById(R.id.divider); | ||||
|         divider.setVisibility(hasDivider ? View.VISIBLE : View.GONE); | ||||
|     } | ||||
| 
 | ||||
|     public void setOption(@NonNull Option<Value> option, @NonNull Callback callback) { | ||||
|         this.option = option; | ||||
|         this.callback = callback; | ||||
|         TextView title = findViewById(R.id.title); | ||||
|         title.setText(option.getName()); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("all") | ||||
|     public void onCameraOpened(CameraView view, CameraOptions options) { | ||||
|         values = new ArrayList(option.getAll(view, options)); | ||||
|         value = (Value) option.get(view); | ||||
|         valuesStrings = new ArrayList<>(); | ||||
|         for (Value value : values) { | ||||
|             valuesStrings.add(option.toString(value)); | ||||
|         } | ||||
| 
 | ||||
|         if (values.isEmpty()) { | ||||
|             spinner.setOnItemSelectedListener(null); | ||||
|             spinner.setEnabled(false); | ||||
|             spinner.setAlpha(0.8f); | ||||
|             spinner.setAdapter(new ArrayAdapter(getContext(), | ||||
|                     R.layout.spinner_text, new String[]{ "Not supported." })); | ||||
|             spinner.setSelection(0, false); | ||||
|         } else { | ||||
|             spinner.setEnabled(true); | ||||
|             spinner.setAlpha(1f); | ||||
|             spinner.setAdapter(new ArrayAdapter(getContext(), | ||||
|                     R.layout.spinner_text, valuesStrings)); | ||||
|             spinner.setSelection(values.indexOf(value), false); | ||||
|             spinner.setOnItemSelectedListener(this); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { | ||||
|         if (!values.get(i).equals(value)) { | ||||
|             Log.e("ControlView", "curr: " + value + " new: " + values.get(i)); | ||||
|             if (!callback.onValueChanged(option, values.get(i), valuesStrings.get(i))) { | ||||
|                 spinner.setSelection(values.indexOf(value)); // Go back.
 | ||||
|             } else { | ||||
|                 value = values.get(i); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onNothingSelected(AdapterView<?> adapterView) {} | ||||
| } | ||||
| @ -1,136 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.demo; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.graphics.Color; | ||||
| import android.graphics.drawable.ColorDrawable; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.core.content.FileProvider; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraUtils; | ||||
| import com.otaliastudios.cameraview.FileCallback; | ||||
| import com.otaliastudios.cameraview.size.AspectRatio; | ||||
| import com.otaliastudios.cameraview.BitmapCallback; | ||||
| import com.otaliastudios.cameraview.PictureResult; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.FileOutputStream; | ||||
| 
 | ||||
| 
 | ||||
| public class PicturePreviewActivity extends AppCompatActivity { | ||||
| 
 | ||||
|     private static PictureResult picture; | ||||
| 
 | ||||
|     public static void setPictureResult(@Nullable PictureResult pictureResult) { | ||||
|         picture = pictureResult; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_picture_preview); | ||||
|         final PictureResult result = picture; | ||||
|         if (result == null) { | ||||
|             finish(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         final ImageView imageView = findViewById(R.id.image); | ||||
|         final MessageView captureResolution = findViewById(R.id.nativeCaptureResolution); | ||||
|         final MessageView captureLatency = findViewById(R.id.captureLatency); | ||||
|         final MessageView exifRotation = findViewById(R.id.exifRotation); | ||||
| 
 | ||||
|         final long delay = getIntent().getLongExtra("delay", 0); | ||||
|         AspectRatio ratio = AspectRatio.of(result.getSize()); | ||||
|         captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds"); | ||||
|         captureResolution.setTitleAndMessage("Resolution", result.getSize() + " (" + ratio + ")"); | ||||
|         exifRotation.setTitleAndMessage("EXIF rotation", result.getRotation() + ""); | ||||
|         try { | ||||
|             result.toBitmap(1000, 1000, new BitmapCallback() { | ||||
|                 @Override | ||||
|                 public void onBitmapReady(Bitmap bitmap) { | ||||
|                     imageView.setImageBitmap(bitmap); | ||||
|                 } | ||||
|             }); | ||||
|         } catch (UnsupportedOperationException e) { | ||||
|             imageView.setImageDrawable(new ColorDrawable(Color.GREEN)); | ||||
|             Toast.makeText(this, "Can't preview this format: " + picture.getFormat(), | ||||
|                     Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
| 
 | ||||
|         if (result.isSnapshot()) { | ||||
|             // Log the real size for debugging reason.
 | ||||
|             BitmapFactory.Options options = new BitmapFactory.Options(); | ||||
|             options.inJustDecodeBounds = true; | ||||
|             BitmapFactory.decodeByteArray(result.getData(), 0, result.getData().length, options); | ||||
|             if (result.getRotation() % 180 != 0) { | ||||
|                 Log.e("PicturePreview", "The picture full size is " + result.getSize().getHeight() + "x" + result.getSize().getWidth()); | ||||
|             } else { | ||||
|                 Log.e("PicturePreview", "The picture full size is " + result.getSize().getWidth() + "x" + result.getSize().getHeight()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (!isChangingConfigurations()) { | ||||
|             setPictureResult(null); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onCreateOptionsMenu(Menu menu) { | ||||
|         getMenuInflater().inflate(R.menu.share, menu); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         if (item.getItemId() == R.id.share) { | ||||
|             Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show(); | ||||
|             String extension; | ||||
|             switch (picture.getFormat()) { | ||||
|                 case JPEG: extension = "jpg"; break; | ||||
|                 case DNG: extension = "dng"; break; | ||||
|                 default: throw new RuntimeException("Unknown format."); | ||||
|             } | ||||
|             File file = new File(getFilesDir(), "picture." + extension); | ||||
|             CameraUtils.writeToFile(picture.getData(), file, new FileCallback() { | ||||
|                 @Override | ||||
|                 public void onFileReady(@Nullable File file) { | ||||
|                     if (file != null) { | ||||
|                         Context context = PicturePreviewActivity.this; | ||||
|                         Intent intent = new Intent(Intent.ACTION_SEND); | ||||
|                         intent.setType("image/*"); | ||||
|                         Uri uri = FileProvider.getUriForFile(context, | ||||
|                                 context.getPackageName() + ".provider", | ||||
|                                 file); | ||||
|                         intent.putExtra(Intent.EXTRA_STREAM, uri); | ||||
|                         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
|                         startActivity(intent); | ||||
|                     } else { | ||||
|                         Toast.makeText(PicturePreviewActivity.this, | ||||
|                                 "Error while writing file.", | ||||
|                                 Toast.LENGTH_SHORT).show(); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|             return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
| } | ||||
| @ -1,136 +0,0 @@ | ||||
| package com.otaliastudios.cameraview.demo; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.media.MediaPlayer; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.core.content.FileProvider; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.MediaController; | ||||
| import android.widget.Toast; | ||||
| import android.widget.VideoView; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraUtils; | ||||
| import com.otaliastudios.cameraview.FileCallback; | ||||
| import com.otaliastudios.cameraview.VideoResult; | ||||
| import com.otaliastudios.cameraview.size.AspectRatio; | ||||
| 
 | ||||
| import java.io.File; | ||||
| 
 | ||||
| 
 | ||||
| public class VideoPreviewActivity extends AppCompatActivity { | ||||
| 
 | ||||
|     private VideoView videoView; | ||||
| 
 | ||||
|     private static VideoResult videoResult; | ||||
| 
 | ||||
|     public static void setVideoResult(@Nullable VideoResult result) { | ||||
|         videoResult = result; | ||||
|     } | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_video_preview); | ||||
|         final VideoResult result = videoResult; | ||||
|         if (result == null) { | ||||
|             finish(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         videoView = findViewById(R.id.video); | ||||
|         videoView.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View view) { | ||||
|                 playVideo(); | ||||
|             } | ||||
|         }); | ||||
|         final MessageView actualResolution = findViewById(R.id.actualResolution); | ||||
|         final MessageView isSnapshot = findViewById(R.id.isSnapshot); | ||||
|         final MessageView rotation = findViewById(R.id.rotation); | ||||
|         final MessageView audio = findViewById(R.id.audio); | ||||
|         final MessageView audioBitRate = findViewById(R.id.audioBitRate); | ||||
|         final MessageView videoCodec = findViewById(R.id.videoCodec); | ||||
|         final MessageView audioCodec = findViewById(R.id.audioCodec); | ||||
|         final MessageView videoBitRate = findViewById(R.id.videoBitRate); | ||||
|         final MessageView videoFrameRate = findViewById(R.id.videoFrameRate); | ||||
| 
 | ||||
|         AspectRatio ratio = AspectRatio.of(result.getSize()); | ||||
|         actualResolution.setTitleAndMessage("Size", result.getSize() + " (" + ratio + ")"); | ||||
|         isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot() + ""); | ||||
|         rotation.setTitleAndMessage("Rotation", result.getRotation() + ""); | ||||
|         audio.setTitleAndMessage("Audio", result.getAudio().name()); | ||||
|         audioBitRate.setTitleAndMessage("Audio bit rate", result.getAudioBitRate() + " bits per sec."); | ||||
|         videoCodec.setTitleAndMessage("VideoCodec", result.getVideoCodec().name()); | ||||
|         audioCodec.setTitleAndMessage("AudioCodec", result.getAudioCodec().name()); | ||||
|         videoBitRate.setTitleAndMessage("Video bit rate", result.getVideoBitRate() + " bits per sec."); | ||||
|         videoFrameRate.setTitleAndMessage("Video frame rate", result.getVideoFrameRate() + " fps"); | ||||
|         MediaController controller = new MediaController(this); | ||||
|         controller.setAnchorView(videoView); | ||||
|         controller.setMediaPlayer(videoView); | ||||
|         videoView.setMediaController(controller); | ||||
|         videoView.setVideoURI(Uri.fromFile(result.getFile())); | ||||
|         videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { | ||||
|             @Override | ||||
|             public void onPrepared(MediaPlayer mp) { | ||||
|                 ViewGroup.LayoutParams lp = videoView.getLayoutParams(); | ||||
|                 float videoWidth = mp.getVideoWidth(); | ||||
|                 float videoHeight = mp.getVideoHeight(); | ||||
|                 float viewWidth = videoView.getWidth(); | ||||
|                 lp.height = (int) (viewWidth * (videoHeight / videoWidth)); | ||||
|                 videoView.setLayoutParams(lp); | ||||
|                 playVideo(); | ||||
| 
 | ||||
|                 if (result.isSnapshot()) { | ||||
|                     // Log the real size for debugging reason.
 | ||||
|                     Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     void playVideo() { | ||||
|         if (!videoView.isPlaying()) { | ||||
|             videoView.start(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (!isChangingConfigurations()) { | ||||
|             setVideoResult(null); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onCreateOptionsMenu(Menu menu) { | ||||
|         getMenuInflater().inflate(R.menu.share, menu); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         if (item.getItemId() == R.id.share) { | ||||
|             Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show(); | ||||
|             Intent intent = new Intent(Intent.ACTION_SEND); | ||||
|             intent.setType("video/*"); | ||||
|             Uri uri = FileProvider.getUriForFile(this, | ||||
|                     this.getPackageName() + ".provider", | ||||
|                     videoResult.getFile()); | ||||
|             intent.putExtra(Intent.EXTRA_STREAM, uri); | ||||
|             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
|             startActivity(intent); | ||||
|             return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,344 @@ | ||||
| package com.otaliastudios.cameraview.demo | ||||
| 
 | ||||
| import android.animation.ValueAnimator | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.content.pm.PackageManager.PERMISSION_GRANTED | ||||
| import android.graphics.* | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.view.ViewGroup.LayoutParams.MATCH_PARENT | ||||
| import android.view.ViewGroup.LayoutParams.WRAP_CONTENT | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import com.google.android.material.bottomsheet.BottomSheetBehavior | ||||
| import com.otaliastudios.cameraview.* | ||||
| import com.otaliastudios.cameraview.controls.Facing | ||||
| import com.otaliastudios.cameraview.controls.Mode | ||||
| import com.otaliastudios.cameraview.controls.Preview | ||||
| import com.otaliastudios.cameraview.filter.Filters | ||||
| import com.otaliastudios.cameraview.frame.Frame | ||||
| import com.otaliastudios.cameraview.frame.FrameProcessor | ||||
| import java.io.ByteArrayOutputStream | ||||
| import java.io.File | ||||
| import java.util.* | ||||
| 
 | ||||
| class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Callback { | ||||
| 
 | ||||
|     companion object { | ||||
|         private val LOG = CameraLogger.create("DemoApp") | ||||
|         private const val USE_FRAME_PROCESSOR = true | ||||
|         private const val DECODE_BITMAP = false | ||||
|     } | ||||
| 
 | ||||
|     private val camera: CameraView by lazy { findViewById(R.id.camera) } | ||||
|     private val controlPanel: ViewGroup by lazy { findViewById(R.id.controls) } | ||||
|     private var captureTime: Long = 0 | ||||
| 
 | ||||
|     private var currentFilter = 0 | ||||
|     private val allFilters = Filters.values() | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         setContentView(R.layout.activity_camera) | ||||
|         CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE) | ||||
|         camera.setLifecycleOwner(this) | ||||
|         camera.addCameraListener(Listener()) | ||||
|         if (USE_FRAME_PROCESSOR) { | ||||
|             camera.addFrameProcessor(object : FrameProcessor { | ||||
|                 private var lastTime = System.currentTimeMillis() | ||||
|                 override fun process(frame: Frame) { | ||||
|                     val newTime = frame.time | ||||
|                     val delay = newTime - lastTime | ||||
|                     lastTime = newTime | ||||
|                     LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay) | ||||
|                     if (DECODE_BITMAP) { | ||||
|                         if (frame.format == ImageFormat.NV21 | ||||
|                                 && frame.dataClass == ByteArray::class.java) { | ||||
|                             val data = frame.getData<ByteArray>() | ||||
|                             val yuvImage = YuvImage(data, | ||||
|                                     frame.format, | ||||
|                                     frame.size.width, | ||||
|                                     frame.size.height, | ||||
|                                     null) | ||||
|                             val jpegStream = ByteArrayOutputStream() | ||||
|                             yuvImage.compressToJpeg(Rect(0, 0, | ||||
|                                     frame.size.width, | ||||
|                                     frame.size.height), 100, jpegStream) | ||||
|                             val jpegByteArray = jpegStream.toByteArray() | ||||
|                             val bitmap = BitmapFactory.decodeByteArray(jpegByteArray, | ||||
|                                     0, jpegByteArray.size) | ||||
|                             bitmap.toString() | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|         findViewById<View>(R.id.edit).setOnClickListener(this) | ||||
|         findViewById<View>(R.id.capturePicture).setOnClickListener(this) | ||||
|         findViewById<View>(R.id.capturePictureSnapshot).setOnClickListener(this) | ||||
|         findViewById<View>(R.id.captureVideo).setOnClickListener(this) | ||||
|         findViewById<View>(R.id.captureVideoSnapshot).setOnClickListener(this) | ||||
|         findViewById<View>(R.id.toggleCamera).setOnClickListener(this) | ||||
|         findViewById<View>(R.id.changeFilter).setOnClickListener(this) | ||||
|         val group = controlPanel.getChildAt(0) as ViewGroup | ||||
|         val watermark = findViewById<View>(R.id.watermark) | ||||
|         val options: List<Option<*>> = listOf( | ||||
|                 // Layout | ||||
|                 Option.Width(), Option.Height(), | ||||
|                 // Engine and preview | ||||
|                 Option.Mode(), Option.Engine(), Option.Preview(), | ||||
|                 // Some controls | ||||
|                 Option.Flash(), Option.WhiteBalance(), Option.Hdr(), | ||||
|                 Option.PictureMetering(), Option.PictureSnapshotMetering(), | ||||
|                 Option.PictureFormat(), | ||||
|                 // Video recording | ||||
|                 Option.PreviewFrameRate(), Option.VideoCodec(), Option.Audio(), Option.AudioCodec(), | ||||
|                 // Gestures | ||||
|                 Option.Pinch(), Option.HorizontalScroll(), Option.VerticalScroll(), | ||||
|                 Option.Tap(), Option.LongTap(), | ||||
|                 // Watermarks | ||||
|                 Option.OverlayInPreview(watermark), | ||||
|                 Option.OverlayInPictureSnapshot(watermark), | ||||
|                 Option.OverlayInVideoSnapshot(watermark), | ||||
|                 // Frame Processing | ||||
|                 Option.FrameProcessingFormat(), | ||||
|                 // Other | ||||
|                 Option.Grid(), Option.GridColor(), Option.UseDeviceOrientation() | ||||
|         ) | ||||
|         val dividers = listOf( | ||||
|                 // Layout | ||||
|                 false, true, | ||||
|                 // Engine and preview | ||||
|                 false, false, true, | ||||
|                 // Some controls | ||||
|                 false, false, false, false, false, true, | ||||
|                 // Video recording | ||||
|                 false, false, false, true, | ||||
|                 // Gestures | ||||
|                 false, false, false, false, true, | ||||
|                 // Watermarks | ||||
|                 false, false, true, | ||||
|                 // Frame Processing | ||||
|                 true, | ||||
|                 // Other | ||||
|                 false, false, true | ||||
|         ) | ||||
|         for (i in options.indices) { | ||||
|             val view = OptionView<Any>(this) | ||||
|             view.setOption(options[i] as Option<Any>, this) | ||||
|             view.setHasDivider(dividers[i]) | ||||
|             group.addView(view, MATCH_PARENT, WRAP_CONTENT) | ||||
|         } | ||||
|         controlPanel.viewTreeObserver.addOnGlobalLayoutListener { | ||||
|             BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_HIDDEN | ||||
|         } | ||||
| 
 | ||||
|         // Animate the watermark just to show we record the animation in video snapshots | ||||
|         val animator = ValueAnimator.ofFloat(1f, 0.8f) | ||||
|         animator.duration = 300 | ||||
|         animator.repeatCount = ValueAnimator.INFINITE | ||||
|         animator.repeatMode = ValueAnimator.REVERSE | ||||
|         animator.addUpdateListener { animation -> | ||||
|             val scale = animation.animatedValue as Float | ||||
|             watermark.scaleX = scale | ||||
|             watermark.scaleY = scale | ||||
|             watermark.rotation = watermark.rotation + 2 | ||||
|         } | ||||
|         animator.start() | ||||
|     } | ||||
| 
 | ||||
|     private fun message(content: String, important: Boolean) { | ||||
|         if (important) { | ||||
|             LOG.w(content) | ||||
|             Toast.makeText(this, content, Toast.LENGTH_LONG).show() | ||||
|         } else { | ||||
|             LOG.i(content) | ||||
|             Toast.makeText(this, content, Toast.LENGTH_SHORT).show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private inner class Listener : CameraListener() { | ||||
|         override fun onCameraOpened(options: CameraOptions) { | ||||
|             val group = controlPanel.getChildAt(0) as ViewGroup | ||||
|             for (i in 0 until group.childCount) { | ||||
|                 val view = group.getChildAt(i) as OptionView<*> | ||||
|                 view.onCameraOpened(camera, options) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         override fun onCameraError(exception: CameraException) { | ||||
|             super.onCameraError(exception) | ||||
|             message("Got CameraException #" + exception.reason, true) | ||||
|         } | ||||
| 
 | ||||
|         override fun onPictureTaken(result: PictureResult) { | ||||
|             super.onPictureTaken(result) | ||||
|             if (camera.isTakingVideo) { | ||||
|                 message("Captured while taking video. Size=" + result.size, false) | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             // This can happen if picture was taken with a gesture. | ||||
|             val callbackTime = System.currentTimeMillis() | ||||
|             if (captureTime == 0L) captureTime = callbackTime - 300 | ||||
|             LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - captureTime) | ||||
|             PicturePreviewActivity.pictureResult = result | ||||
|             val intent = Intent(this@CameraActivity, PicturePreviewActivity::class.java) | ||||
|             intent.putExtra("delay", callbackTime - captureTime) | ||||
|             startActivity(intent) | ||||
|             captureTime = 0 | ||||
|             LOG.w("onPictureTaken called! Launched activity.") | ||||
|         } | ||||
| 
 | ||||
|         override fun onVideoTaken(result: VideoResult) { | ||||
|             super.onVideoTaken(result) | ||||
|             LOG.w("onVideoTaken called! Launching activity.") | ||||
|             VideoPreviewActivity.videoResult = result | ||||
|             val intent = Intent(this@CameraActivity, VideoPreviewActivity::class.java) | ||||
|             startActivity(intent) | ||||
|             LOG.w("onVideoTaken called! Launched activity.") | ||||
|         } | ||||
| 
 | ||||
|         override fun onVideoRecordingStart() { | ||||
|             super.onVideoRecordingStart() | ||||
|             LOG.w("onVideoRecordingStart!") | ||||
|         } | ||||
| 
 | ||||
|         override fun onVideoRecordingEnd() { | ||||
|             super.onVideoRecordingEnd() | ||||
|             message("Video taken. Processing...", false) | ||||
|             LOG.w("onVideoRecordingEnd!") | ||||
|         } | ||||
| 
 | ||||
|         override fun onExposureCorrectionChanged(newValue: Float, bounds: FloatArray, fingers: Array<PointF>?) { | ||||
|             super.onExposureCorrectionChanged(newValue, bounds, fingers) | ||||
|             message("Exposure correction:$newValue", false) | ||||
|         } | ||||
| 
 | ||||
|         override fun onZoomChanged(newValue: Float, bounds: FloatArray, fingers: Array<PointF>?) { | ||||
|             super.onZoomChanged(newValue, bounds, fingers) | ||||
|             message("Zoom:$newValue", false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onClick(view: View) { | ||||
|         when (view.id) { | ||||
|             R.id.edit -> edit() | ||||
|             R.id.capturePicture -> capturePicture() | ||||
|             R.id.capturePictureSnapshot -> capturePictureSnapshot() | ||||
|             R.id.captureVideo -> captureVideo() | ||||
|             R.id.captureVideoSnapshot -> captureVideoSnapshot() | ||||
|             R.id.toggleCamera -> toggleCamera() | ||||
|             R.id.changeFilter -> changeCurrentFilter() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackPressed() { | ||||
|         val b = BottomSheetBehavior.from(controlPanel) | ||||
|         if (b.state != BottomSheetBehavior.STATE_HIDDEN) { | ||||
|             b.state = BottomSheetBehavior.STATE_HIDDEN | ||||
|             return | ||||
|         } | ||||
|         super.onBackPressed() | ||||
|     } | ||||
| 
 | ||||
|     private fun edit() { | ||||
|         BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_COLLAPSED | ||||
|     } | ||||
| 
 | ||||
|     private fun capturePicture() { | ||||
|         if (camera.mode == Mode.VIDEO) return run { | ||||
|             message("Can't take HQ pictures while in VIDEO mode.", false) | ||||
|         } | ||||
|         if (camera.isTakingPicture) return | ||||
|         captureTime = System.currentTimeMillis() | ||||
|         message("Capturing picture...", false) | ||||
|         camera.takePicture() | ||||
|     } | ||||
| 
 | ||||
|     private fun capturePictureSnapshot() { | ||||
|         if (camera.isTakingPicture) return | ||||
|         if (camera.preview != Preview.GL_SURFACE) return run { | ||||
|             message("Picture snapshots are only allowed with the GL_SURFACE preview.", true) | ||||
|         } | ||||
|         captureTime = System.currentTimeMillis() | ||||
|         message("Capturing picture snapshot...", false) | ||||
|         camera.takePictureSnapshot() | ||||
|     } | ||||
| 
 | ||||
|     private fun captureVideo() { | ||||
|         if (camera.mode == Mode.PICTURE) return run { | ||||
|             message("Can't record HQ videos while in PICTURE mode.", false) | ||||
|         } | ||||
|         if (camera.isTakingPicture || camera.isTakingVideo) return | ||||
|         message("Recording for 5 seconds...", true) | ||||
|         camera.takeVideo(File(filesDir, "video.mp4"), 5000) | ||||
|     } | ||||
| 
 | ||||
|     private fun captureVideoSnapshot() { | ||||
|         if (camera.isTakingVideo) return run { | ||||
|             message("Already taking video.", false) | ||||
|         } | ||||
|         if (camera.preview != Preview.GL_SURFACE) return run { | ||||
|             message("Video snapshots are only allowed with the GL_SURFACE preview.", true) | ||||
|         } | ||||
|         message("Recording snapshot for 5 seconds...", true) | ||||
|         camera.takeVideoSnapshot(File(filesDir, "video.mp4"), 5000) | ||||
|     } | ||||
| 
 | ||||
|     private fun toggleCamera() { | ||||
|         if (camera.isTakingPicture || camera.isTakingVideo) return | ||||
|         when (camera.toggleFacing()) { | ||||
|             Facing.BACK -> message("Switched to back camera!", false) | ||||
|             Facing.FRONT -> message("Switched to front camera!", false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun changeCurrentFilter() { | ||||
|         if (camera.preview != Preview.GL_SURFACE) return run { | ||||
|             message("Filters are supported only when preview is Preview.GL_SURFACE.", true) | ||||
|         } | ||||
|         if (currentFilter < allFilters.size - 1) { | ||||
|             currentFilter++ | ||||
|         } else { | ||||
|             currentFilter = 0 | ||||
|         } | ||||
|         val filter = allFilters[currentFilter] | ||||
|         message(filter.toString(), false) | ||||
| 
 | ||||
|         // Normal behavior: | ||||
|         camera.filter = filter.newInstance() | ||||
| 
 | ||||
|         // To test MultiFilter: | ||||
|         // DuotoneFilter duotone = new DuotoneFilter(); | ||||
|         // duotone.setFirstColor(Color.RED); | ||||
|         // duotone.setSecondColor(Color.GREEN); | ||||
|         // camera.setFilter(new MultiFilter(duotone, filter.newInstance())); | ||||
|     } | ||||
| 
 | ||||
|     override fun <T : Any> onValueChanged(option: Option<T>, value: T, name: String): Boolean { | ||||
|         if (option is Option.Width || option is Option.Height) { | ||||
|             val preview = camera.preview | ||||
|             val wrapContent = value as Int == WRAP_CONTENT | ||||
|             if (preview == Preview.SURFACE && !wrapContent) { | ||||
|                 message("The SurfaceView preview does not support width or height changes. " + | ||||
|                         "The view will act as WRAP_CONTENT by default.", true) | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         option.set(camera, value) | ||||
|         BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_HIDDEN | ||||
|         message("Changed " + option.name + " to " + name, false) | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) { | ||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults) | ||||
|         val valid = grantResults.all { it == PERMISSION_GRANTED } | ||||
|         if (valid && !camera.isOpened) { | ||||
|             camera.open() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,34 @@ | ||||
| package com.otaliastudios.cameraview.demo | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.LinearLayout | ||||
| import android.widget.TextView | ||||
| 
 | ||||
| class MessageView @JvmOverloads constructor( | ||||
|         context: Context, | ||||
|         attrs: AttributeSet? = null, | ||||
|         defStyleAttr: Int = 0 | ||||
| ) : LinearLayout(context, attrs, defStyleAttr) { | ||||
| 
 | ||||
|     init { | ||||
|         orientation = VERTICAL | ||||
|         View.inflate(context, R.layout.option_view, this) | ||||
|         val content = findViewById<ViewGroup>(R.id.content) | ||||
|         View.inflate(context, R.layout.spinner_text, content) | ||||
|     } | ||||
| 
 | ||||
|     private val message: TextView = findViewById<ViewGroup>(R.id.content).getChildAt(0) as TextView | ||||
|     private val title: TextView = findViewById(R.id.title) | ||||
| 
 | ||||
|     fun setMessage(message: String) { this.message.text = message } | ||||
| 
 | ||||
|     fun setTitle(title: String) { this.title.text = title } | ||||
| 
 | ||||
|     fun setTitleAndMessage(title: String, message: String) { | ||||
|         setTitle(title) | ||||
|         setMessage(message) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,296 @@ | ||||
| package com.otaliastudios.cameraview.demo | ||||
| 
 | ||||
| import android.graphics.Color | ||||
| import android.graphics.ImageFormat | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.view.ViewGroup.LayoutParams.WRAP_CONTENT | ||||
| import android.view.ViewGroup.LayoutParams.MATCH_PARENT | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraListener | ||||
| import com.otaliastudios.cameraview.CameraOptions | ||||
| import com.otaliastudios.cameraview.CameraView | ||||
| import com.otaliastudios.cameraview.gesture.GestureAction | ||||
| import com.otaliastudios.cameraview.overlay.OverlayLayout | ||||
| import kotlin.math.roundToInt | ||||
| import kotlin.reflect.KClass | ||||
| 
 | ||||
| /** | ||||
|  * Controls that we want to display in a ControlView. | ||||
|  */ | ||||
| abstract class Option<T: Any>(val name: String) { | ||||
| 
 | ||||
|     abstract fun get(view: CameraView): T | ||||
| 
 | ||||
|     abstract fun getAll(view: CameraView, options: CameraOptions): Collection<T> | ||||
| 
 | ||||
|     abstract fun set(view: CameraView, value: T) | ||||
| 
 | ||||
|     open fun toString(value: T): String { | ||||
|         return "$value".replace("_", "_").toLowerCase() | ||||
|     } | ||||
| 
 | ||||
|     class Width : Option<Int>("Width") { | ||||
|         override fun get(view: CameraView) = view.layoutParams.width | ||||
| 
 | ||||
|         override fun set(view: CameraView, value: Int) { | ||||
|             view.layoutParams.width = value | ||||
|             view.layoutParams = view.layoutParams | ||||
|         } | ||||
| 
 | ||||
|         override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> { | ||||
|             val root = view.parent as View | ||||
|             val boundary = root.width.takeIf { it > 0 } ?: 1000 | ||||
|             val step = boundary / 10 | ||||
|             val list = mutableListOf(WRAP_CONTENT, MATCH_PARENT) | ||||
|             for (i in step until boundary step step) { list.add(i) } | ||||
|             return list | ||||
|         } | ||||
| 
 | ||||
|         override fun toString(value: Int): String { | ||||
|             if (value == MATCH_PARENT) return "match parent" | ||||
|             if (value == WRAP_CONTENT) return "wrap content" | ||||
|             return super.toString(value) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Height : Option<Int>("Height") { | ||||
|         override fun get(view: CameraView) = view.layoutParams.height | ||||
| 
 | ||||
|         override fun set(view: CameraView, value: Int) { | ||||
|             view.layoutParams.height = value | ||||
|             view.layoutParams = view.layoutParams | ||||
|         } | ||||
| 
 | ||||
|         override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> { | ||||
|             val root = view.parent as View | ||||
|             val boundary = root.height.takeIf { it > 0 } ?: 1000 | ||||
|             val step = boundary / 10 | ||||
|             val list = mutableListOf(WRAP_CONTENT, MATCH_PARENT) | ||||
|             for (i in step until boundary step step) { list.add(i) } | ||||
|             return list | ||||
|         } | ||||
| 
 | ||||
|         override fun toString(value: Int): String { | ||||
|             if (value == MATCH_PARENT) return "match parent" | ||||
|             if (value == WRAP_CONTENT) return "wrap content" | ||||
|             return super.toString(value) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     abstract class Control<T: com.otaliastudios.cameraview.controls.Control>(private val kclass: KClass<T>, name: String) : Option<T>(name) { | ||||
|         override fun set(view: CameraView, value: T) = view.set(value) | ||||
|         override fun get(view: CameraView) = view.get(kclass.java) | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) | ||||
|                 = options.getSupportedControls(kclass.java) | ||||
|     } | ||||
| 
 | ||||
|     class Mode : Control<com.otaliastudios.cameraview.controls.Mode>(com.otaliastudios.cameraview.controls.Mode::class, "Mode") | ||||
| 
 | ||||
|     class Engine : Control<com.otaliastudios.cameraview.controls.Engine>(com.otaliastudios.cameraview.controls.Engine::class, "Engine") { | ||||
|         override fun set(view: CameraView, value: com.otaliastudios.cameraview.controls.Engine) { | ||||
|             if (view.isOpened) { | ||||
|                 view.addCameraListener(object : CameraListener() { | ||||
|                     override fun onCameraClosed() { | ||||
|                         super.onCameraClosed() | ||||
|                         view.removeCameraListener(this) | ||||
|                         view.engine = value | ||||
|                         view.open() | ||||
|                     } | ||||
|                 }) | ||||
|                 view.close() | ||||
|             } else { | ||||
|                 view.engine = value | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Preview : Control<com.otaliastudios.cameraview.controls.Preview>(com.otaliastudios.cameraview.controls.Preview::class, "Preview Surface") { | ||||
|         override fun set(view: CameraView, value: com.otaliastudios.cameraview.controls.Preview) { | ||||
|             if (view.isOpened) { | ||||
|                 view.addCameraListener(object : CameraListener() { | ||||
|                     override fun onCameraClosed() { | ||||
|                         super.onCameraClosed() | ||||
|                         view.removeCameraListener(this) | ||||
|                         applyPreview(view, value) | ||||
|                         view.open() | ||||
|                     } | ||||
|                 }) | ||||
|                 view.close() | ||||
|             } else { | ||||
|                 applyPreview(view, value) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // This is really tricky since the preview can only be changed when not attached to window. | ||||
|         private fun applyPreview(view: CameraView, value: com.otaliastudios.cameraview.controls.Preview) { | ||||
|             val params = view.layoutParams | ||||
|             val parent = view.parent as ViewGroup | ||||
|             val index = (0 until parent.childCount).first { parent.getChildAt(it) === view } | ||||
|             parent.removeView(view) | ||||
|             view.preview = value | ||||
|             parent.addView(view, index, params) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Flash : Control<com.otaliastudios.cameraview.controls.Flash>(com.otaliastudios.cameraview.controls.Flash::class, "Flash") | ||||
| 
 | ||||
|     class WhiteBalance : Control<com.otaliastudios.cameraview.controls.WhiteBalance>(com.otaliastudios.cameraview.controls.WhiteBalance::class, "White Balance") | ||||
| 
 | ||||
|     class Hdr : Control<com.otaliastudios.cameraview.controls.Hdr>(com.otaliastudios.cameraview.controls.Hdr::class, "HDR") | ||||
| 
 | ||||
|     class PictureMetering : Option<Boolean>("Picture Metering") { | ||||
|         override fun get(view: CameraView) = view.pictureMetering | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false) | ||||
|         override fun set(view: CameraView, value: Boolean) { view.pictureMetering = value } | ||||
|     } | ||||
| 
 | ||||
|     class PictureSnapshotMetering : Option<Boolean>("Picture Snapshot Metering") { | ||||
|         override fun get(view: CameraView) = view.pictureSnapshotMetering | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false) | ||||
|         override fun set(view: CameraView, value: Boolean) { view.pictureSnapshotMetering = value } | ||||
|     } | ||||
| 
 | ||||
|     class VideoCodec : Control<com.otaliastudios.cameraview.controls.VideoCodec>(com.otaliastudios.cameraview.controls.VideoCodec::class, "Video Codec") | ||||
| 
 | ||||
|     class AudioCodec : Control<com.otaliastudios.cameraview.controls.AudioCodec>(com.otaliastudios.cameraview.controls.AudioCodec::class, "Audio Codec") | ||||
| 
 | ||||
|     class Audio : Control<com.otaliastudios.cameraview.controls.Audio>(com.otaliastudios.cameraview.controls.Audio::class, "Audio") | ||||
| 
 | ||||
|     abstract class Gesture(val gesture: com.otaliastudios.cameraview.gesture.Gesture, name: String) : Option<GestureAction>(name) { | ||||
|         override fun set(view: CameraView, value: GestureAction) { | ||||
|             view.mapGesture(gesture, value) | ||||
|         } | ||||
| 
 | ||||
|         override fun get(view: CameraView): GestureAction { | ||||
|             return view.getGestureAction(gesture) | ||||
|         } | ||||
| 
 | ||||
|         override fun getAll(view: CameraView, options: CameraOptions): Collection<GestureAction> { | ||||
|             return GestureAction.values().filter { | ||||
|                 gesture.isAssignableTo(it) && options.supports(it) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Pinch : Gesture(com.otaliastudios.cameraview.gesture.Gesture.PINCH, "Pinch") | ||||
| 
 | ||||
|     class HorizontalScroll : Gesture(com.otaliastudios.cameraview.gesture.Gesture.SCROLL_HORIZONTAL, "Horizontal Scroll") | ||||
| 
 | ||||
|     class VerticalScroll : Gesture(com.otaliastudios.cameraview.gesture.Gesture.SCROLL_VERTICAL, "Vertical Scroll") | ||||
| 
 | ||||
|     class Tap : Gesture(com.otaliastudios.cameraview.gesture.Gesture.TAP, "Tap") | ||||
| 
 | ||||
|     class LongTap : Gesture(com.otaliastudios.cameraview.gesture.Gesture.LONG_TAP, "Long Tap") | ||||
| 
 | ||||
|     abstract class Overlay(private val overlay: View, private val target: com.otaliastudios.cameraview.overlay.Overlay.Target, name: String) : Option<Boolean>(name) { | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false) | ||||
|         override fun get(view: CameraView): Boolean { | ||||
|             val params = overlay.layoutParams as OverlayLayout.LayoutParams | ||||
|             return when (target) { | ||||
|                 com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW -> params.drawOnPreview | ||||
|                 com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT -> params.drawOnPictureSnapshot | ||||
|                 com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT -> params.drawOnVideoSnapshot | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         override fun set(view: CameraView, value: Boolean) { | ||||
|             val params = overlay.layoutParams as OverlayLayout.LayoutParams | ||||
|             when (target) { | ||||
|                 com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW -> params.drawOnPreview = value | ||||
|                 com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT -> params.drawOnPictureSnapshot = value | ||||
|                 com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT -> params.drawOnVideoSnapshot = value | ||||
|             } | ||||
|             overlay.layoutParams = params | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class OverlayInPreview(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW, "Overlay in Preview") | ||||
| 
 | ||||
|     class OverlayInPictureSnapshot(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT, "Overlay in Picture Snapshot") | ||||
| 
 | ||||
|     class OverlayInVideoSnapshot(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT, "Overlay in Video Snapshot") | ||||
| 
 | ||||
|     class Grid : Control<com.otaliastudios.cameraview.controls.Grid>(com.otaliastudios.cameraview.controls.Grid::class, "Grid Lines") | ||||
| 
 | ||||
|     class GridColor : Option<Pair<Int, String>>("Grid Color") { | ||||
|         private val all = listOf( | ||||
|                 Color.argb(160, 255, 255, 255) to "default", | ||||
|                 Color.WHITE to "white", | ||||
|                 Color.BLACK to "black", | ||||
|                 Color.YELLOW to "yellow" | ||||
|         ) | ||||
| 
 | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) = all | ||||
| 
 | ||||
|         override fun get(view: CameraView) = all.first { it.first == view.gridColor } | ||||
| 
 | ||||
|         override fun set(view: CameraView, value: Pair<Int, String>) { | ||||
|             view.gridColor = value.first | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class UseDeviceOrientation : Option<Boolean>("Use Device Orientation") { | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false) | ||||
|         override fun get(view: CameraView) = view.useDeviceOrientation | ||||
|         override fun set(view: CameraView, value: Boolean) { | ||||
|             view.useDeviceOrientation = value | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class PreviewFrameRate : Option<Int>("Preview FPS") { | ||||
|         override fun get(view: CameraView) = view.previewFrameRate.roundToInt() | ||||
| 
 | ||||
|         override fun set(view: CameraView, value: Int) { | ||||
|             view.previewFrameRate = value.toFloat() | ||||
|         } | ||||
| 
 | ||||
|         override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> { | ||||
|             val min = options.previewFrameRateMinValue | ||||
|             val max = options.previewFrameRateMaxValue | ||||
|             val delta = max - min | ||||
|             return when { | ||||
|                 min == 0F && max == 0F -> listOf() | ||||
|                 delta < 0.005F -> listOf(min.roundToInt()) | ||||
|                 else -> { | ||||
|                     val results = mutableListOf<Int>() | ||||
|                     var value = min | ||||
|                     for (i in 0 until 3) { | ||||
|                         results.add(value.roundToInt()) | ||||
|                         value += delta / 3 | ||||
|                     } | ||||
|                     results | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class PictureFormat : Control<com.otaliastudios.cameraview.controls.PictureFormat>(com.otaliastudios.cameraview.controls.PictureFormat::class, "Picture Format") | ||||
| 
 | ||||
|     class FrameProcessingFormat : Option<Int>("Frame Processing Format") { | ||||
|         override fun set(view: CameraView, value: Int) { | ||||
|             view.frameProcessingFormat = value | ||||
|         } | ||||
| 
 | ||||
|         override fun get(view: CameraView) = view.frameProcessingFormat | ||||
| 
 | ||||
|         override fun getAll(view: CameraView, options: CameraOptions) | ||||
|                 = options.supportedFrameProcessingFormats | ||||
| 
 | ||||
|         override fun toString(value: Int): String { | ||||
|             return when (value) { | ||||
|                 ImageFormat.NV21 -> "NV21" | ||||
|                 ImageFormat.NV16 -> "NV16" | ||||
|                 ImageFormat.JPEG -> "JPEG" | ||||
|                 ImageFormat.YUY2 -> "YUY2" | ||||
|                 ImageFormat.YUV_420_888 -> "YUV_420_888" | ||||
|                 ImageFormat.YUV_422_888 -> "YUV_422_888" | ||||
|                 ImageFormat.YUV_444_888 -> "YUV_444_888" | ||||
|                 ImageFormat.RAW10 -> "RAW10" | ||||
|                 ImageFormat.RAW12 -> "RAW12" | ||||
|                 ImageFormat.RAW_SENSOR -> "RAW_SENSOR" | ||||
|                 else -> super.toString(value) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,76 @@ | ||||
| package com.otaliastudios.cameraview.demo | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.util.Log | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.* | ||||
| import com.otaliastudios.cameraview.CameraOptions | ||||
| import com.otaliastudios.cameraview.CameraView | ||||
| 
 | ||||
| class OptionView<Value: Any>(context: Context) : LinearLayout(context), AdapterView.OnItemSelectedListener { | ||||
| 
 | ||||
|     interface Callback { | ||||
|         fun <T: Any> onValueChanged(option: Option<T>, value: T, name: String): Boolean | ||||
|     } | ||||
| 
 | ||||
|     private lateinit var option: Option<Value> | ||||
|     private lateinit var callback: Callback | ||||
|     private lateinit var value: Value | ||||
|     private lateinit var values: List<Value> | ||||
|     private lateinit var valuesStrings: List<String> | ||||
| 
 | ||||
|     init { | ||||
|         orientation = VERTICAL | ||||
|         View.inflate(context, R.layout.option_view, this) | ||||
|     } | ||||
| 
 | ||||
|     val spinner = Spinner(context, Spinner.MODE_DROPDOWN).also { | ||||
|         val content = findViewById<ViewGroup>(R.id.content) | ||||
|         content.addView(it) | ||||
|     } | ||||
| 
 | ||||
|     fun setHasDivider(hasDivider: Boolean) { | ||||
|         val divider = findViewById<View>(R.id.divider) | ||||
|         divider.visibility = if (hasDivider) View.VISIBLE else View.GONE | ||||
|     } | ||||
| 
 | ||||
|     fun setOption(option: Option<Value>, callback: Callback) { | ||||
|         this.option = option | ||||
|         this.callback = callback | ||||
|         val title = findViewById<TextView>(R.id.title) | ||||
|         title.text = option.name | ||||
|     } | ||||
| 
 | ||||
|     fun onCameraOpened(view: CameraView, options: CameraOptions) { | ||||
|         values = option.getAll(view, options).toList() | ||||
|         value = option.get(view) | ||||
|         valuesStrings = values.map { option.toString(it) } | ||||
|         if (values.isEmpty()) { | ||||
|             spinner.onItemSelectedListener = null | ||||
|             spinner.isEnabled = false | ||||
|             spinner.alpha = 0.8f | ||||
|             spinner.adapter = ArrayAdapter(context, R.layout.spinner_text, arrayOf("Not supported.")) | ||||
|             spinner.setSelection(0, false) | ||||
|         } else { | ||||
|             spinner.isEnabled = true | ||||
|             spinner.alpha = 1f | ||||
|             spinner.adapter = ArrayAdapter(context, R.layout.spinner_text, valuesStrings) | ||||
|             spinner.setSelection(values.indexOf(value), false) | ||||
|             spinner.onItemSelectedListener = this | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) { | ||||
|         if (values[i] != value) { | ||||
|             Log.e("ControlView", "curr: " + value + " new: " + values[i]) | ||||
|             if (!callback.onValueChanged(option, values[i], valuesStrings[i])) { | ||||
|                 spinner.setSelection(values.indexOf(value)) // Go back. | ||||
|             } else { | ||||
|                 value = values[i] | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onNothingSelected(adapterView: AdapterView<*>?) {} | ||||
| } | ||||
| @ -0,0 +1,105 @@ | ||||
| package com.otaliastudios.cameraview.demo | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.graphics.BitmapFactory | ||||
| import android.graphics.Color | ||||
| import android.graphics.drawable.ColorDrawable | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import android.widget.ImageView | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.content.FileProvider | ||||
| import com.otaliastudios.cameraview.CameraUtils | ||||
| import com.otaliastudios.cameraview.PictureResult | ||||
| import com.otaliastudios.cameraview.controls.PictureFormat | ||||
| import com.otaliastudios.cameraview.size.AspectRatio | ||||
| import java.io.File | ||||
| 
 | ||||
| class PicturePreviewActivity : AppCompatActivity() { | ||||
| 
 | ||||
|     companion object { | ||||
|         var pictureResult: PictureResult? = null | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         setContentView(R.layout.activity_picture_preview) | ||||
|         val result = pictureResult ?: run { | ||||
|             finish() | ||||
|             return | ||||
|         } | ||||
|         val imageView = findViewById<ImageView>(R.id.image) | ||||
|         val captureResolution = findViewById<MessageView>(R.id.nativeCaptureResolution) | ||||
|         val captureLatency = findViewById<MessageView>(R.id.captureLatency) | ||||
|         val exifRotation = findViewById<MessageView>(R.id.exifRotation) | ||||
| 
 | ||||
|         val delay = intent.getLongExtra("delay", 0) | ||||
|         val ratio = AspectRatio.of(result.size) | ||||
|         captureLatency.setTitleAndMessage("Approx. latency", "$delay milliseconds") | ||||
|         captureResolution.setTitleAndMessage("Resolution", "${result.size} ($ratio)") | ||||
|         exifRotation.setTitleAndMessage("EXIF rotation", result.rotation.toString()) | ||||
| 
 | ||||
|         try { | ||||
|             result.toBitmap(1000, 1000) { bitmap -> imageView.setImageBitmap(bitmap) } | ||||
|         } catch (e: UnsupportedOperationException) { | ||||
|             imageView.setImageDrawable(ColorDrawable(Color.GREEN)) | ||||
|             Toast.makeText(this, "Can't preview this format: " + result.getFormat(), Toast.LENGTH_LONG).show() | ||||
|         } | ||||
|         if (result.isSnapshot) { | ||||
|             // Log the real size for debugging reason. | ||||
|             val options = BitmapFactory.Options() | ||||
|             options.inJustDecodeBounds = true | ||||
|             BitmapFactory.decodeByteArray(result.data, 0, result.data.size, options) | ||||
|             if (result.rotation % 180 != 0) { | ||||
|                 Log.e("PicturePreview", "The picture full size is ${result.size.height}x${result.size.width}") | ||||
|             } else { | ||||
|                 Log.e("PicturePreview", "The picture full size is ${result.size.width}x${result.size.height}") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         if (!isChangingConfigurations) { | ||||
|             pictureResult = null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { | ||||
|         menuInflater.inflate(R.menu.share, menu) | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         if (item.itemId == R.id.share) { | ||||
|             Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show() | ||||
|             val extension = when (pictureResult!!.format) { | ||||
|                 PictureFormat.JPEG -> "jpg" | ||||
|                 PictureFormat.DNG -> "dng" | ||||
|                 else -> throw RuntimeException("Unknown format.") | ||||
|             } | ||||
|             val file = File(filesDir, "picture.$extension") | ||||
|             CameraUtils.writeToFile(pictureResult!!.data, file) { file -> | ||||
|                 if (file != null) { | ||||
|                     val context = this@PicturePreviewActivity | ||||
|                     val intent = Intent(Intent.ACTION_SEND) | ||||
|                     intent.type = "image/*" | ||||
|                     val uri = FileProvider.getUriForFile(context, | ||||
|                             context.packageName + ".provider", file) | ||||
|                     intent.putExtra(Intent.EXTRA_STREAM, uri) | ||||
|                     intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|                     startActivity(intent) | ||||
|                 } else { | ||||
|                     Toast.makeText(this@PicturePreviewActivity, | ||||
|                             "Error while writing file.", | ||||
|                             Toast.LENGTH_SHORT).show() | ||||
|                 } | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,107 @@ | ||||
| package com.otaliastudios.cameraview.demo | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import android.widget.MediaController | ||||
| import android.widget.Toast | ||||
| import android.widget.VideoView | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.content.FileProvider | ||||
| import com.otaliastudios.cameraview.VideoResult | ||||
| import com.otaliastudios.cameraview.size.AspectRatio | ||||
| 
 | ||||
| class VideoPreviewActivity : AppCompatActivity() { | ||||
|     companion object { | ||||
|         var videoResult: VideoResult? = null | ||||
|     } | ||||
| 
 | ||||
|     private val videoView: VideoView by lazy { findViewById<VideoView>(R.id.video) } | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         setContentView(R.layout.activity_video_preview) | ||||
|         val result = videoResult ?: run { | ||||
|             finish() | ||||
|             return | ||||
|         } | ||||
|         videoView.setOnClickListener { playVideo() } | ||||
|         val actualResolution = findViewById<MessageView>(R.id.actualResolution) | ||||
|         val isSnapshot = findViewById<MessageView>(R.id.isSnapshot) | ||||
|         val rotation = findViewById<MessageView>(R.id.rotation) | ||||
|         val audio = findViewById<MessageView>(R.id.audio) | ||||
|         val audioBitRate = findViewById<MessageView>(R.id.audioBitRate) | ||||
|         val videoCodec = findViewById<MessageView>(R.id.videoCodec) | ||||
|         val audioCodec = findViewById<MessageView>(R.id.audioCodec) | ||||
|         val videoBitRate = findViewById<MessageView>(R.id.videoBitRate) | ||||
|         val videoFrameRate = findViewById<MessageView>(R.id.videoFrameRate) | ||||
| 
 | ||||
|         val ratio = AspectRatio.of(result.size) | ||||
|         actualResolution.setTitleAndMessage("Size", "${result.size} ($ratio)") | ||||
|         isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot.toString()) | ||||
|         rotation.setTitleAndMessage("Rotation", result.rotation.toString()) | ||||
|         audio.setTitleAndMessage("Audio", result.audio.name) | ||||
|         audioBitRate.setTitleAndMessage("Audio bit rate", "${result.audioBitRate} bits per sec.") | ||||
|         videoCodec.setTitleAndMessage("VideoCodec", result.videoCodec.name) | ||||
|         audioCodec.setTitleAndMessage("AudioCodec", result.audioCodec.name) | ||||
|         videoBitRate.setTitleAndMessage("Video bit rate", "${result.videoBitRate} bits per sec.") | ||||
|         videoFrameRate.setTitleAndMessage("Video frame rate", "${result.videoFrameRate} fps") | ||||
| 
 | ||||
|         val controller = MediaController(this) | ||||
|         controller.setAnchorView(videoView) | ||||
|         controller.setMediaPlayer(videoView) | ||||
|         videoView.setMediaController(controller) | ||||
|         videoView.setVideoURI(Uri.fromFile(result.file)) | ||||
|         videoView.setOnPreparedListener { mp -> | ||||
|             val lp = videoView.layoutParams | ||||
|             val videoWidth = mp.videoWidth.toFloat() | ||||
|             val videoHeight = mp.videoHeight.toFloat() | ||||
|             val viewWidth = videoView.width.toFloat() | ||||
|             lp.height = (viewWidth * (videoHeight / videoWidth)).toInt() | ||||
|             videoView.layoutParams = lp | ||||
|             playVideo() | ||||
|             if (result.isSnapshot) { | ||||
|                 // Log the real size for debugging reason. | ||||
|                 Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun playVideo() { | ||||
|         if (!videoView.isPlaying) { | ||||
|             videoView.start() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         if (!isChangingConfigurations) { | ||||
|             videoResult = null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { | ||||
|         menuInflater.inflate(R.menu.share, menu) | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         if (item.itemId == R.id.share) { | ||||
|             Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show() | ||||
|             val intent = Intent(Intent.ACTION_SEND) | ||||
|             intent.type = "video/*" | ||||
|             val uri = FileProvider.getUriForFile(this, | ||||
|                     this.packageName + ".provider", | ||||
|                     videoResult!!.file) | ||||
|             intent.putExtra(Intent.EXTRA_STREAM, uri) | ||||
|             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|             startActivity(intent) | ||||
|             return true | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,248 +0,0 @@ | ||||
| GEM | ||||
|   remote: https://rubygems.org/ | ||||
|   specs: | ||||
|     activesupport (4.2.10) | ||||
|       i18n (~> 0.7) | ||||
|       minitest (~> 5.1) | ||||
|       thread_safe (~> 0.3, >= 0.3.4) | ||||
|       tzinfo (~> 1.1) | ||||
|     addressable (2.5.2) | ||||
|       public_suffix (>= 2.0.2, < 4.0) | ||||
|     coffee-script (2.4.1) | ||||
|       coffee-script-source | ||||
|       execjs | ||||
|     coffee-script-source (1.11.1) | ||||
|     colorator (1.1.0) | ||||
|     commonmarker (0.17.13) | ||||
|       ruby-enum (~> 0.5) | ||||
|     concurrent-ruby (1.1.4) | ||||
|     dnsruby (1.61.2) | ||||
|       addressable (~> 2.5) | ||||
|     em-websocket (0.5.1) | ||||
|       eventmachine (>= 0.12.9) | ||||
|       http_parser.rb (~> 0.6.0) | ||||
|     ethon (0.11.0) | ||||
|       ffi (>= 1.3.0) | ||||
|     eventmachine (1.2.7) | ||||
|     execjs (2.7.0) | ||||
|     faraday (0.15.4) | ||||
|       multipart-post (>= 1.2, < 3) | ||||
|     ffi (1.9.25) | ||||
|     forwardable-extended (2.6.0) | ||||
|     gemoji (3.0.0) | ||||
|     github-pages (193) | ||||
|       activesupport (= 4.2.10) | ||||
|       github-pages-health-check (= 1.8.1) | ||||
|       jekyll (= 3.7.4) | ||||
|       jekyll-avatar (= 0.6.0) | ||||
|       jekyll-coffeescript (= 1.1.1) | ||||
|       jekyll-commonmark-ghpages (= 0.1.5) | ||||
|       jekyll-default-layout (= 0.1.4) | ||||
|       jekyll-feed (= 0.11.0) | ||||
|       jekyll-gist (= 1.5.0) | ||||
|       jekyll-github-metadata (= 2.9.4) | ||||
|       jekyll-mentions (= 1.4.1) | ||||
|       jekyll-optional-front-matter (= 0.3.0) | ||||
|       jekyll-paginate (= 1.1.0) | ||||
|       jekyll-readme-index (= 0.2.0) | ||||
|       jekyll-redirect-from (= 0.14.0) | ||||
|       jekyll-relative-links (= 0.5.3) | ||||
|       jekyll-remote-theme (= 0.3.1) | ||||
|       jekyll-sass-converter (= 1.5.2) | ||||
|       jekyll-seo-tag (= 2.5.0) | ||||
|       jekyll-sitemap (= 1.2.0) | ||||
|       jekyll-swiss (= 0.4.0) | ||||
|       jekyll-theme-architect (= 0.1.1) | ||||
|       jekyll-theme-cayman (= 0.1.1) | ||||
|       jekyll-theme-dinky (= 0.1.1) | ||||
|       jekyll-theme-hacker (= 0.1.1) | ||||
|       jekyll-theme-leap-day (= 0.1.1) | ||||
|       jekyll-theme-merlot (= 0.1.1) | ||||
|       jekyll-theme-midnight (= 0.1.1) | ||||
|       jekyll-theme-minimal (= 0.1.1) | ||||
|       jekyll-theme-modernist (= 0.1.1) | ||||
|       jekyll-theme-primer (= 0.5.3) | ||||
|       jekyll-theme-slate (= 0.1.1) | ||||
|       jekyll-theme-tactile (= 0.1.1) | ||||
|       jekyll-theme-time-machine (= 0.1.1) | ||||
|       jekyll-titles-from-headings (= 0.5.1) | ||||
|       jemoji (= 0.10.1) | ||||
|       kramdown (= 1.17.0) | ||||
|       liquid (= 4.0.0) | ||||
|       listen (= 3.1.5) | ||||
|       mercenary (~> 0.3) | ||||
|       minima (= 2.5.0) | ||||
|       nokogiri (>= 1.8.2, < 2.0) | ||||
|       rouge (= 2.2.1) | ||||
|       terminal-table (~> 1.4) | ||||
|     github-pages-health-check (1.8.1) | ||||
|       addressable (~> 2.3) | ||||
|       dnsruby (~> 1.60) | ||||
|       octokit (~> 4.0) | ||||
|       public_suffix (~> 2.0) | ||||
|       typhoeus (~> 1.3) | ||||
|     html-pipeline (2.9.1) | ||||
|       activesupport (>= 2) | ||||
|       nokogiri (>= 1.4) | ||||
|     http_parser.rb (0.6.0) | ||||
|     i18n (0.9.5) | ||||
|       concurrent-ruby (~> 1.0) | ||||
|     jekyll (3.7.4) | ||||
|       addressable (~> 2.4) | ||||
|       colorator (~> 1.0) | ||||
|       em-websocket (~> 0.5) | ||||
|       i18n (~> 0.7) | ||||
|       jekyll-sass-converter (~> 1.0) | ||||
|       jekyll-watch (~> 2.0) | ||||
|       kramdown (~> 1.14) | ||||
|       liquid (~> 4.0) | ||||
|       mercenary (~> 0.3.3) | ||||
|       pathutil (~> 0.9) | ||||
|       rouge (>= 1.7, < 4) | ||||
|       safe_yaml (~> 1.0) | ||||
|     jekyll-avatar (0.6.0) | ||||
|       jekyll (~> 3.0) | ||||
|     jekyll-coffeescript (1.1.1) | ||||
|       coffee-script (~> 2.2) | ||||
|       coffee-script-source (~> 1.11.1) | ||||
|     jekyll-commonmark (1.2.0) | ||||
|       commonmarker (~> 0.14) | ||||
|       jekyll (>= 3.0, < 4.0) | ||||
|     jekyll-commonmark-ghpages (0.1.5) | ||||
|       commonmarker (~> 0.17.6) | ||||
|       jekyll-commonmark (~> 1) | ||||
|       rouge (~> 2) | ||||
|     jekyll-default-layout (0.1.4) | ||||
|       jekyll (~> 3.0) | ||||
|     jekyll-feed (0.11.0) | ||||
|       jekyll (~> 3.3) | ||||
|     jekyll-gist (1.5.0) | ||||
|       octokit (~> 4.2) | ||||
|     jekyll-github-metadata (2.9.4) | ||||
|       jekyll (~> 3.1) | ||||
|       octokit (~> 4.0, != 4.4.0) | ||||
|     jekyll-mentions (1.4.1) | ||||
|       html-pipeline (~> 2.3) | ||||
|       jekyll (~> 3.0) | ||||
|     jekyll-optional-front-matter (0.3.0) | ||||
|       jekyll (~> 3.0) | ||||
|     jekyll-paginate (1.1.0) | ||||
|     jekyll-readme-index (0.2.0) | ||||
|       jekyll (~> 3.0) | ||||
|     jekyll-redirect-from (0.14.0) | ||||
|       jekyll (~> 3.3) | ||||
|     jekyll-relative-links (0.5.3) | ||||
|       jekyll (~> 3.3) | ||||
|     jekyll-remote-theme (0.3.1) | ||||
|       jekyll (~> 3.5) | ||||
|       rubyzip (>= 1.2.1, < 3.0) | ||||
|     jekyll-sass-converter (1.5.2) | ||||
|       sass (~> 3.4) | ||||
|     jekyll-seo-tag (2.5.0) | ||||
|       jekyll (~> 3.3) | ||||
|     jekyll-sitemap (1.2.0) | ||||
|       jekyll (~> 3.3) | ||||
|     jekyll-swiss (0.4.0) | ||||
|     jekyll-theme-architect (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-cayman (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-dinky (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-hacker (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-leap-day (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-merlot (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-midnight (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-minimal (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-modernist (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-primer (0.5.3) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-github-metadata (~> 2.9) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-slate (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-tactile (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-theme-time-machine (0.1.1) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-seo-tag (~> 2.0) | ||||
|     jekyll-titles-from-headings (0.5.1) | ||||
|       jekyll (~> 3.3) | ||||
|     jekyll-watch (2.1.2) | ||||
|       listen (~> 3.0) | ||||
|     jemoji (0.10.1) | ||||
|       gemoji (~> 3.0) | ||||
|       html-pipeline (~> 2.2) | ||||
|       jekyll (~> 3.0) | ||||
|     kramdown (1.17.0) | ||||
|     liquid (4.0.0) | ||||
|     listen (3.1.5) | ||||
|       rb-fsevent (~> 0.9, >= 0.9.4) | ||||
|       rb-inotify (~> 0.9, >= 0.9.7) | ||||
|       ruby_dep (~> 1.2) | ||||
|     mercenary (0.3.6) | ||||
|     mini_portile2 (2.4.0) | ||||
|     minima (2.5.0) | ||||
|       jekyll (~> 3.5) | ||||
|       jekyll-feed (~> 0.9) | ||||
|       jekyll-seo-tag (~> 2.1) | ||||
|     minitest (5.11.3) | ||||
|     multipart-post (2.0.0) | ||||
|     nokogiri (1.10.8) | ||||
|       mini_portile2 (~> 2.4.0) | ||||
|     octokit (4.13.0) | ||||
|       sawyer (~> 0.8.0, >= 0.5.3) | ||||
|     pathutil (0.16.2) | ||||
|       forwardable-extended (~> 2.6) | ||||
|     public_suffix (2.0.5) | ||||
|     rb-fsevent (0.10.3) | ||||
|     rb-inotify (0.10.0) | ||||
|       ffi (~> 1.0) | ||||
|     rouge (2.2.1) | ||||
|     ruby-enum (0.7.2) | ||||
|       i18n | ||||
|     ruby_dep (1.5.0) | ||||
|     rubyzip (2.0.0) | ||||
|     safe_yaml (1.0.4) | ||||
|     sass (3.7.2) | ||||
|       sass-listen (~> 4.0.0) | ||||
|     sass-listen (4.0.0) | ||||
|       rb-fsevent (~> 0.9, >= 0.9.4) | ||||
|       rb-inotify (~> 0.9, >= 0.9.7) | ||||
|     sawyer (0.8.1) | ||||
|       addressable (>= 2.3.5, < 2.6) | ||||
|       faraday (~> 0.8, < 1.0) | ||||
|     terminal-table (1.8.0) | ||||
|       unicode-display_width (~> 1.1, >= 1.1.1) | ||||
|     thread_safe (0.3.6) | ||||
|     typhoeus (1.3.1) | ||||
|       ethon (>= 0.9.0) | ||||
|     tzinfo (1.2.5) | ||||
|       thread_safe (~> 0.1) | ||||
|     unicode-display_width (1.4.0) | ||||
| 
 | ||||
| PLATFORMS | ||||
|   ruby | ||||
| 
 | ||||
| DEPENDENCIES | ||||
|   github-pages | ||||
| 
 | ||||
| BUNDLED WITH | ||||
|    1.17.2 | ||||
					Loading…
					
					
				
		Reference in new issue