Refactoring. Bug fixing. New logic for sizing. Some docs. Correct orientation in EXIF headers

pull/1/head
Mattia Iavarone 7 years ago
parent b99cc8f746
commit bd36fd73ea
  1. 12
      camerakit/build.gradle
  2. 17
      camerakit/src/main/AndroidManifest.xml
  3. 305
      camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java
  4. 14
      camerakit/src/main/api21/com/flurgle/camerakit/Camera2.java
  5. 7
      camerakit/src/main/base/com/flurgle/camerakit/CameraImpl.java
  6. 69
      camerakit/src/main/base/com/flurgle/camerakit/PreviewImpl.java
  7. 29
      camerakit/src/main/base/com/flurgle/camerakit/TextureViewPreview.java
  8. 29
      camerakit/src/main/java/com/flurgle/camerakit/CameraKit.java
  9. 198
      camerakit/src/main/java/com/flurgle/camerakit/CameraView.java
  10. 5
      camerakit/src/main/types/com/flurgle/camerakit/Permissions.java
  11. 2
      camerakit/src/main/utils/com/flurgle/camerakit/CommonAspectRatioFilter.java
  12. 68
      camerakit/src/main/utils/com/flurgle/camerakit/DisplayOrientationDetector.java
  13. 81
      camerakit/src/main/utils/com/flurgle/camerakit/OrientationHelper.java

@ -7,14 +7,14 @@ ext {
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionName "0.1"
}
buildTypes {
release {
@ -40,5 +40,5 @@ dependencies {
annotationProcessor 'android.arch.lifecycle:compiler:1.0.0-alpha1'
}
apply from: 'https://raw.githubusercontent.com/blundell/release-android-library/master/android-release-aar.gradle'
// apply from: 'https://raw.githubusercontent.com/blundell/release-android-library/master/android-release-aar.gradle'

@ -2,19 +2,24 @@
package="com.flurgle.camerakit">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.camera" />
<!-- Have developers add this. We don't want AUDIO permission to be auto-added to
apps that just want to take pictures. -->
<!-- uses-permission android:name="android.permission.RECORD_AUDIO" /-->
<uses-feature
android:name="android.hardware.camera"
android:required="true"/>
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.front"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
android:required="false"/>
<application />
<application/>
</manifest>

@ -5,7 +5,6 @@ import android.graphics.YuvImage;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.support.annotation.Nullable;
@ -16,11 +15,8 @@ import android.view.View;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import static com.flurgle.camerakit.CameraKit.Constants.FLASH_OFF;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_CONTINUOUS;
@ -30,7 +26,7 @@ import static com.flurgle.camerakit.CameraKit.Constants.METHOD_STANDARD;
import static com.flurgle.camerakit.CameraKit.Constants.METHOD_STILL;
@SuppressWarnings("deprecation")
public class Camera1 extends CameraImpl {
class Camera1 extends CameraImpl {
private static final String TAG = Camera1.class.getSimpleName();
@ -50,35 +46,26 @@ public class Camera1 extends CameraImpl {
private Camera.AutoFocusCallback mAutofocusCallback;
private boolean capturingImage = false;
private int mDisplayOrientation;
private int mDisplayOffset;
private int mDeviceOrientation;
@Facing
private int mFacing;
@Flash
private int mFlash;
@Focus
private int mFocus;
@Method
private int mMethod;
@Zoom
private int mZoom;
@VideoQuality
private int mVideoQuality;
@Facing private int mFacing;
@Flash private int mFlash;
@Focus private int mFocus;
@Method private int mMethod;
@Zoom private int mZoom;
@VideoQuality private int mVideoQuality;
private Handler mHandler = new Handler();
Camera1(CameraListener callback, PreviewImpl preview) {
super(callback, preview);
preview.setCallback(new PreviewImpl.Callback() {
preview.setCallback(new PreviewImpl.OnSurfaceChangedCallback() {
@Override
public void onSurfaceChanged() {
if (mCamera != null) {
setupPreview();
collectCameraSizes();
adjustCameraParameters();
}
}
@ -105,9 +92,32 @@ public class Camera1 extends CameraImpl {
releaseCamera();
}
/**
* Sets the output stream for our preview.
* To be called when preview is ready.
*/
private void setupPreview() {
try {
if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay(mPreview.getSurfaceHolder());
} else {
mCamera.setPreviewTexture(mPreview.getSurfaceTexture());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
void onDisplayOffset(int displayOrientation) {
// I doubt this will ever change.
this.mDisplayOffset = displayOrientation;
}
@Override
void setDisplayOrientation(int displayOrientation) {
this.mDisplayOrientation = displayOrientation;
void onDeviceOrientation(int deviceOrientation) {
this.mDeviceOrientation = deviceOrientation;
}
@Override
@ -217,19 +227,18 @@ public class Camera1 extends CameraImpl {
case METHOD_STANDARD:
// Null check required for camera here as is briefly null when View is detached
if (!capturingImage && mCamera != null) {
// Set boolean to wait for image callback
capturingImage = true;
Camera.Parameters parameters = mCamera.getParameters();
parameters.setRotation(computeExifOrientation());
mCamera.setParameters(parameters);
mCamera.takePicture(null, null, null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
mCameraListener.onPictureTaken(data);
// Reset capturing state to allow photos to be taken
capturingImage = false;
camera.startPreview();
}
});
@ -240,6 +249,7 @@ public class Camera1 extends CameraImpl {
break;
case METHOD_STILL:
// TODO: will calculateCaptureRotation work here? It's the only usage. Test...
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
@ -270,79 +280,14 @@ public class Camera1 extends CameraImpl {
mCameraListener.onVideoTaken(mVideoFile);
}
// Code from SandriosCamera library
// https://github.com/sandrios/sandriosCamera/blob/master/sandriosCamera/src/main/java/com/sandrios/sandriosCamera/internal/utils/CameraHelper.java#L218
public static Size getSizeWithClosestRatio(List<Size> sizes, int width, int height)
{
if (sizes == null) return null;
double MIN_TOLERANCE = 100;
double targetRatio = (double) height / width;
Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
int targetHeight = height;
for (Size size : sizes) {
if (size.getWidth() == width && size.getHeight() == height)
return size;
double ratio = (double) size.getHeight() / size.getWidth();
if (Math.abs(ratio - targetRatio) < MIN_TOLERANCE) MIN_TOLERANCE = ratio;
else continue;
if (Math.abs(size.getHeight() - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.getHeight() - targetHeight);
}
}
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.getHeight() - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.getHeight() - targetHeight);
}
}
}
return optimalSize;
}
List<Size> sizesFromList(List<Camera.Size> sizes) {
if (sizes == null) return null;
List<Size> result = new ArrayList<>(sizes.size());
for (Camera.Size size : sizes) {
result.add(new Size(size.width, size.height));
}
return result;
}
// Code from SandriosCamera library
// https://github.com/sandrios/sandriosCamera/blob/master/sandriosCamera/src/main/java/com/sandrios/sandriosCamera/internal/manager/impl/Camera1Manager.java#L212
void initResolutions() {
List<Size> previewSizes = sizesFromList(mCameraParameters.getSupportedPreviewSizes());
List<Size> videoSizes = (Build.VERSION.SDK_INT > 10) ? sizesFromList(mCameraParameters.getSupportedVideoSizes()) : previewSizes;
CamcorderProfile camcorderProfile = getCamcorderProfile(mVideoQuality);
mCaptureSize = getSizeWithClosestRatio(
(videoSizes == null || videoSizes.isEmpty()) ? previewSizes : videoSizes,
camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
mPreviewSize = getSizeWithClosestRatio(previewSizes, mCaptureSize.getWidth(), mCaptureSize.getHeight());
}
@Override
Size getCaptureResolution() {
Size getCaptureSize() {
return mCaptureSize;
}
@Override
Size getPreviewResolution() {
Size getPreviewSize() {
return mPreviewSize;
}
@ -368,24 +313,12 @@ public class Camera1 extends CameraImpl {
mCameraParameters = mCamera.getParameters();
collectCameraProperties();
collectCameraSizes();
adjustCameraParameters();
mCamera.setDisplayOrientation(calculatePreviewRotation());
mCamera.setDisplayOrientation(computeCameraToDisplayOffset());
mCameraListener.onCameraOpened();
}
private void setupPreview() {
try {
if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay(mPreview.getSurfaceHolder());
} else {
mCamera.setPreviewTexture(mPreview.getSurfaceTexture());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void releaseCamera() {
if (mCamera != null) {
mCamera.release();
@ -397,48 +330,75 @@ public class Camera1 extends CameraImpl {
}
}
private int calculatePreviewRotation() {
/**
* Returns how much should the sensor image be rotated before being shown.
* It is meant to be fed to Camera.setDisplayOrientation().
*/
private int computeCameraToDisplayOffset() {
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return ((mCameraInfo.orientation - mDisplayOrientation) + 360 + 180) % 360;
// or: (360 - ((info.orientation + displayOrientation) % 360)) % 360;
return ((mCameraInfo.orientation - mDisplayOffset) + 360 + 180) % 360;
} else {
return (mCameraInfo.orientation - mDisplayOrientation + 360) % 360;
return (mCameraInfo.orientation - mDisplayOffset + 360) % 360;
}
}
/**
* Returns the orientation to be set as a exif tag. This is already managed by
* the camera APIs as long as you call {@link Camera.Parameters#setRotation(int)}.
*/
private int computeExifOrientation() {
return (mDeviceOrientation + mCameraInfo.orientation) % 360;
}
private int calculateCaptureRotation() {
int previewRotation = calculatePreviewRotation();
int previewRotation = computeCameraToDisplayOffset();
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
//Front is flipped
return (previewRotation + 180 + 2*mDisplayOrientation + 720) %360;
return (previewRotation + 180 + 2* mDisplayOffset + 720) % 360;
} else {
return previewRotation;
}
}
private void adjustCameraParameters() {
initResolutions();
boolean invertPreviewSizes = mDisplayOrientation%180 != 0;
mPreview.setTruePreviewSize(
invertPreviewSizes? getPreviewResolution().getHeight() : getPreviewResolution().getWidth(),
invertPreviewSizes? getPreviewResolution().getWidth() : getPreviewResolution().getHeight()
);
/**
* This is called either on cameraView.start(), or when the underlying surface changes.
* It is possible that in the first call the preview surface has not already computed its
* dimensions.
* But when it does, the {@link PreviewImpl.OnSurfaceChangedCallback} should be called,
* and this should be refreshed.
*
*
* TODO: either camera or video.
*/
private void collectCameraSizes() {
List<Size> previewSizes = sizesFromList(mCameraParameters.getSupportedPreviewSizes());
List<Size> captureSizes = sizesFromList(mCameraParameters.getSupportedPictureSizes());
mCameraParameters.setPreviewSize(
getPreviewResolution().getWidth(),
getPreviewResolution().getHeight()
);
/* video stuff: CamcorderProfile camcorderProfile = getCamcorderProfile(mVideoQuality);
mCaptureSize = getSizeWithClosestRatio(
(videoSizes == null || videoSizes.isEmpty()) ? previewSizes : videoSizes,
camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight); */
Size surfaceSize = mPreview.getSurfaceSize();
mCaptureSize = Collections.max(captureSizes);
mPreviewSize = computePreviewSize(previewSizes, mCaptureSize, surfaceSize);
}
mCameraParameters.setPictureSize(
getCaptureResolution().getWidth(),
getCaptureResolution().getHeight()
);
int rotation = calculateCaptureRotation();
mCameraParameters.setRotation(rotation);
private void adjustCameraParameters() {
boolean invertPreviewSizes = mDisplayOffset % 180 != 0;
mPreview.setDesiredSize(
invertPreviewSizes? getPreviewSize().getHeight() : getPreviewSize().getWidth(),
invertPreviewSizes? getPreviewSize().getWidth() : getPreviewSize().getHeight()
);
mCameraParameters.setPreviewSize(getPreviewSize().getWidth(), getPreviewSize().getHeight());
mCameraParameters.setPictureSize(getCaptureSize().getWidth(), getCaptureSize().getHeight());
// int rotation = calculateCaptureRotation();
// mCameraParameters.setRotation(rotation);
setFocus(mFocus);
setFlash(mFlash);
mCamera.setParameters(mCameraParameters);
}
@ -447,28 +407,6 @@ public class Camera1 extends CameraImpl {
mCameraParameters.getHorizontalViewAngle());
}
private TreeSet<AspectRatio> findCommonAspectRatios(List<Camera.Size> previewSizes, List<Camera.Size> captureSizes) {
Set<AspectRatio> previewAspectRatios = new HashSet<>();
for (Camera.Size size : previewSizes) {
if (size.width >= CameraKit.Internal.screenHeight && size.height >= CameraKit.Internal.screenWidth) {
previewAspectRatios.add(AspectRatio.of(size.width, size.height));
}
}
Set<AspectRatio> captureAspectRatios = new HashSet<>();
for (Camera.Size size : captureSizes) {
captureAspectRatios.add(AspectRatio.of(size.width, size.height));
}
TreeSet<AspectRatio> output = new TreeSet<>();
for (AspectRatio aspectRatio : previewAspectRatios) {
if (captureAspectRatios.contains(aspectRatio)) {
output.add(aspectRatio);
}
}
return output;
}
private void initMediaRecorder() {
mMediaRecorder = new MediaRecorder();
@ -483,7 +421,7 @@ public class Camera1 extends CameraImpl {
mVideoFile = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video.mp4");
mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath());
mMediaRecorder.setOrientationHint(calculatePreviewRotation());
mMediaRecorder.setOrientationHint(computeCameraToDisplayOffset());
mMediaRecorder.setVideoSize(mCaptureSize.getWidth(), mCaptureSize.getHeight());
}
@ -644,7 +582,7 @@ public class Camera1 extends CameraImpl {
if (camera != null) {
camera.cancelAutoFocus();
Camera.Parameters params = camera.getParameters();
if (params.getFocusMode() != Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) {
if (!params.getFocusMode().equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
params.setFocusAreas(null);
params.setMeteringAreas(null);
@ -684,4 +622,53 @@ public class Camera1 extends CameraImpl {
}
}
// Size static stuff.
/**
* Returns a list of {@link Size} out of Camera.Sizes.
*/
private static List<Size> sizesFromList(List<Camera.Size> sizes) {
if (sizes == null) return null;
List<Size> result = new ArrayList<>(sizes.size());
for (Camera.Size size : sizes) {
result.add(new Size(size.width, size.height));
}
return result;
}
/**
* Policy here is to return a size that is big enough to fit the surface size,
* and possibly consistent with the capture size.
* @param sizes list of possible sizes
* @param captureSize size representing desired aspect ratio
* @param surfaceSize size representing the current surface size. We'd like the returned value to be bigger.
* @return chosen size
*/
private static Size computePreviewSize(List<Size> sizes, Size captureSize, Size surfaceSize) {
if (sizes == null) return null;
List<Size> consistent = new ArrayList<>(5);
List<Size> bigEnoughAndConsistent = new ArrayList<>(5);
final AspectRatio targetRatio = AspectRatio.of(captureSize.getWidth(), captureSize.getHeight());
final Size targetSize = captureSize;
for (Size size : sizes) {
AspectRatio ratio = AspectRatio.of(size.getWidth(), size.getHeight());
if (ratio.equals(targetRatio)) {
consistent.add(size);
if (size.getHeight() >= targetSize.getHeight() && size.getWidth() >= targetSize.getWidth()) {
bigEnoughAndConsistent.add(size);
}
}
}
if (bigEnoughAndConsistent.size() > 0) return Collections.min(bigEnoughAndConsistent);
if (consistent.size() > 0) return Collections.max(consistent);
return Collections.max(sizes);
}
}

@ -3,7 +3,6 @@ package com.flurgle.camerakit;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.PointF;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
@ -36,7 +35,7 @@ class Camera2 extends CameraImpl {
Camera2(CameraListener callback, PreviewImpl preview, Context context) {
super(callback, preview);
preview.setCallback(new PreviewImpl.Callback() {
preview.setCallback(new PreviewImpl.OnSurfaceChangedCallback() {
@Override
public void onSurfaceChanged() {
@ -89,7 +88,12 @@ class Camera2 extends CameraImpl {
}
@Override
void setDisplayOrientation(int displayOrientation) {
void onDisplayOffset(int displayOrientation) {
}
@Override
void onDeviceOrientation(int deviceOrientation) {
}
@ -176,7 +180,7 @@ class Camera2 extends CameraImpl {
}
@Override
Size getCaptureResolution() {
Size getCaptureSize() {
if (mCaptureSize == null && mCameraCharacteristics != null) {
TreeSet<Size> sizes = new TreeSet<>();
sizes.addAll(getAvailableCaptureResolutions());
@ -202,7 +206,7 @@ class Camera2 extends CameraImpl {
}
@Override
Size getPreviewResolution() {
Size getPreviewSize() {
if (mPreviewSize == null && mCameraCharacteristics != null) {
TreeSet<Size> sizes = new TreeSet<>();
sizes.addAll(getAvailablePreviewResolutions());

@ -15,7 +15,8 @@ abstract class CameraImpl {
abstract void start();
abstract void stop();
abstract void setDisplayOrientation(int displayOrientation);
abstract void onDisplayOffset(int displayOrientation);
abstract void onDeviceOrientation(int deviceOrientation);
abstract void setFacing(@Facing int facing);
abstract void setFlash(@Flash int flash);
@ -28,8 +29,8 @@ abstract class CameraImpl {
abstract void startVideo();
abstract void endVideo();
abstract Size getCaptureResolution();
abstract Size getPreviewResolution();
abstract Size getCaptureSize();
abstract Size getPreviewSize();
abstract boolean isCameraOpened();
@Nullable

@ -7,20 +7,22 @@ import android.view.View;
abstract class PreviewImpl {
interface Callback {
interface OnSurfaceChangedCallback {
void onSurfaceChanged();
}
private Callback mCallback;
private OnSurfaceChangedCallback mOnSurfaceChangedCallback;
private int mWidth;
private int mHeight;
// As far as I can see, these are the view/surface dimensions.
private int mSurfaceWidth;
private int mSurfaceHeight;
protected int mTrueWidth;
protected int mTrueHeight;
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
private int mDesiredWidth;
private int mDesiredHeight;
void setCallback(Callback callback) {
mCallback = callback;
void setCallback(OnSurfaceChangedCallback callback) {
mOnSurfaceChangedCallback = callback;
}
abstract Surface getSurface();
@ -29,12 +31,13 @@ abstract class PreviewImpl {
abstract Class getOutputClass();
abstract void setDisplayOrientation(int displayOrientation);
protected void onDisplayOffset(int displayOrientation) {}
protected void onDeviceOrientation(int deviceOrientation) {}
abstract boolean isReady();
protected void dispatchSurfaceChanged() {
mCallback.onSurfaceChanged();
mOnSurfaceChangedCallback.onSurfaceChanged();
}
SurfaceHolder getSurfaceHolder() {
@ -45,30 +48,37 @@ abstract class PreviewImpl {
return null;
}
void setSize(int width, int height) {
mWidth = width;
mHeight = height;
// Refresh true preview size to adjust scaling
setTruePreviewSize(mTrueWidth, mTrueHeight);
// As far as I can see, these are the view/surface dimensions.
// This is called by subclasses.
protected void setSurfaceSize(int width, int height) {
this.mSurfaceWidth = width;
this.mSurfaceHeight = height;
refreshScale(); // Refresh true preview size to adjust scaling
}
int getWidth() {
return mWidth;
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl.
void setDesiredSize(int width, int height) {
this.mDesiredWidth = width;
this.mDesiredHeight = height;
refreshScale();
}
int getHeight() {
return mHeight;
Size getSurfaceSize() {
return new Size(mSurfaceWidth, mSurfaceHeight);
}
void setTruePreviewSize(final int width, final int height) {
this.mTrueWidth = width;
this.mTrueHeight = height;
/**
* As far as I can see, this extends either width or height of the surface,
* to match the desired aspect ratio.
* This means that the external part of the surface will be cropped by the outer view.
*/
protected void refreshScale() {
getView().post(new Runnable() {
@Override
public void run() {
if (width != 0 && height != 0) {
AspectRatio aspectRatio = AspectRatio.of(width, height);
if (mDesiredWidth != 0 && mDesiredHeight != 0) {
AspectRatio aspectRatio = AspectRatio.of(mDesiredWidth, mDesiredHeight);
int targetHeight = (int) (getView().getWidth() * aspectRatio.toFloat());
float scaleY;
if (getView().getHeight() > 0) {
@ -88,13 +98,4 @@ abstract class PreviewImpl {
}
});
}
int getTrueWidth() {
return mTrueWidth;
}
int getTrueHeight() {
return mTrueHeight;
}
}

@ -13,8 +13,6 @@ class TextureViewPreview extends PreviewImpl {
private final TextureView mTextureView;
private int mDisplayOrientation;
TextureViewPreview(Context context, ViewGroup parent) {
final View view = View.inflate(context, R.layout.texture_view, parent);
mTextureView = (TextureView) view.findViewById(R.id.texture_view);
@ -22,20 +20,20 @@ class TextureViewPreview extends PreviewImpl {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
setSize(width, height);
setSurfaceSize(width, height);
dispatchSurfaceChanged();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
setSize(width, height);
setSurfaceSize(width, height);
dispatchSurfaceChanged();
setTruePreviewSize(mTrueWidth, mTrueHeight);
refreshScale();
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
setSize(0, 0);
setSurfaceSize(0, 0);
return true;
}
@ -45,11 +43,6 @@ class TextureViewPreview extends PreviewImpl {
});
}
@Override
void setCallback(Callback callback) {
super.setCallback(callback);
}
@Override
Surface getSurface() {
return new Surface(mTextureView.getSurfaceTexture());
@ -65,21 +58,11 @@ class TextureViewPreview extends PreviewImpl {
return SurfaceTexture.class;
}
@Override
void setDisplayOrientation(int displayOrientation) {
mDisplayOrientation = displayOrientation;
}
@Override
boolean isReady() {
return mTextureView.getSurfaceTexture() != null;
}
@Override
protected void dispatchSurfaceChanged() {
super.dispatchSurfaceChanged();
}
@Override
SurfaceTexture getSurfaceTexture() {
return mTextureView.getSurfaceTexture();
@ -87,8 +70,8 @@ class TextureViewPreview extends PreviewImpl {
@TargetApi(15)
@Override
void setTruePreviewSize(int width, int height) {
super.setTruePreviewSize(width, height);
void setDesiredSize(int width, int height) {
super.setDesiredSize(width, height);
if (mTextureView.getSurfaceTexture() != null) {
mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height);
}

@ -4,16 +4,9 @@ import android.content.res.Resources;
public class CameraKit {
static class Internal {
static final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
static final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
}
public static class Constants {
public static final int PERMISSION_REQUEST_CAMERA = 16;
public static final int PERMISSION_REQUEST_CODE = 16;
public static final int FACING_BACK = 0;
public static final int FACING_FRONT = 1;
@ -35,9 +28,21 @@ public class CameraKit {
public static final int METHOD_STILL = 1;
public static final int METHOD_SPEED = 2;
public static final int PERMISSIONS_STRICT = 0;
public static final int PERMISSIONS_LAZY = 1;
public static final int PERMISSIONS_PICTURE = 2;
/**
* This means the app will be requesting Camera and Audio permissions on Marshmallow+.
* With this configuration, it is mandatory that the developer adds
* <uses-permission android:name="android.permission.RECORD_AUDIO" />
* to its manifest, or the app will crash.
*/
public static final int PERMISSIONS_VIDEO = 0;
/**
* This means the app will be requesting the Camera permissions on Marshmallow+.
* On older APIs, this is not important, since the Camera permission is already
* declared in the manifest by the library.
*/
public static final int PERMISSIONS_PICTURE = 1;
public static final int VIDEO_QUALITY_480P = 0;
public static final int VIDEO_QUALITY_720P = 1;
@ -56,7 +61,7 @@ public class CameraKit {
static final int DEFAULT_FOCUS = Constants.FOCUS_CONTINUOUS;
static final int DEFAULT_ZOOM = Constants.ZOOM_OFF;
static final int DEFAULT_METHOD = Constants.METHOD_STANDARD;
static final int DEFAULT_PERMISSIONS = Constants.PERMISSIONS_STRICT;
static final int DEFAULT_PERMISSIONS = Constants.PERMISSIONS_PICTURE;
static final int DEFAULT_VIDEO_QUALITY = Constants.VIDEO_QUALITY_480P;
static final int DEFAULT_JPEG_QUALITY = 100;

@ -8,6 +8,7 @@ import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.OnLifecycleEvent;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Rect;
@ -18,12 +19,12 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.hardware.display.DisplayManagerCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import java.io.ByteArrayOutputStream;
@ -38,9 +39,8 @@ import static com.flurgle.camerakit.CameraKit.Constants.FLASH_OFF;
import static com.flurgle.camerakit.CameraKit.Constants.FLASH_ON;
import static com.flurgle.camerakit.CameraKit.Constants.FLASH_TORCH;
import static com.flurgle.camerakit.CameraKit.Constants.METHOD_STANDARD;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_LAZY;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_PICTURE;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_STRICT;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_VIDEO;
/**
* The CameraView implements the LifecycleObserver interface for ease of use. To take advantage of
@ -58,46 +58,37 @@ import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_STRICT;
*/
public class CameraView extends FrameLayout implements LifecycleObserver {
private final static String TAG = CameraView.class.getSimpleName();
private static Handler sWorkerHandler;
static {
// Initialize a single worker thread. This can be static since only a single camera
// reference can exist at a time.
private static Handler getWorkerHandler() {
if (sWorkerHandler == null) {
HandlerThread workerThread = new HandlerThread("CameraViewWorker");
workerThread.setDaemon(true);
workerThread.start();
sWorkerHandler = new Handler(workerThread.getLooper());
}
return sWorkerHandler;
}
@Facing
private int mFacing;
@Flash
private int mFlash;
@Focus
private int mFocus;
@Method
private int mMethod;
@Zoom
private int mZoom;
@Permissions
private int mPermissions;
private void run(Runnable runnable) {
getWorkerHandler().post(runnable);
}
@VideoQuality
private int mVideoQuality;
@Facing private int mFacing;
@Flash private int mFlash;
@Focus private int mFocus;
@Method private int mMethod;
@Zoom private int mZoom;
@Permissions private int mPermissions;
@VideoQuality private int mVideoQuality;
private int mJpegQuality;
private boolean mCropOutput;
private boolean mAdjustViewBounds;
private CameraListenerMiddleWare mCameraListener;
private DisplayOrientationDetector mDisplayOrientationDetector;
private OrientationHelper mOrientationHelper;
private CameraImpl mCameraImpl;
private PreviewImpl mPreviewImpl;
private Lifecycle mLifecycle;
@ -108,7 +99,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
init(context, null);
}
@SuppressWarnings("all")
public CameraView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
@ -117,11 +107,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@SuppressWarnings("WrongConstant")
private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CameraView,
0, 0);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0);
try {
mFacing = a.getInteger(R.styleable.CameraView_ckFacing, CameraKit.Defaults.DEFAULT_FACING);
mFlash = a.getInteger(R.styleable.CameraView_ckFlash, CameraKit.Defaults.DEFAULT_FLASH);
@ -149,15 +135,21 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setFocus(mFocus);
setMethod(mMethod);
setZoom(mZoom);
setPermissions(mPermissions);
setPermissionPolicy(mPermissions);
setVideoQuality(mVideoQuality);
if (!isInEditMode()) {
mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
mOrientationHelper = new OrientationHelper(context) {
@Override
public void onDisplayOffsetChanged(int displayOffset) {
mCameraImpl.onDisplayOffset(displayOffset);
mPreviewImpl.onDisplayOffset(displayOffset);
}
@Override
public void onDisplayOrientationChanged(int displayOrientation) {
mCameraImpl.setDisplayOrientation(displayOrientation);
mPreviewImpl.setDisplayOrientation(displayOrientation);
protected void onDeviceOrientationChanged(int deviceOrientation) {
mCameraImpl.onDeviceOrientation(deviceOrientation);
mPreviewImpl.onDeviceOrientation(deviceOrientation);
}
};
@ -167,7 +159,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
public boolean onTouch(View v, MotionEvent motionEvent) {
int action = motionEvent.getAction();
if (motionEvent.getAction() == MotionEvent.ACTION_UP && mFocus == CameraKit.Constants.FOCUS_TAP_WITH_MARKER) {
if (action == MotionEvent.ACTION_UP && mFocus == CameraKit.Constants.FOCUS_TAP_WITH_MARKER) {
focusMarkerLayout.focus(motionEvent.getX(), motionEvent.getY());
}
@ -182,29 +174,48 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.e(TAG, "onAttachedToWindow");
if (!isInEditMode()) {
mDisplayOrientationDetector.enable(
ViewCompat.isAttachedToWindow(this)
? DisplayManagerCompat.getInstance(getContext())
.getDisplay(Display.DEFAULT_DISPLAY)
: null
);
Log.e(TAG, "onAttachedToWindow: enabling orientation detector.");
WindowManager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
mOrientationHelper.enable(display);
}
}
@Override
protected void onDetachedFromWindow() {
Log.e(TAG, "onDetachedFromWindow");
if (!isInEditMode()) {
mDisplayOrientationDetector.disable();
Log.e(TAG, "onDetachedFromWindow: disabling orientation detector.");
mOrientationHelper.disable();
}
super.onDetachedFromWindow();
}
/**
* If {@link CameraView#mAdjustViewBounds} was set AND one of the dimensions is set to WRAP_CONTENT,
* CameraView will adjust that dimensions to fit the preview aspect ratio as returned by
* {@link #getPreviewSize()}.
*
* If this is not true, the surface will adapt to the dimension specified in the layout file.
* Having fixed dimensions means that, very likely, what the user sees is different from what
* the final picture will be. This is also due to what happens in {@link PreviewImpl#refreshScale()}.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mAdjustViewBounds) {
if (!mAdjustViewBounds) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
Size previewSize = getPreviewSize();
if (previewSize != null) {
if (previewSize == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
if (getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
int height = MeasureSpec.getSize(heightMeasureSpec);
float ratio = (float) height / (float) previewSize.getWidth();
@ -213,7 +224,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
heightMeasureSpec
);
return;
} else if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
int width = MeasureSpec.getSize(widthMeasureSpec);
float ratio = (float) width / (float) previewSize.getHeight();
@ -222,17 +233,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
return;
}
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public boolean isStarted() {
return mIsStarted;
}
@ -263,30 +269,29 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
stop();
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy(LifecycleOwner owner) {
mLifecycle = owner.getLifecycle();
// This might be useless, but no time to think about it now.
sWorkerHandler = null;
}
public void start() {
if (mIsStarted || !isEnabled()) {
// Already started, do nothing.
return;
}
mIsStarted = true;
checkPermissionPolicyOrThrow();
int cameraCheck = ContextCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA);
int audioCheck = ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO);
switch (mPermissions) {
case PERMISSIONS_STRICT:
case PERMISSIONS_VIDEO:
if (cameraCheck != PackageManager.PERMISSION_GRANTED || audioCheck != PackageManager.PERMISSION_GRANTED) {
requestPermissions(true, true);
return;
}
break;
case PERMISSIONS_LAZY:
if (cameraCheck != PackageManager.PERMISSION_GRANTED) {
requestPermissions(true, true);
return;
}
break;
case PERMISSIONS_PICTURE:
if (cameraCheck != PackageManager.PERMISSION_GRANTED) {
requestPermissions(true, false);
@ -295,7 +300,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
break;
}
sWorkerHandler.post(new Runnable() {
mIsStarted = true;
run(new Runnable() {
@Override
public void run() {
mCameraImpl.start();
@ -325,7 +331,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
public void setFacing(@Facing final int facing) {
this.mFacing = facing;
sWorkerHandler.post(new Runnable() {
run(new Runnable() {
@Override
public void run() {
mCameraImpl.setFacing(facing);
@ -363,8 +369,13 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mCameraImpl.setZoom(mZoom);
}
public void setPermissions(@Permissions int permissions) {
/**
* Sets permission policy.
* @param permissions desired policy, either picture or video.
*/
public void setPermissionPolicy(@Permissions int permissions) {
this.mPermissions = permissions;
checkPermissionPolicyOrThrow();
}
public void setVideoQuality(@VideoQuality int videoQuality) {
@ -431,12 +442,18 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mCameraImpl.endVideo();
}
/**
* Returns the size used for the preview,
* or null if it hasn't been computed (for example if the surface is not ready).
*/
@Nullable
public Size getPreviewSize() {
return mCameraImpl != null ? mCameraImpl.getPreviewResolution() : null;
return mCameraImpl != null ? mCameraImpl.getPreviewSize() : null;
}
@Nullable
public Size getCaptureSize() {
return mCameraImpl != null ? mCameraImpl.getCaptureResolution() : null;
return mCameraImpl != null ? mCameraImpl.getCaptureSize() : null;
}
private void requestPermissions(boolean requestCamera, boolean requestAudio) {
@ -452,12 +469,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
List<String> permissions = new ArrayList<>();
if (requestCamera) permissions.add(Manifest.permission.CAMERA);
if (requestAudio) permissions.add(Manifest.permission.RECORD_AUDIO);
if (activity != null) {
ActivityCompat.requestPermissions(
activity,
ActivityCompat.requestPermissions(activity,
permissions.toArray(new String[permissions.size()]),
CameraKit.Constants.PERMISSION_REQUEST_CAMERA);
CameraKit.Constants.PERMISSION_REQUEST_CODE);
}
}
@ -481,8 +496,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
public void onPictureTaken(byte[] jpeg) {
super.onPictureTaken(jpeg);
if (mCropOutput) {
int width = mMethod == METHOD_STANDARD ? mCameraImpl.getCaptureResolution().getWidth() : mCameraImpl.getPreviewResolution().getWidth();
int height = mMethod == METHOD_STANDARD ? mCameraImpl.getCaptureResolution().getHeight() : mCameraImpl.getPreviewResolution().getHeight();
int width = mMethod == METHOD_STANDARD ? mCameraImpl.getCaptureSize().getWidth() : mCameraImpl.getPreviewSize().getWidth();
int height = mMethod == METHOD_STANDARD ? mCameraImpl.getCaptureSize().getHeight() : mCameraImpl.getPreviewSize().getHeight();
AspectRatio outputRatio = AspectRatio.of(getWidth(), getHeight());
getCameraListener().onPictureTaken(new CenterCrop(jpeg, outputRatio, mJpegQuality).getJpeg());
} else {
@ -521,4 +536,29 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
/**
* If mPermissions == PERMISSIONS_VIDEO we will ask for RECORD_AUDIO permission.
* If the developer did not add this to its manifest, throw and fire warnings.
* (Hoping this is not cought elsewhere... we should test).
*/
private void checkPermissionPolicyOrThrow() {
if (mPermissions == PERMISSIONS_VIDEO) {
try {
PackageManager manager = getContext().getPackageManager();
PackageInfo info = manager.getPackageInfo(getContext().getPackageName(), PackageManager.GET_PERMISSIONS);
for (String requestedPermission : info.requestedPermissions) {
if (requestedPermission.equals(Manifest.permission.RECORD_AUDIO)) {
return;
}
}
String message = "When the permission policy is PERMISSION_VIDEO, the RECORD_AUDIO permission " +
"should be added to the application manifest file.";
Log.w(TAG, message);
throw new IllegalStateException(message);
} catch (PackageManager.NameNotFoundException e) {
// Not possible.
}
}
}
}

@ -5,11 +5,10 @@ import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_LAZY;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_PICTURE;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_STRICT;
import static com.flurgle.camerakit.CameraKit.Constants.PERMISSIONS_VIDEO;
@Retention(RetentionPolicy.SOURCE)
@IntDef({PERMISSIONS_STRICT, PERMISSIONS_LAZY, PERMISSIONS_PICTURE})
@IntDef({PERMISSIONS_VIDEO, PERMISSIONS_PICTURE})
public @interface Permissions {
}

@ -18,10 +18,8 @@ public class CommonAspectRatioFilter {
public TreeSet<AspectRatio> filter() {
Set<AspectRatio> previewAspectRatios = new HashSet<>();
for (Size size : mPreviewSizes) {
if (size.getWidth() >= CameraKit.Internal.screenHeight && size.getHeight() >= CameraKit.Internal.screenWidth) {
previewAspectRatios.add(AspectRatio.of(size.getWidth(), size.getHeight()));
}
}
Set<AspectRatio> captureAspectRatios = new HashSet<>();
for (Size size : mCaptureSizes) {

@ -1,68 +0,0 @@
package com.flurgle.camerakit;
import android.content.Context;
import android.util.SparseIntArray;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.Surface;
public abstract class DisplayOrientationDetector {
private final OrientationEventListener mOrientationEventListener;
static final SparseIntArray DISPLAY_ORIENTATIONS = new SparseIntArray();
static {
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0);
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90);
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180);
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270);
}
private Display mDisplay;
private int mLastKnownDisplayOrientation = 0;
public DisplayOrientationDetector(Context context) {
mOrientationEventListener = new OrientationEventListener(context) {
private int mLastKnownRotation = -1;
@Override
public void onOrientationChanged(int orientation) {
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN || mDisplay == null) {
return;
}
final int rotation = mDisplay.getRotation();
if (mLastKnownRotation != rotation) {
mLastKnownRotation = rotation;
dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(rotation));
}
}
};
}
public void enable(Display display) {
mDisplay = display;
mOrientationEventListener.enable();
dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(display.getRotation()));
}
public void disable() {
mOrientationEventListener.disable();
mDisplay = null;
}
public int getLastKnownDisplayOrientation() {
return mLastKnownDisplayOrientation;
}
void dispatchOnDisplayOrientationChanged(int displayOrientation) {
mLastKnownDisplayOrientation = displayOrientation;
onDisplayOrientationChanged(displayOrientation);
}
public abstract void onDisplayOrientationChanged(int displayOrientation);
}

@ -0,0 +1,81 @@
package com.flurgle.camerakit;
import android.content.Context;
import android.hardware.SensorManager;
import android.media.ExifInterface;
import android.util.SparseIntArray;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.Surface;
abstract class OrientationHelper {
private final OrientationEventListener mOrientationEventListener;
private static final SparseIntArray DISPLAY_ORIENTATIONS = new SparseIntArray();
static {
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0);
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90);
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180);
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270);
}
private Display mDisplay;
private int mLastKnownDisplayOffset = -1;
private int mLastOrientation = -1;
OrientationHelper(Context context) {
mOrientationEventListener = new OrientationEventListener(context, SensorManager.SENSOR_DELAY_NORMAL) {
@Override
public void onOrientationChanged(int orientation) {
int or = 0;
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
or = 0;
} else if (orientation >= 315 || orientation < 45) {
or = 0;
} else if (orientation >= 45 && orientation < 135) {
or = 90;
} else if (orientation >= 135 && orientation < 225) {
or = 180;
} else if (orientation >= 225 && orientation < 315) {
or = 270;
}
if (or != mLastOrientation) {
mLastOrientation = or;
onDeviceOrientationChanged(mLastOrientation);
}
// Let's see if display rotation has changed.. but how could it ever change...??
// This makes no sense apparently. I'll leave it for now.
if (mDisplay != null) {
final int offset = mDisplay.getRotation();
if (mLastKnownDisplayOffset != offset) {
mLastKnownDisplayOffset = offset;
onDisplayOffsetChanged(DISPLAY_ORIENTATIONS.get(offset));
}
}
}
};
}
void enable(Display display) {
mDisplay = display;
mOrientationEventListener.enable();
mLastKnownDisplayOffset = DISPLAY_ORIENTATIONS.get(display.getRotation());
onDisplayOffsetChanged(mLastKnownDisplayOffset);
}
void disable() {
mOrientationEventListener.disable();
mDisplay = null;
}
protected abstract void onDisplayOffsetChanged(int displayOffset);
protected abstract void onDeviceOrientationChanged(int deviceOrientation);
}
Loading…
Cancel
Save