Smart sizing behavior, removed adjustViewBounds

pull/1/head
Mattia Iavarone 7 years ago
parent 3dbd7fa945
commit e7330fcd7b
  1. 14
      camerakit/src/main/api16/com/flurgle/camerakit/Camera1.java
  2. 1
      camerakit/src/main/base/com/flurgle/camerakit/CameraImpl.java
  3. 52
      camerakit/src/main/base/com/flurgle/camerakit/PreviewImpl.java
  4. 1
      camerakit/src/main/base/com/flurgle/camerakit/SurfaceViewPreview.java
  5. 1
      camerakit/src/main/base/com/flurgle/camerakit/TextureViewPreview.java
  6. 187
      camerakit/src/main/java/com/flurgle/camerakit/CameraView.java
  7. 10
      demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java
  8. 13
      demo/src/main/java/com/flurgle/camerakit/demo/PicturePreviewActivity.java
  9. 50
      demo/src/main/java/com/flurgle/camerakit/demo/ResultHolder.java
  10. 7
      demo/src/main/java/com/flurgle/camerakit/demo/VideoPreviewActivity.java

@ -43,7 +43,6 @@ class Camera1 extends CameraImpl {
private MediaRecorder mMediaRecorder;
private File mVideoFile;
private Camera.AutoFocusCallback mAutofocusCallback;
private boolean isCapturingImage = false;
private int mDisplayOffset;
private int mDeviceOrientation;
@ -63,6 +62,7 @@ class Camera1 extends CameraImpl {
private Handler mFocusHandler = new Handler();
private ConstantMapper.MapperImpl mMapper = new ConstantMapper.Mapper1();
private boolean mIsSetup = false;
private boolean mIsCapturingImage = false;
private final Object mLock = new Object();
@ -91,6 +91,7 @@ class Camera1 extends CameraImpl {
Size newSize = computePreviewSize();
if (!newSize.equals(mPreviewSize)) {
mPreviewSize = newSize;
mCameraListener.onCameraPreviewSizeChanged();
synchronized (mLock) {
mCamera.stopPreview();
Camera.Parameters params = mCamera.getParameters();
@ -127,6 +128,7 @@ class Camera1 extends CameraImpl {
boolean invertPreviewSizes = shouldFlipSizes(); // mDisplayOffset % 180 != 0;
mCaptureSize = computeCaptureSize();
mPreviewSize = computePreviewSize();
mCameraListener.onCameraPreviewSizeChanged();
mPreview.setDesiredSize(
invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(),
invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight()
@ -356,12 +358,12 @@ class Camera1 extends CameraImpl {
@Override
void captureImage() {
if (isCapturingImage) return;
if (mIsCapturingImage) return;
if (!isCameraOpened()) return;
switch (mSessionType) {
case SESSION_TYPE_PICTURE:
// Set boolean to wait for image callback
isCapturingImage = true;
mIsCapturingImage = true;
synchronized (mLock) {
Camera.Parameters parameters = mCamera.getParameters();
parameters.setRotation(computeExifOrientation());
@ -372,7 +374,7 @@ class Camera1 extends CameraImpl {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
mCameraListener.onPictureTaken(data);
isCapturingImage = false;
mIsCapturingImage = false;
camera.startPreview(); // This is needed, read somewhere in the docs.
}
});
@ -382,7 +384,7 @@ class Camera1 extends CameraImpl {
// If we are in a video session, camera captures are fast captures coming
// from the preview stream.
// TODO: will this work while recording a video? test...
isCapturingImage = true;
mIsCapturingImage = true;
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
@ -402,7 +404,7 @@ class Camera1 extends CameraImpl {
byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, rotation);
YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null);
mCameraListener.processYuvImage(yuv);
isCapturingImage = false;
mIsCapturingImage = false;
}
}).start();
}

@ -1,5 +1,6 @@
package com.flurgle.camerakit;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.MotionEvent;

@ -1,12 +1,16 @@
package com.flurgle.camerakit;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
abstract class PreviewImpl {
// This is used to notify CameraImpl to recompute its camera Preview size.
// After that, CameraView will need a new layout pass to adapt to the Preview size.
interface SurfaceCallback {
void onSurfaceAvailable();
void onSurfaceChanged();
@ -23,35 +27,48 @@ abstract class PreviewImpl {
private int mDesiredWidth;
private int mDesiredHeight;
void setSurfaceCallback(SurfaceCallback callback) {
mSurfaceCallback = callback;
}
PreviewImpl(Context context, ViewGroup parent) {}
abstract Surface getSurface();
abstract View getView();
abstract Class getOutputClass();
abstract boolean isReady();
protected void onDisplayOffset(int displayOrientation) {}
protected void onDeviceOrientation(int deviceOrientation) {}
abstract boolean isReady();
SurfaceHolder getSurfaceHolder() {
return null;
}
SurfaceTexture getSurfaceTexture() {
return null;
}
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl.
// These must be alredy rotated, if needed, to be consistent with surface/view sizes.
void setDesiredSize(int width, int height) {
this.mDesiredWidth = width;
this.mDesiredHeight = height;
refreshScale();
}
final Size getSurfaceSize() {
return new Size(mSurfaceWidth, mSurfaceHeight);
}
final void setSurfaceCallback(SurfaceCallback callback) {
mSurfaceCallback = callback;
}
protected final void onSurfaceAvailable(int width, int height) {
mSurfaceWidth = width;
mSurfaceHeight = height;
refreshScale();
mSurfaceCallback.onSurfaceAvailable();
}
// As far as I can see, these are the view/surface dimensions.
// This is called by subclasses.
protected final void onSurfaceSizeChanged(int width, int height) {
@ -63,31 +80,20 @@ abstract class PreviewImpl {
}
}
protected final void onSurfaceDestroyed() {
mSurfaceWidth = 0;
mSurfaceHeight = 0;
refreshScale();
}
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl.
// These must be alredy rotated, if needed, to be consistent with surface/view sizes.
void setDesiredSize(int width, int height) {
this.mDesiredWidth = width;
this.mDesiredHeight = height;
refreshScale();
}
Size getSurfaceSize() {
return new Size(mSurfaceWidth, mSurfaceHeight);
}
/**
* 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.
*/
private void refreshScale() {
private final void refreshScale() {
getView().post(new Runnable() {
@Override
public void run() {

@ -13,6 +13,7 @@ class SurfaceViewPreview extends PreviewImpl {
private final SurfaceView mSurfaceView;
SurfaceViewPreview(Context context, ViewGroup parent) {
super(context, parent);
final View view = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT
mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
final SurfaceHolder holder = mSurfaceView.getHolder();

@ -15,6 +15,7 @@ class TextureViewPreview extends PreviewImpl {
private Surface mSurface;
TextureViewPreview(Context context, ViewGroup parent) {
super(context, parent);
final View view = View.inflate(context, R.layout.texture_view, parent); // MATCH_PARENT
mTextureView = (TextureView) view.findViewById(R.id.texture_view);
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {

@ -42,6 +42,12 @@ import static com.flurgle.camerakit.CameraKit.Constants.FLASH_TORCH;
import static com.flurgle.camerakit.CameraKit.Constants.SESSION_TYPE_PICTURE;
import static com.flurgle.camerakit.CameraKit.Constants.SESSION_TYPE_VIDEO;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
/**
* The CameraView implements the LifecycleObserver interface for ease of use. To take advantage of
* this, simply call the following from any LifecycleOwner:
@ -188,79 +194,139 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
private String ms(int mode) {
switch (mode) {
case AT_MOST: return "AT_MOST";
case EXACTLY: return "EXACTLY";
case UNSPECIFIED: return "UNSPECIFIED";
}
return null;
}
/**
* If adjustViewBounds 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()}.
* Measuring is basically controlled by layout params width and height.
* The basic semantics are:
*
* 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()}.
* - MATCH_PARENT: CameraView should completely fill this dimension, even if this might mean
* not respecting the preview aspect ratio.
* - WRAP_CONTENT: CameraView should try to adapt this dimension to respect the preview
* aspect ratio.
*
* If this is a problem, you can use {@link #setCropOutput(boolean)} set to true.
* In that case, the final image will have the same aspect ratio of the preview.
* When both dimensions are MATCH_PARENT, CameraView will fill its
* parent no matter the preview. Thanks to what happens in {@link PreviewImpl}, this acts like
* a CENTER CROP scale type.
*
* When both dimensions are WRAP_CONTENT, CameraView will take the biggest dimensions that
* fit the preview aspect ratio. This acts like a CENTER INSIDE scale type.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mAdjustViewBounds) {
Log.e(TAG, "onMeasure, adjustViewBounds=false");
Size previewSize = getPreviewSize();
if (previewSize == null) {
Log.e(TAG, "onMeasure, surface is not ready. Calling default behavior.");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
Size previewSize = getPreviewSize();
if (previewSize == null) { // Early measure.
Log.e(TAG, "onMeasure, early measure");
// Let's which dimensions need to be adapted.
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int widthValue = MeasureSpec.getSize(widthMeasureSpec);
final int heightValue = MeasureSpec.getSize(heightMeasureSpec);
final boolean flip = mCameraImpl.shouldFlipSizes();
final float previewWidth = flip ? previewSize.getHeight() : previewSize.getWidth();
final float previewHeight = flip ? previewSize.getWidth() : previewSize.getHeight();
// If MATCH_PARENT is interpreted as AT_MOST, transform to EXACTLY
// to be consistent with our semantics.
final ViewGroup.LayoutParams lp = getLayoutParams();
if (widthMode == AT_MOST && lp.width == MATCH_PARENT) widthMode = EXACTLY;
if (heightMode == AT_MOST && lp.height == MATCH_PARENT) heightMode = EXACTLY;
Log.e(TAG, "onMeasure, requested dimensions are (" +
widthValue + "[" + ms(widthMode) + "]x" +
heightValue + "[" + ms(heightMode) + "])");
Log.e(TAG, "onMeasure, previewSize is (" + previewWidth + "x" + previewHeight + ")");
// If we have fixed dimensions (either 300dp or MATCH_PARENT), there's nothing we should do,
// other than respect it. The preview will eventually be cropped at the sides (by PreviewImpl scaling)
// except the case in which these fixed dimensions somehow fit exactly the preview aspect ratio.
if (widthMode == EXACTLY && heightMode == EXACTLY) {
Log.e(TAG, "onMeasure, both are MATCH_PARENT or fixed value. We adapt. This means CROP_INSIDE. " +
"(" + widthValue + "x" + heightValue + ")");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
boolean wwc = getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT;
boolean hwc = getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT;
boolean flip = mCameraImpl.shouldFlipSizes();
float previewWidth = flip ? previewSize.getHeight() : previewSize.getWidth();
float previewHeight = flip ? previewSize.getWidth() : previewSize.getHeight();
float parentHeight = MeasureSpec.getSize(heightMeasureSpec);
float parentWidth = MeasureSpec.getSize(widthMeasureSpec); // mode = AT_MOST
Log.e(TAG, "onMeasure, parent size is "+new Size((int)parentWidth, (int)parentHeight)); // 1080x1794
Log.e(TAG, "onMeasure, surface size is "+new Size((int)previewWidth, (int)previewHeight)); // 1600x1200
if (wwc && hwc) {
// If both dimensions are WRAP_CONTENT, let's try to fit the preview size perfectly
// without cropping.
// TODO: This should actually be a flag, like scaleMode, that replaces cropOutput and adjustViewBounds.
// This is like a fitCenter.
float targetRatio = previewHeight / previewWidth;
float currentRatio = parentHeight / parentWidth;
if (currentRatio > targetRatio) {
// View is too tall. Must reduce height.
int newHeight = (int) (parentWidth * targetRatio);
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY));
// If both dimensions are free, with no limits, then our size will be exactly the
// preview size. This can happen rarely, for example in scrollable containers.
if (widthMode == UNSPECIFIED && heightMode == UNSPECIFIED) {
Log.e(TAG, "onMeasure, both are completely free. We respect that and extend to the whole preview size. " +
"(" + previewWidth + "x" + previewHeight + ")");
super.onMeasure(MeasureSpec.makeMeasureSpec((int) previewWidth, EXACTLY),
MeasureSpec.makeMeasureSpec((int) previewHeight, EXACTLY));
return;
}
// It sure now that at least one dimension can be determined (either because EXACTLY or AT_MOST).
// This starts to seem a pleasant situation.
// If one of the dimension is completely free, take the other and fit the ratio.
// One of the two might be AT_MOST, but we use the value anyway.
float ratio = previewHeight / previewWidth;
if (widthMode == UNSPECIFIED || heightMode == UNSPECIFIED) {
boolean freeWidth = widthMode == UNSPECIFIED;
int height, width;
if (freeWidth) {
height = heightValue;
width = (int) (height / ratio);
} else {
// View is too wide. Must reduce width.
int newWidth = (int) (parentHeight / targetRatio);
super.onMeasure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
width = widthValue;
height = (int) (width * ratio);
}
Log.e(TAG, "onMeasure, one dimension was free, we adapted it to fit the aspect ratio. " +
"(" + width + "x" + height + ")");
super.onMeasure(MeasureSpec.makeMeasureSpec(width, EXACTLY),
MeasureSpec.makeMeasureSpec(height, EXACTLY));
return;
}
} else if (wwc) {
// Legacy behavior, with just a WC dimension. This is dangerous because the final size
// might be bigger than the available size, resulting in part of the surface getting cropped.
// Think for example of a 4:3 preview in a 16:9 screen, with width=MP and height=WC.
// This is like a cropCenter.
float height = MeasureSpec.getSize(heightMeasureSpec);
float ratio = height / previewWidth;
int width = (int) (previewHeight * ratio);
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), heightMeasureSpec);
} else if (hwc) {
float width = MeasureSpec.getSize(widthMeasureSpec);
float ratio = width / previewHeight;
int height = (int) (previewWidth * ratio);
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
// At this point both dimensions are either AT_MOST-AT_MOST, EXACTLY-AT_MOST or AT_MOST-EXACTLY.
// Let's manage this sanely. If only one is EXACTLY, we can TRY to fit the aspect ratio,
// but it is not guaranteed to succeed. It depends on the AT_MOST value of the other dimensions.
if (widthMode == EXACTLY || heightMode == EXACTLY) {
boolean freeWidth = widthMode == AT_MOST;
int height, width;
if (freeWidth) {
height = heightValue;
width = Math.min((int) (height / ratio), widthValue);
} else {
width = widthValue;
height = Math.min((int) (width * ratio), heightValue);
}
Log.e(TAG, "onMeasure, one dimension was EXACTLY, another AT_MOST. We have TRIED to fit " +
"the aspect ratio, but it's not guaranteed. (" + width + "x" + height + ")");
super.onMeasure(MeasureSpec.makeMeasureSpec(width, EXACTLY),
MeasureSpec.makeMeasureSpec(height, EXACTLY));
return;
}
// Last case, AT_MOST and AT_MOST. Here we can SURELY fit the aspect ratio by filling one
// dimension and adapting the other.
int height, width;
float atMostRatio = heightValue / widthValue;
if (atMostRatio >= ratio) {
// We must reduce height.
width = widthValue;
height = (int) (width * ratio);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
height = heightValue;
width = (int) (height / ratio);
}
Log.e(TAG, "onMeasure, both dimension were AT_MOST. We fit the preview aspect ratio. " +
"(" + width + "x" + height + ")");
super.onMeasure(MeasureSpec.makeMeasureSpec(width, EXACTLY),
MeasureSpec.makeMeasureSpec(height, EXACTLY));
}
@ -759,23 +825,29 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
public void onCameraOpened() {
super.onCameraOpened();
if (mWrappedListener == null) return;
mWrappedListener.onCameraOpened();
}
@Override
public void onCameraClosed() {
super.onCameraClosed();
if (mWrappedListener == null) return;
mWrappedListener.onCameraClosed();
}
public void onCameraPreviewSizeChanged() {
// Camera preview size, as returned by getPreviewSize(), has changed.
// Request a layout pass for onMeasure() to do its stuff.
// Potentially this will change CameraView size, which changes Surface size,
// which triggers a new Preview size. But hopefully it will converge.
requestLayout();
}
@Override
public void onPictureTaken(byte[] jpeg) {
super.onPictureTaken(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 {
@ -783,7 +855,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
}
void processYuvImage(YuvImage yuv) {
public void processYuvImage(YuvImage yuv) {
if (mWrappedListener == null) return;
if (mCropOutput) {
AspectRatio outputRatio = AspectRatio.of(getWidth(), getHeight());
@ -797,7 +869,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
public void onVideoTaken(File video) {
super.onVideoTaken(video);
mWrappedListener.onVideoTaken(video);
}

@ -121,10 +121,9 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan
mCapturing = false;
long callbackTime = System.currentTimeMillis();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length);
ResultHolder.dispose();
ResultHolder.setImage(bitmap);
ResultHolder.setTimeToCallback(callbackTime - startTime);
PicturePreviewActivity.setImage(bitmap);
Intent intent = new Intent(MainActivity.this, PicturePreviewActivity.class);
intent.putExtra("delay", callbackTime-startTime);
startActivity(intent);
}
});
@ -144,9 +143,8 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan
public void onVideoTaken(File video) {
super.onVideoTaken(video);
mCapturing = false;
ResultHolder.dispose();
ResultHolder.setVideo(Uri.fromFile(video));
Intent intent = new Intent(MainActivity.this, VideoPreviewActivity.class);
intent.putExtra("video", Uri.fromFile(video));
startActivity(intent);
}
});
@ -169,7 +167,6 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan
break;
case CameraKit.Constants.FACING_FRONT:
Toast.makeText(this, "Switched to front camera!", Toast.LENGTH_SHORT).show();
break;
}
@ -307,6 +304,7 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan
break;
case R.id.heightMatchParent:
// We are in a vertically scrolling container, match parent would not work at all.
height = parent.getHeight();
break;
}

@ -10,6 +10,8 @@ import android.widget.TextView;
import com.flurgle.camerakit.AspectRatio;
import com.flurgle.camerakit.Size;
import java.lang.ref.WeakReference;
import butterknife.BindView;
import butterknife.ButterKnife;
@ -30,13 +32,20 @@ public class PicturePreviewActivity extends Activity {
@BindView(R.id.captureLatency)
TextView captureLatency;
private static WeakReference<Bitmap> image;
public static void setImage(@Nullable Bitmap im) {
image = im != null ? new WeakReference<>(im) : null;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_picture_preview);
ButterKnife.bind(this);
Bitmap bitmap = ResultHolder.getImage();
long delay = getIntent().getLongExtra("delay", 0);
Bitmap bitmap = image == null ? null : image.get();
if (bitmap == null) {
finish();
return;
@ -51,7 +60,7 @@ public class PicturePreviewActivity extends Activity {
actualResolution.setText(bitmap.getWidth() + " x " + bitmap.getHeight());
approxUncompressedSize.setText(getApproximateFileMegabytes(bitmap) + "MB");
captureLatency.setText(ResultHolder.getTimeToCallback() + " milliseconds");
captureLatency.setText(delay + " milliseconds");
}
private static float getApproximateFileMegabytes(Bitmap bitmap) {

@ -1,50 +0,0 @@
package com.flurgle.camerakit.demo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.flurgle.camerakit.Size;
import java.lang.ref.WeakReference;
public class ResultHolder {
private static WeakReference<Bitmap> image;
private static Size nativeCaptureSize;
private static long timeToCallback;
private static Uri video;
public static Uri getVideo() {
return video;
}
public static void setVideo(Uri video) {
ResultHolder.video = video;
}
public static void setImage(@Nullable Bitmap image) {
ResultHolder.image = image != null ? new WeakReference<>(image) : null;
}
@Nullable
public static Bitmap getImage() {
return image != null ? image.get() : null;
}
public static void setTimeToCallback(long timeToCallback) {
ResultHolder.timeToCallback = timeToCallback;
}
public static long getTimeToCallback() {
return timeToCallback;
}
public static void dispose() {
setImage(null);
setVideo(null);
setTimeToCallback(0);
}
}

@ -33,12 +33,7 @@ public class VideoPreviewActivity extends Activity {
setContentView(R.layout.activity_video_preview);
ButterKnife.bind(this);
Uri videoUri = ResultHolder.getVideo();
if (videoUri == null) {
finish();
return;
}
Uri videoUri = getIntent().getParcelableExtra("video");
MediaController controller = new MediaController(this);
controller.setAnchorView(videoView);
controller.setMediaPlayer(videoView);

Loading…
Cancel
Save