diff --git a/README.md b/README.md index 13650d29..8ef8865c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - *option to pass a `File` when recording a video* - *introduced a smart measuring and sizing behavior, replacing bugged `adjustViewBounds`* - *measure `CameraView` as center crop or center inside* +- *add multiple `CameraListener`s for events* *Feel free to open issues with suggestions or contribute.* @@ -46,7 +47,7 @@ CameraKit is an easy to use utility to work with the Android Camera APIs. Everyt # Features -- Image and video capture seamlessly working with the same preview session. (TODO: remove this, use different sessions) +- Seamless image and video capturing, even within the same session - System permission handling - Dynamic sizing behavior - Create a `CameraView` of any size (not just presets!) @@ -59,12 +60,12 @@ CameraKit is an easy to use utility to work with the Android Camera APIs. Everyt - EXIF support - Automatically detected orientation tag - Plug in location tags with `CameraView.setLocation(double, double)` +- Control the camera parameters via XML or programmatically - TODO: Built-in pinch to zoom ## Setup -For now, clone the repo and add it to your project. -TODO: publish to bintray. +For now, you must clone the repo and add it to your project. ## Usage @@ -379,12 +380,12 @@ If you don't request this feature, you can use `CameraKit.hasCameras()` to detec ## Roadmap -This are things that need to be done, off the top of my head: +These are things that need to be done, off the top of my head: - [ ] fix CropOutput class presumably not working on rotated pictures - [ ] test video and 'frame' capture behavior, I expect some bugs there - [ ] simple APIs to draw grid lines -- [ ] replace setCameraListener() with addCameraListener() +- [x] replace setCameraListener() with addCameraListener() - [ ] add a `sizingMethod` API to choose the capture size? Could be `max`, `4:3`, `16:9`... Right now it's `max` - [ ] pinch to zoom support - [ ] exposure correction APIs @@ -393,5 +394,5 @@ This are things that need to be done, off the top of my head: - [ ] EXIF support for 'frame' captured pictures, using ExifInterface library, so we can stop rotating it in Java - [ ] add onRequestPermissionResults for easy permission callback - [ ] better error handling, maybe with a onError(e) method in the public listener -- [ ] better threading, for example ensure callbacks are called in the ui thread +- [x] better threading, for example ensure callbacks are called in the ui thread diff --git a/camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java b/camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java index 32966843..2dbce590 100644 --- a/camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java +++ b/camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java @@ -66,7 +66,7 @@ class Camera1 extends CameraImpl { private final Object mLock = new Object(); - Camera1(CameraView.CameraListenerWrapper callback, final PreviewImpl preview) { + Camera1(CameraView.CameraCallbacks callback, final PreviewImpl preview) { super(callback, preview); } diff --git a/camerakit/src/main/api21/com/flurgle/camerakit/Camera2.java b/camerakit/src/main/api21/com/flurgle/camerakit/Camera2.java index 0085db34..791c8d8f 100644 --- a/camerakit/src/main/api21/com/flurgle/camerakit/Camera2.java +++ b/camerakit/src/main/api21/com/flurgle/camerakit/Camera2.java @@ -47,7 +47,7 @@ class Camera2 extends CameraImpl { } - Camera2(CameraView.CameraListenerWrapper callback, PreviewImpl preview, Context context) { + Camera2(CameraView.CameraCallbacks callback, PreviewImpl preview, Context context) { super(callback, preview); mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); diff --git a/camerakit/src/main/base/com/flurgle/camerakit/CameraImpl.java b/camerakit/src/main/base/com/flurgle/camerakit/CameraImpl.java index d59fc761..4595ee92 100644 --- a/camerakit/src/main/base/com/flurgle/camerakit/CameraImpl.java +++ b/camerakit/src/main/base/com/flurgle/camerakit/CameraImpl.java @@ -1,6 +1,5 @@ package com.flurgle.camerakit; -import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.MotionEvent; @@ -9,10 +8,10 @@ import java.io.File; abstract class CameraImpl implements PreviewImpl.SurfaceCallback { - protected final CameraView.CameraListenerWrapper mCameraListener; + protected final CameraView.CameraCallbacks mCameraListener; protected final PreviewImpl mPreview; - CameraImpl(CameraView.CameraListenerWrapper callback, PreviewImpl preview) { + CameraImpl(CameraView.CameraCallbacks callback, PreviewImpl preview) { mCameraListener = callback; mPreview = preview; mPreview.setSurfaceCallback(this); diff --git a/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java b/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java index bf7c9005..0fec32cb 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java @@ -15,15 +15,14 @@ import android.graphics.Rect; import android.graphics.YuvImage; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; -import android.view.Display; import android.view.MotionEvent; -import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; @@ -95,7 +94,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @WhiteBalance private int mWhiteBalance; private int mJpegQuality; private boolean mCropOutput; - private CameraListenerWrapper mCameraListener; + private CameraCallbacks mCameraCallbacks; private OrientationHelper mOrientationHelper; private CameraImpl mCameraImpl; private PreviewImpl mPreviewImpl; @@ -128,9 +127,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mCropOutput = a.getBoolean(R.styleable.CameraView_cameraCropOutput, CameraKit.Defaults.DEFAULT_CROP_OUTPUT); a.recycle(); - mCameraListener = new CameraListenerWrapper(); + mCameraCallbacks = new CameraCallbacks(); mPreviewImpl = new TextureViewPreview(context, this); - mCameraImpl = new Camera1(mCameraListener, mPreviewImpl); + mCameraImpl = new Camera1(mCameraCallbacks, mPreviewImpl); mFocusMarkerLayout = new FocusMarkerLayout(context); addView(mFocusMarkerLayout); @@ -463,7 +462,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } public void destroy() { - mCameraListener = new CameraListenerWrapper(); // Release inner listener. + mCameraCallbacks.clearListeners(); // Release inner listener. // This might be useless, but no time to think about it now. sWorkerHandler = null; } @@ -724,18 +723,56 @@ public class CameraView extends FrameLayout implements LifecycleObserver { - - /** * Sets a {@link CameraListener} instance to be notified of all * interesting events that will happen during the camera lifecycle. * * @param cameraListener a listener for events. + * @deprecated use {@link #addCameraListener(CameraListener)} instead. */ + @Deprecated public void setCameraListener(CameraListener cameraListener) { - this.mCameraListener.wrapListener(cameraListener); + mCameraCallbacks.clearListeners(); + if (cameraListener != null) { + mCameraCallbacks.addListener(cameraListener); + } + } + + + /** + * Adds a {@link CameraListener} instance to be notified of all + * interesting events that happen during the camera lifecycle. + * + * @param cameraListener a listener for events. + */ + public void addCameraListener(CameraListener cameraListener) { + if (cameraListener != null) { + mCameraCallbacks.addListener(cameraListener); + } + } + + + /** + * Remove a {@link CameraListener} that was previously registered. + * + * @param cameraListener a listener for events. + */ + public void removeCameraListener(CameraListener cameraListener) { + if (cameraListener != null) { + mCameraCallbacks.removeListener(cameraListener); + } } + + /** + * Clears the list of {@link CameraListener} that are registered + * to camera events. + */ + public void clearCameraListeners() { + mCameraCallbacks.clearListeners(); + } + + public void captureImage() { mCameraImpl.captureImage(); } @@ -817,20 +854,38 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } } - class CameraListenerWrapper extends CameraListener { + class CameraCallbacks extends CameraListener { - private CameraListener mWrappedListener; + private ArrayList mListeners; + private Handler uiHandler; + + public CameraCallbacks() { + mListeners = new ArrayList<>(2); + uiHandler = new Handler(Looper.getMainLooper()); + } @Override public void onCameraOpened() { - if (mWrappedListener == null) return; - mWrappedListener.onCameraOpened(); + uiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onCameraOpened(); + } + } + }); } @Override public void onCameraClosed() { - if (mWrappedListener == null) return; - mWrappedListener.onCameraClosed(); + uiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onCameraClosed(); + } + } + }); } public void onCameraPreviewSizeChanged() { @@ -843,35 +898,67 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void onPictureTaken(byte[] jpeg) { - if (mWrappedListener == null) return; if (mCropOutput) { // TODO cropOutput won't work if image is rotated (e.g. byte[] contains exif orientation). AspectRatio outputRatio = AspectRatio.of(getWidth(), getHeight()); - mWrappedListener.onPictureTaken(new CenterCrop(jpeg, outputRatio, mJpegQuality).getJpeg()); - } else { - mWrappedListener.onPictureTaken(jpeg); + jpeg = new CenterCrop(jpeg, outputRatio, mJpegQuality).getJpeg(); } + + final byte[] data = jpeg; + uiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onPictureTaken(data); + } + } + }); } public void processYuvImage(YuvImage yuv) { - if (mWrappedListener == null) return; + byte[] jpeg; if (mCropOutput) { AspectRatio outputRatio = AspectRatio.of(getWidth(), getHeight()); - mWrappedListener.onPictureTaken(new CenterCrop(yuv, outputRatio, mJpegQuality).getJpeg()); + jpeg = new CenterCrop(yuv, outputRatio, mJpegQuality).getJpeg(); } else { ByteArrayOutputStream out = new ByteArrayOutputStream(); yuv.compressToJpeg(new Rect(0, 0, yuv.getWidth(), yuv.getHeight()), mJpegQuality, out); - mWrappedListener.onPictureTaken(out.toByteArray()); + jpeg = out.toByteArray(); } + + final byte[] data = jpeg; + uiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onPictureTaken(data); + } + } + }); } @Override - public void onVideoTaken(File video) { - mWrappedListener.onVideoTaken(video); + public void onVideoTaken(final File video) { + uiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onVideoTaken(video); + } + } + }); + } + + private void addListener(@NonNull CameraListener cameraListener) { + mListeners.add(cameraListener); + } + + private void removeListener(@NonNull CameraListener cameraListener) { + mListeners.remove(cameraListener); } - private void wrapListener(@Nullable CameraListener cameraListener) { - mWrappedListener = cameraListener; + private void clearListeners() { + mListeners.clear(); } } diff --git a/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java b/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java index 7b22570c..d19ad9b9 100644 --- a/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java +++ b/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java @@ -114,7 +114,8 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan if (mCapturing) return; mCapturing = true; final long startTime = System.currentTimeMillis(); - camera.setCameraListener(new CameraListener() { + camera.clearCameraListeners(); + camera.addCameraListener(new CameraListener() { @Override public void onPictureTaken(byte[] jpeg) { super.onPictureTaken(jpeg); @@ -138,7 +139,8 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan } if (mCapturing) return; mCapturing = true; - camera.setCameraListener(new CameraListener() { + camera.clearCameraListeners(); + camera.addCameraListener(new CameraListener() { @Override public void onVideoTaken(File video) { super.onVideoTaken(video);