1067 lines
34 KiB
Java
1067 lines
34 KiB
Java
|
package org.thoughtcrime.securesms.mediasend.camerax;
|
||
|
|
||
|
/*
|
||
|
* Copyright (C) 2019 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
import android.Manifest.permission;
|
||
|
import android.annotation.SuppressLint;
|
||
|
import android.content.Context;
|
||
|
import android.graphics.Matrix;
|
||
|
import android.graphics.Paint;
|
||
|
import android.graphics.Rect;
|
||
|
import android.graphics.SurfaceTexture;
|
||
|
import android.hardware.display.DisplayManager;
|
||
|
import android.hardware.display.DisplayManager.DisplayListener;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Handler;
|
||
|
import android.os.Looper;
|
||
|
import android.os.Parcelable;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.AttributeSet;
|
||
|
import android.util.Size;
|
||
|
import android.view.Display;
|
||
|
import android.view.MotionEvent;
|
||
|
import android.view.ScaleGestureDetector;
|
||
|
import android.view.Surface;
|
||
|
import android.view.TextureView;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewConfiguration;
|
||
|
import android.view.ViewGroup;
|
||
|
import android.view.animation.BaseInterpolator;
|
||
|
import android.view.animation.DecelerateInterpolator;
|
||
|
|
||
|
import androidx.annotation.Nullable;
|
||
|
import androidx.annotation.RequiresApi;
|
||
|
import androidx.annotation.RequiresPermission;
|
||
|
import androidx.annotation.RestrictTo;
|
||
|
import androidx.annotation.RestrictTo.Scope;
|
||
|
import androidx.annotation.UiThread;
|
||
|
import androidx.camera.core.CameraX.LensFacing;
|
||
|
import androidx.camera.core.FlashMode;
|
||
|
import androidx.camera.core.ImageCapture.OnImageCapturedListener;
|
||
|
import androidx.camera.core.ImageCapture.OnImageSavedListener;
|
||
|
import androidx.camera.core.ImageProxy;
|
||
|
import androidx.camera.core.VideoCapture.OnVideoSavedListener;
|
||
|
import androidx.lifecycle.LifecycleOwner;
|
||
|
|
||
|
import org.thoughtcrime.securesms.logging.Log;
|
||
|
|
||
|
import java.io.File;
|
||
|
|
||
|
/**
|
||
|
* A {@link View} that displays a preview of the camera with methods {@link
|
||
|
* #takePicture(OnImageCapturedListener)}, {@link #takePicture(File, OnImageSavedListener)}, {@link
|
||
|
* #startRecording(File, OnVideoSavedListener)} and {@link #stopRecording()}.
|
||
|
*
|
||
|
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
||
|
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
||
|
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
|
||
|
*/
|
||
|
@RequiresApi(21)
|
||
|
public final class CameraXView extends ViewGroup {
|
||
|
static final String TAG = CameraXView.class.getSimpleName();
|
||
|
static final boolean DEBUG = false;
|
||
|
|
||
|
static final int INDEFINITE_VIDEO_DURATION = -1;
|
||
|
static final int INDEFINITE_VIDEO_SIZE = -1;
|
||
|
|
||
|
private static final String EXTRA_SUPER = "super";
|
||
|
private static final String EXTRA_ZOOM_LEVEL = "zoom_level";
|
||
|
private static final String EXTRA_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled";
|
||
|
private static final String EXTRA_FLASH = "flash";
|
||
|
private static final String EXTRA_MAX_VIDEO_DURATION = "max_video_duration";
|
||
|
private static final String EXTRA_MAX_VIDEO_SIZE = "max_video_size";
|
||
|
private static final String EXTRA_SCALE_TYPE = "scale_type";
|
||
|
private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
|
||
|
private static final String EXTRA_CAPTURE_MODE = "captureMode";
|
||
|
|
||
|
private final Rect mFocusingRect = new Rect();
|
||
|
private final Rect mMeteringRect = new Rect();
|
||
|
// For tap-to-focus
|
||
|
private long mDownEventTimestamp;
|
||
|
// For pinch-to-zoom
|
||
|
private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
|
||
|
private boolean mIsPinchToZoomEnabled = true;
|
||
|
CameraXModule mCameraModule;
|
||
|
private final DisplayManager.DisplayListener mDisplayListener =
|
||
|
new DisplayListener() {
|
||
|
@Override
|
||
|
public void onDisplayAdded(int displayId) {
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDisplayRemoved(int displayId) {
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDisplayChanged(int displayId) {
|
||
|
mCameraModule.invalidateView();
|
||
|
}
|
||
|
};
|
||
|
private TextureView mCameraTextureView;
|
||
|
private Size mPreviewSrcSize = new Size(0, 0);
|
||
|
private ScaleType mScaleType = ScaleType.CENTER_CROP;
|
||
|
// For accessibility event
|
||
|
private MotionEvent mUpEvent;
|
||
|
private @Nullable
|
||
|
Paint mLayerPaint;
|
||
|
|
||
|
public CameraXView(Context context) {
|
||
|
this(context, null);
|
||
|
}
|
||
|
|
||
|
public CameraXView(Context context, AttributeSet attrs) {
|
||
|
this(context, attrs, 0);
|
||
|
}
|
||
|
|
||
|
public CameraXView(Context context, AttributeSet attrs, int defStyle) {
|
||
|
super(context, attrs, defStyle);
|
||
|
init(context, attrs);
|
||
|
}
|
||
|
|
||
|
@RequiresApi(21)
|
||
|
public CameraXView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||
|
init(context, attrs);
|
||
|
}
|
||
|
|
||
|
/** Debug logging that can be enabled. */
|
||
|
private static void log(String msg) {
|
||
|
if (DEBUG) {
|
||
|
Log.i(TAG, msg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Utility method for converting an displayRotation int into a human readable string. */
|
||
|
private static String displayRotationToString(int displayRotation) {
|
||
|
if (displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180) {
|
||
|
return "Portrait-" + (displayRotation * 90);
|
||
|
} else if (displayRotation == Surface.ROTATION_90
|
||
|
|| displayRotation == Surface.ROTATION_270) {
|
||
|
return "Landscape-" + (displayRotation * 90);
|
||
|
} else {
|
||
|
return "Unknown";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Binds control of the camera used by this view to the given lifecycle.
|
||
|
*
|
||
|
* <p>This links opening/closing the camera to the given lifecycle. The camera will not operate
|
||
|
* unless this method is called with a valid {@link LifecycleOwner} that is not in the {@link
|
||
|
* androidx.lifecycle.Lifecycle.State#DESTROYED} state. Call this method only once camera
|
||
|
* permissions have been obtained.
|
||
|
*
|
||
|
* <p>Once the provided lifecycle has transitioned to a {@link
|
||
|
* androidx.lifecycle.Lifecycle.State#DESTROYED} state, CameraView must be bound to a new
|
||
|
* lifecycle through this method in order to operate the camera.
|
||
|
*
|
||
|
* @param lifecycleOwner The lifecycle that will control this view's camera
|
||
|
* @throws IllegalArgumentException if provided lifecycle is in a {@link
|
||
|
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
|
||
|
* @throws IllegalStateException if camera permissions are not granted.
|
||
|
*/
|
||
|
@RequiresPermission(permission.CAMERA)
|
||
|
public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||
|
mCameraModule.bindToLifecycle(lifecycleOwner);
|
||
|
}
|
||
|
|
||
|
private void init(Context context, @Nullable AttributeSet attrs) {
|
||
|
addView(mCameraTextureView = new TextureView(getContext()), 0 /* view position */);
|
||
|
mCameraTextureView.setLayerPaint(mLayerPaint);
|
||
|
mCameraModule = new CameraXModule(this);
|
||
|
|
||
|
if (isInEditMode()) {
|
||
|
onPreviewSourceDimensUpdated(640, 480);
|
||
|
}
|
||
|
|
||
|
setScaleType(ScaleType.CENTER_CROP);
|
||
|
setPinchToZoomEnabled(true);
|
||
|
setCaptureMode(CaptureMode.IMAGE);
|
||
|
setCameraLensFacing(LensFacing.FRONT);
|
||
|
setFlash(FlashMode.OFF);
|
||
|
|
||
|
if (getBackground() == null) {
|
||
|
setBackgroundColor(0xFF111111);
|
||
|
}
|
||
|
|
||
|
mPinchToZoomGestureDetector = new PinchToZoomGestureDetector(context);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected LayoutParams generateDefaultLayoutParams() {
|
||
|
return new LayoutParams(
|
||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected Parcelable onSaveInstanceState() {
|
||
|
// TODO(b/113884082): Decide what belongs here or what should be invalidated on
|
||
|
// configuration
|
||
|
// change
|
||
|
Bundle state = new Bundle();
|
||
|
state.putParcelable(EXTRA_SUPER, super.onSaveInstanceState());
|
||
|
state.putInt(EXTRA_SCALE_TYPE, getScaleType().getId());
|
||
|
state.putFloat(EXTRA_ZOOM_LEVEL, getZoomLevel());
|
||
|
state.putBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED, isPinchToZoomEnabled());
|
||
|
state.putString(EXTRA_FLASH, getFlash().name());
|
||
|
state.putLong(EXTRA_MAX_VIDEO_DURATION, getMaxVideoDuration());
|
||
|
state.putLong(EXTRA_MAX_VIDEO_SIZE, getMaxVideoSize());
|
||
|
if (getCameraLensFacing() != null) {
|
||
|
state.putString(EXTRA_CAMERA_DIRECTION, getCameraLensFacing().name());
|
||
|
}
|
||
|
state.putInt(EXTRA_CAPTURE_MODE, getCaptureMode().getId());
|
||
|
return state;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onRestoreInstanceState(Parcelable savedState) {
|
||
|
// TODO(b/113884082): Decide what belongs here or what should be invalidated on
|
||
|
// configuration
|
||
|
// change
|
||
|
if (savedState instanceof Bundle) {
|
||
|
Bundle state = (Bundle) savedState;
|
||
|
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
|
||
|
setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
|
||
|
setZoomLevel(state.getFloat(EXTRA_ZOOM_LEVEL));
|
||
|
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
|
||
|
setFlash(FlashMode.valueOf(state.getString(EXTRA_FLASH)));
|
||
|
setMaxVideoDuration(state.getLong(EXTRA_MAX_VIDEO_DURATION));
|
||
|
setMaxVideoSize(state.getLong(EXTRA_MAX_VIDEO_SIZE));
|
||
|
String lensFacingString = state.getString(EXTRA_CAMERA_DIRECTION);
|
||
|
setCameraLensFacing(
|
||
|
TextUtils.isEmpty(lensFacingString)
|
||
|
? null
|
||
|
: LensFacing.valueOf(lensFacingString));
|
||
|
setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
|
||
|
} else {
|
||
|
super.onRestoreInstanceState(savedState);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the paint on the preview.
|
||
|
*
|
||
|
* <p>This only affects the preview, and does not affect captured images/video.
|
||
|
*
|
||
|
* @param paint The paint object to apply to the preview.
|
||
|
* @hide This may not work once {@link android.view.SurfaceView} is supported along with {@link
|
||
|
* TextureView}.
|
||
|
*/
|
||
|
@Override
|
||
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||
|
public void setLayerPaint(@Nullable Paint paint) {
|
||
|
super.setLayerPaint(paint);
|
||
|
mLayerPaint = paint;
|
||
|
mCameraTextureView.setLayerPaint(paint);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onAttachedToWindow() {
|
||
|
super.onAttachedToWindow();
|
||
|
DisplayManager dpyMgr =
|
||
|
(DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
|
||
|
dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onDetachedFromWindow() {
|
||
|
super.onDetachedFromWindow();
|
||
|
DisplayManager dpyMgr =
|
||
|
(DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
|
||
|
dpyMgr.unregisterDisplayListener(mDisplayListener);
|
||
|
}
|
||
|
|
||
|
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||
|
@SuppressLint("MissingPermission")
|
||
|
@Override
|
||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||
|
int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||
|
int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||
|
|
||
|
int displayRotation = getDisplay().getRotation();
|
||
|
|
||
|
if (mPreviewSrcSize.getHeight() == 0 || mPreviewSrcSize.getWidth() == 0) {
|
||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||
|
mCameraTextureView.measure(viewWidth, viewHeight);
|
||
|
} else {
|
||
|
Size scaled =
|
||
|
calculatePreviewViewDimens(
|
||
|
mPreviewSrcSize, viewWidth, viewHeight, displayRotation, mScaleType);
|
||
|
super.setMeasuredDimension(
|
||
|
Math.min(scaled.getWidth(), viewWidth),
|
||
|
Math.min(scaled.getHeight(), viewHeight));
|
||
|
mCameraTextureView.measure(scaled.getWidth(), scaled.getHeight());
|
||
|
}
|
||
|
|
||
|
// Since bindToLifecycle will depend on the measured dimension, only call it when measured
|
||
|
// dimension is not 0x0
|
||
|
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
|
||
|
mCameraModule.bindToLifecycleAfterViewMeasured();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||
|
@SuppressLint("MissingPermission")
|
||
|
@Override
|
||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||
|
// In case that the CameraView size is always set as 0x0, we still need to trigger to force
|
||
|
// binding to lifecycle
|
||
|
mCameraModule.bindToLifecycleAfterViewMeasured();
|
||
|
|
||
|
// If we don't know the src buffer size yet, set the preview to be the parent size
|
||
|
if (mPreviewSrcSize.getWidth() == 0 || mPreviewSrcSize.getHeight() == 0) {
|
||
|
mCameraTextureView.layout(left, top, right, bottom);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Compute the preview ui size based on the available width, height, and ui orientation.
|
||
|
int viewWidth = (right - left);
|
||
|
int viewHeight = (bottom - top);
|
||
|
int displayRotation = getDisplay().getRotation();
|
||
|
Size scaled =
|
||
|
calculatePreviewViewDimens(
|
||
|
mPreviewSrcSize, viewWidth, viewHeight, displayRotation, mScaleType);
|
||
|
|
||
|
// Compute the center of the view.
|
||
|
int centerX = viewWidth / 2;
|
||
|
int centerY = viewHeight / 2;
|
||
|
|
||
|
// Compute the left / top / right / bottom values such that preview is centered.
|
||
|
int layoutL = centerX - (scaled.getWidth() / 2);
|
||
|
int layoutT = centerY - (scaled.getHeight() / 2);
|
||
|
int layoutR = layoutL + scaled.getWidth();
|
||
|
int layoutB = layoutT + scaled.getHeight();
|
||
|
|
||
|
// Layout debugging
|
||
|
log("layout: viewWidth: " + viewWidth);
|
||
|
log("layout: viewHeight: " + viewHeight);
|
||
|
log("layout: viewRatio: " + (viewWidth / (float) viewHeight));
|
||
|
log("layout: sizeWidth: " + mPreviewSrcSize.getWidth());
|
||
|
log("layout: sizeHeight: " + mPreviewSrcSize.getHeight());
|
||
|
log(
|
||
|
"layout: sizeRatio: "
|
||
|
+ (mPreviewSrcSize.getWidth() / (float) mPreviewSrcSize.getHeight()));
|
||
|
log("layout: scaledWidth: " + scaled.getWidth());
|
||
|
log("layout: scaledHeight: " + scaled.getHeight());
|
||
|
log("layout: scaledRatio: " + (scaled.getWidth() / (float) scaled.getHeight()));
|
||
|
log(
|
||
|
"layout: size: "
|
||
|
+ scaled
|
||
|
+ " ("
|
||
|
+ (scaled.getWidth() / (float) scaled.getHeight())
|
||
|
+ " - "
|
||
|
+ mScaleType
|
||
|
+ "-"
|
||
|
+ displayRotationToString(displayRotation)
|
||
|
+ ")");
|
||
|
log("layout: final " + layoutL + ", " + layoutT + ", " + layoutR + ", " + layoutB);
|
||
|
|
||
|
mCameraTextureView.layout(layoutL, layoutT, layoutR, layoutB);
|
||
|
|
||
|
mCameraModule.invalidateView();
|
||
|
}
|
||
|
|
||
|
/** Records the size of the preview's buffers. */
|
||
|
@UiThread
|
||
|
void onPreviewSourceDimensUpdated(int srcWidth, int srcHeight) {
|
||
|
if (srcWidth != mPreviewSrcSize.getWidth()
|
||
|
|| srcHeight != mPreviewSrcSize.getHeight()) {
|
||
|
mPreviewSrcSize = new Size(srcWidth, srcHeight);
|
||
|
requestLayout();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private Size calculatePreviewViewDimens(
|
||
|
Size srcSize,
|
||
|
int parentWidth,
|
||
|
int parentHeight,
|
||
|
int displayRotation,
|
||
|
ScaleType scaleType) {
|
||
|
int inWidth = srcSize.getWidth();
|
||
|
int inHeight = srcSize.getHeight();
|
||
|
if (displayRotation == Surface.ROTATION_90 || displayRotation == Surface.ROTATION_270) {
|
||
|
// Need to reverse the width and height since we're in landscape orientation.
|
||
|
inWidth = srcSize.getHeight();
|
||
|
inHeight = srcSize.getWidth();
|
||
|
}
|
||
|
|
||
|
int outWidth = parentWidth;
|
||
|
int outHeight = parentHeight;
|
||
|
if (inWidth != 0 && inHeight != 0) {
|
||
|
float vfRatio = inWidth / (float) inHeight;
|
||
|
float parentRatio = parentWidth / (float) parentHeight;
|
||
|
|
||
|
switch (scaleType) {
|
||
|
case CENTER_INSIDE:
|
||
|
// Match longest sides together.
|
||
|
if (vfRatio > parentRatio) {
|
||
|
outWidth = parentWidth;
|
||
|
outHeight = Math.round(parentWidth / vfRatio);
|
||
|
} else {
|
||
|
outWidth = Math.round(parentHeight * vfRatio);
|
||
|
outHeight = parentHeight;
|
||
|
}
|
||
|
break;
|
||
|
case CENTER_CROP:
|
||
|
// Match shortest sides together.
|
||
|
if (vfRatio < parentRatio) {
|
||
|
outWidth = parentWidth;
|
||
|
outHeight = Math.round(parentWidth / vfRatio);
|
||
|
} else {
|
||
|
outWidth = Math.round(parentHeight * vfRatio);
|
||
|
outHeight = parentHeight;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return new Size(outWidth, outHeight);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, {@link
|
||
|
* Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
|
||
|
*/
|
||
|
int getDisplaySurfaceRotation() {
|
||
|
Display display = getDisplay();
|
||
|
|
||
|
// Null when the View is detached. If we were in the middle of a background operation,
|
||
|
// better to not NPE. When the background operation finishes, it'll realize that the camera
|
||
|
// was closed.
|
||
|
if (display == null) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
return display.getRotation();
|
||
|
}
|
||
|
|
||
|
@UiThread
|
||
|
SurfaceTexture getSurfaceTexture() {
|
||
|
if (mCameraTextureView != null) {
|
||
|
return mCameraTextureView.getSurfaceTexture();
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
@UiThread
|
||
|
void setSurfaceTexture(SurfaceTexture surfaceTexture) {
|
||
|
if (mCameraTextureView.getSurfaceTexture() != surfaceTexture) {
|
||
|
if (mCameraTextureView.isAvailable()) {
|
||
|
// Remove the old TextureView to properly detach the old SurfaceTexture from the GL
|
||
|
// Context.
|
||
|
removeView(mCameraTextureView);
|
||
|
addView(mCameraTextureView = new TextureView(getContext()), 0);
|
||
|
mCameraTextureView.setLayerPaint(mLayerPaint);
|
||
|
requestLayout();
|
||
|
}
|
||
|
|
||
|
mCameraTextureView.setSurfaceTexture(surfaceTexture);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@UiThread
|
||
|
Matrix getTransform(Matrix matrix) {
|
||
|
return mCameraTextureView.getTransform(matrix);
|
||
|
}
|
||
|
|
||
|
@UiThread
|
||
|
int getPreviewWidth() {
|
||
|
return mCameraTextureView.getWidth();
|
||
|
}
|
||
|
|
||
|
@UiThread
|
||
|
int getPreviewHeight() {
|
||
|
return mCameraTextureView.getHeight();
|
||
|
}
|
||
|
|
||
|
@UiThread
|
||
|
void setTransform(final Matrix matrix) {
|
||
|
if (mCameraTextureView != null) {
|
||
|
mCameraTextureView.setTransform(matrix);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the scale type used to scale the preview.
|
||
|
*
|
||
|
* @return The current {@link ScaleType}.
|
||
|
*/
|
||
|
public ScaleType getScaleType() {
|
||
|
return mScaleType;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the view finder scale type.
|
||
|
*
|
||
|
* <p>This controls how the view finder should be scaled and positioned within the view.
|
||
|
*
|
||
|
* @param scaleType The desired {@link ScaleType}.
|
||
|
*/
|
||
|
public void setScaleType(ScaleType scaleType) {
|
||
|
if (scaleType != mScaleType) {
|
||
|
mScaleType = scaleType;
|
||
|
requestLayout();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the scale type used to scale the preview.
|
||
|
*
|
||
|
* @return The current {@link CaptureMode}.
|
||
|
*/
|
||
|
public CaptureMode getCaptureMode() {
|
||
|
return mCameraModule.getCaptureMode();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the CameraView capture mode
|
||
|
*
|
||
|
* <p>This controls only image or video capture function is enabled or both are enabled.
|
||
|
*
|
||
|
* @param captureMode The desired {@link CaptureMode}.
|
||
|
*/
|
||
|
public void setCaptureMode(CaptureMode captureMode) {
|
||
|
mCameraModule.setCaptureMode(captureMode);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the maximum duration of videos, or {@link #INDEFINITE_VIDEO_DURATION} if there is no
|
||
|
* timeout.
|
||
|
*
|
||
|
* @hide Not currently implemented.
|
||
|
*/
|
||
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||
|
public long getMaxVideoDuration() {
|
||
|
return mCameraModule.getMaxVideoDuration();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the maximum video duration before {@link OnVideoSavedListener#onVideoSaved(File)} is
|
||
|
* called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
|
||
|
*/
|
||
|
private void setMaxVideoDuration(long duration) {
|
||
|
mCameraModule.setMaxVideoDuration(duration);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the maximum size of videos in bytes, or {@link #INDEFINITE_VIDEO_SIZE} if there is no
|
||
|
* timeout.
|
||
|
*/
|
||
|
private long getMaxVideoSize() {
|
||
|
return mCameraModule.getMaxVideoSize();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the maximum video size in bytes before {@link OnVideoSavedListener#onVideoSaved(File)}
|
||
|
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
|
||
|
*/
|
||
|
private void setMaxVideoSize(long size) {
|
||
|
mCameraModule.setMaxVideoSize(size);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
|
||
|
* once when done.
|
||
|
*
|
||
|
* @param listener Listener which will receive success or failure callbacks.
|
||
|
*/
|
||
|
public void takePicture(OnImageCapturedListener listener) {
|
||
|
mCameraModule.takePicture(listener);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
|
||
|
*
|
||
|
* @param file The destination.
|
||
|
* @param listener Listener which will receive success or failure callbacks.
|
||
|
*/
|
||
|
public void takePicture(File file, OnImageSavedListener listener) {
|
||
|
mCameraModule.takePicture(file, listener);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Takes a video and calls the OnVideoSavedListener when done.
|
||
|
*
|
||
|
* @param file The destination.
|
||
|
*/
|
||
|
public void startRecording(File file, OnVideoSavedListener listener) {
|
||
|
mCameraModule.startRecording(file, listener);
|
||
|
}
|
||
|
|
||
|
/** Stops an in progress video. */
|
||
|
public void stopRecording() {
|
||
|
mCameraModule.stopRecording();
|
||
|
}
|
||
|
|
||
|
/** @return True if currently recording. */
|
||
|
public boolean isRecording() {
|
||
|
return mCameraModule.isRecording();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Queries whether the current device has a camera with the specified direction.
|
||
|
*
|
||
|
* @return True if the device supports the direction.
|
||
|
* @throws IllegalStateException if the CAMERA permission is not currently granted.
|
||
|
*/
|
||
|
@RequiresPermission(permission.CAMERA)
|
||
|
public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
|
||
|
return mCameraModule.hasCameraWithLensFacing(lensFacing);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Toggles between the primary front facing camera and the primary back facing camera.
|
||
|
*
|
||
|
* <p>This will have no effect if not already bound to a lifecycle via {@link
|
||
|
* #bindToLifecycle(LifecycleOwner)}.
|
||
|
*/
|
||
|
public void toggleCamera() {
|
||
|
mCameraModule.toggleCamera();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the desired camera by specifying desired lensFacing.
|
||
|
*
|
||
|
* <p>This will choose the primary camera with the specified camera lensFacing.
|
||
|
*
|
||
|
* <p>If called before {@link #bindToLifecycle(LifecycleOwner)}, this will set the camera to be
|
||
|
* used when first bound to the lifecycle. If the specified lensFacing is not supported by the
|
||
|
* device, as determined by {@link #hasCameraWithLensFacing(LensFacing)}, the first supported
|
||
|
* lensFacing will be chosen when {@link #bindToLifecycle(LifecycleOwner)} is called.
|
||
|
*
|
||
|
* <p>If called with {@code null} AFTER binding to the lifecycle, the behavior would be
|
||
|
* equivalent to unbind the use cases without the lifecycle having to be destroyed.
|
||
|
*
|
||
|
* @param lensFacing The desired camera lensFacing.
|
||
|
*/
|
||
|
public void setCameraLensFacing(@Nullable LensFacing lensFacing) {
|
||
|
mCameraModule.setCameraLensFacing(lensFacing);
|
||
|
}
|
||
|
|
||
|
/** Returns the currently selected {@link LensFacing}. */
|
||
|
@Nullable
|
||
|
public LensFacing getCameraLensFacing() {
|
||
|
return mCameraModule.getLensFacing();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Focuses the camera on the given area.
|
||
|
*
|
||
|
* <p>Sets the focus and exposure metering rectangles. Coordinates for both X and Y dimensions
|
||
|
* are Limited from -1000 to 1000, where (0, 0) is the center of the image and the width/height
|
||
|
* represent the values from -1000 to 1000.
|
||
|
*
|
||
|
* @param focus Area used to focus the camera.
|
||
|
* @param metering Area used for exposure metering.
|
||
|
*/
|
||
|
public void focus(Rect focus, Rect metering) {
|
||
|
mCameraModule.focus(focus, metering);
|
||
|
}
|
||
|
|
||
|
/** Gets the active flash strategy. */
|
||
|
public FlashMode getFlash() {
|
||
|
return mCameraModule.getFlash();
|
||
|
}
|
||
|
|
||
|
/** Sets the active flash strategy. */
|
||
|
public void setFlash(FlashMode flashMode) {
|
||
|
mCameraModule.setFlash(flashMode);
|
||
|
}
|
||
|
|
||
|
private int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||
|
return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
|
||
|
}
|
||
|
|
||
|
private long delta() {
|
||
|
return System.currentTimeMillis() - mDownEventTimestamp;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onTouchEvent(MotionEvent event) {
|
||
|
// Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
|
||
|
if (mCameraModule.isPaused()) {
|
||
|
return false;
|
||
|
}
|
||
|
// Only forward the event to the pinch-to-zoom gesture detector when pinch-to-zoom is
|
||
|
// enabled.
|
||
|
if (isPinchToZoomEnabled()) {
|
||
|
mPinchToZoomGestureDetector.onTouchEvent(event);
|
||
|
}
|
||
|
if (event.getPointerCount() == 2 && isPinchToZoomEnabled() && isZoomSupported()) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Camera focus
|
||
|
switch (event.getAction()) {
|
||
|
case MotionEvent.ACTION_DOWN:
|
||
|
mDownEventTimestamp = System.currentTimeMillis();
|
||
|
break;
|
||
|
case MotionEvent.ACTION_UP:
|
||
|
if (delta() < ViewConfiguration.getLongPressTimeout()) {
|
||
|
mUpEvent = event;
|
||
|
performClick();
|
||
|
}
|
||
|
break;
|
||
|
default:
|
||
|
// Unhandled event.
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Focus the position of the touch event, or focus the center of the preview for
|
||
|
* accessibility events
|
||
|
*/
|
||
|
@Override
|
||
|
public boolean performClick() {
|
||
|
super.performClick();
|
||
|
|
||
|
final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
|
||
|
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
||
|
mUpEvent = null;
|
||
|
calculateTapArea(mFocusingRect, x, y, 1f);
|
||
|
calculateTapArea(mMeteringRect, x, y, 1.5f);
|
||
|
if (area(mFocusingRect) > 0 && area(mMeteringRect) > 0) {
|
||
|
focus(mFocusingRect, mMeteringRect);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** Returns the width * height of the given rect */
|
||
|
private int area(Rect rect) {
|
||
|
return rect.width() * rect.height();
|
||
|
}
|
||
|
|
||
|
/** The area must be between -1000,-1000 and 1000,1000 */
|
||
|
private void calculateTapArea(Rect rect, float x, float y, float coefficient) {
|
||
|
int max = 1000;
|
||
|
int min = -1000;
|
||
|
|
||
|
// Default to 300 (1/6th the total area) and scale by the coefficient
|
||
|
int areaSize = (int) (300 * coefficient);
|
||
|
|
||
|
// Rotate the coordinates if the camera orientation is different
|
||
|
int width = getWidth();
|
||
|
int height = getHeight();
|
||
|
|
||
|
// Compensate orientation as it's mirrored on preview for forward facing cameras
|
||
|
boolean compensateForMirroring = (getCameraLensFacing() == LensFacing.FRONT);
|
||
|
int relativeCameraOrientation = getRelativeCameraOrientation(compensateForMirroring);
|
||
|
int temp;
|
||
|
float tempf;
|
||
|
switch (relativeCameraOrientation) {
|
||
|
case 90:
|
||
|
// Fall-through
|
||
|
case 270:
|
||
|
// We're horizontal. Swap width/height. Swap x/y.
|
||
|
temp = width;
|
||
|
//noinspection SuspiciousNameCombination
|
||
|
width = height;
|
||
|
height = temp;
|
||
|
|
||
|
tempf = x;
|
||
|
//noinspection SuspiciousNameCombination
|
||
|
x = y;
|
||
|
y = tempf;
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
switch (relativeCameraOrientation) {
|
||
|
// Map to correct coordinates according to relativeCameraOrientation
|
||
|
case 90:
|
||
|
y = height - y;
|
||
|
break;
|
||
|
case 180:
|
||
|
x = width - x;
|
||
|
y = height - y;
|
||
|
break;
|
||
|
case 270:
|
||
|
x = width - x;
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Swap x if it's a mirrored preview
|
||
|
if (compensateForMirroring) {
|
||
|
x = width - x;
|
||
|
}
|
||
|
|
||
|
// Grab the x, y position from within the View and normalize it to -1000 to 1000
|
||
|
x = min + distance(max, min) * (x / width);
|
||
|
y = min + distance(max, min) * (y / height);
|
||
|
|
||
|
// Modify the rect to the bounding area
|
||
|
rect.top = (int) y - areaSize / 2;
|
||
|
rect.left = (int) x - areaSize / 2;
|
||
|
rect.bottom = rect.top + areaSize;
|
||
|
rect.right = rect.left + areaSize;
|
||
|
|
||
|
// Cap at -1000 to 1000
|
||
|
rect.top = rangeLimit(rect.top, max, min);
|
||
|
rect.left = rangeLimit(rect.left, max, min);
|
||
|
rect.bottom = rangeLimit(rect.bottom, max, min);
|
||
|
rect.right = rangeLimit(rect.right, max, min);
|
||
|
}
|
||
|
|
||
|
private int rangeLimit(int val, int max, int min) {
|
||
|
return Math.min(Math.max(val, min), max);
|
||
|
}
|
||
|
|
||
|
float rangeLimit(float val, float max, float min) {
|
||
|
return Math.min(Math.max(val, min), max);
|
||
|
}
|
||
|
|
||
|
private int distance(int a, int b) {
|
||
|
return Math.abs(a - b);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the view allows pinch-to-zoom.
|
||
|
*
|
||
|
* @return True if pinch to zoom is enabled.
|
||
|
*/
|
||
|
public boolean isPinchToZoomEnabled() {
|
||
|
return mIsPinchToZoomEnabled;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets whether the view should allow pinch-to-zoom.
|
||
|
*
|
||
|
* <p>When enabled, the user can pinch the camera to zoom in/out. This only has an effect if the
|
||
|
* bound camera supports zoom.
|
||
|
*
|
||
|
* @param enabled True to enable pinch-to-zoom.
|
||
|
*/
|
||
|
public void setPinchToZoomEnabled(boolean enabled) {
|
||
|
mIsPinchToZoomEnabled = enabled;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the current zoom level.
|
||
|
*
|
||
|
* @return The current zoom level.
|
||
|
*/
|
||
|
public float getZoomLevel() {
|
||
|
return mCameraModule.getZoomLevel();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the current zoom level.
|
||
|
*
|
||
|
* <p>Valid zoom values range from 1 to {@link #getMaxZoomLevel()}.
|
||
|
*
|
||
|
* @param zoomLevel The requested zoom level.
|
||
|
*/
|
||
|
public void setZoomLevel(float zoomLevel) {
|
||
|
mCameraModule.setZoomLevel(zoomLevel);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the minimum zoom level.
|
||
|
*
|
||
|
* <p>For most cameras this should return a zoom level of 1. A zoom level of 1 corresponds to a
|
||
|
* non-zoomed image.
|
||
|
*
|
||
|
* @return The minimum zoom level.
|
||
|
*/
|
||
|
public float getMinZoomLevel() {
|
||
|
return mCameraModule.getMinZoomLevel();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the maximum zoom level.
|
||
|
*
|
||
|
* <p>The zoom level corresponds to the ratio between both the widths and heights of a
|
||
|
* non-zoomed image and a maximally zoomed image for the selected camera.
|
||
|
*
|
||
|
* @return The maximum zoom level.
|
||
|
*/
|
||
|
public float getMaxZoomLevel() {
|
||
|
return mCameraModule.getMaxZoomLevel();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the bound camera supports zooming.
|
||
|
*
|
||
|
* @return True if the camera supports zooming.
|
||
|
*/
|
||
|
public boolean isZoomSupported() {
|
||
|
return mCameraModule.isZoomSupported();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Turns on/off torch.
|
||
|
*
|
||
|
* @param torch True to turn on torch, false to turn off torch.
|
||
|
*/
|
||
|
public void enableTorch(boolean torch) {
|
||
|
mCameraModule.enableTorch(torch);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns current torch status.
|
||
|
*
|
||
|
* @return true if torch is on , otherwise false
|
||
|
*/
|
||
|
public boolean isTorchOn() {
|
||
|
return mCameraModule.isTorchOn();
|
||
|
}
|
||
|
|
||
|
/** Options for scaling the bounds of the view finder to the bounds of this view. */
|
||
|
public enum ScaleType {
|
||
|
/**
|
||
|
* Scale the view finder, maintaining the source aspect ratio, so the view finder fills the
|
||
|
* entire view. This will cause the view finder to crop the source image if the camera
|
||
|
* aspect ratio does not match the view aspect ratio.
|
||
|
*/
|
||
|
CENTER_CROP(0),
|
||
|
/**
|
||
|
* Scale the view finder, maintaining the source aspect ratio, so the view finder is
|
||
|
* entirely contained within the view.
|
||
|
*/
|
||
|
CENTER_INSIDE(1);
|
||
|
|
||
|
private int mId;
|
||
|
|
||
|
int getId() {
|
||
|
return mId;
|
||
|
}
|
||
|
|
||
|
ScaleType(int id) {
|
||
|
mId = id;
|
||
|
}
|
||
|
|
||
|
static ScaleType fromId(int id) {
|
||
|
for (ScaleType st : values()) {
|
||
|
if (st.mId == id) {
|
||
|
return st;
|
||
|
}
|
||
|
}
|
||
|
throw new IllegalArgumentException();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The capture mode used by CameraView.
|
||
|
*
|
||
|
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
||
|
* CameraXView}.
|
||
|
*/
|
||
|
public enum CaptureMode {
|
||
|
/** A mode where image capture is enabled. */
|
||
|
IMAGE(0),
|
||
|
/** A mode where video capture is enabled. */
|
||
|
VIDEO(1),
|
||
|
/**
|
||
|
* A mode where both image capture and video capture are simultaneously enabled. Note that
|
||
|
* this mode may not be available on every device.
|
||
|
*/
|
||
|
MIXED(2);
|
||
|
|
||
|
private int mId;
|
||
|
|
||
|
int getId() {
|
||
|
return mId;
|
||
|
}
|
||
|
|
||
|
CaptureMode(int id) {
|
||
|
mId = id;
|
||
|
}
|
||
|
|
||
|
static CaptureMode fromId(int id) {
|
||
|
for (CaptureMode f : values()) {
|
||
|
if (f.mId == id) {
|
||
|
return f;
|
||
|
}
|
||
|
}
|
||
|
throw new IllegalArgumentException();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static class S extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||
|
private ScaleGestureDetector.OnScaleGestureListener mListener;
|
||
|
|
||
|
void setRealGestureDetector(ScaleGestureDetector.OnScaleGestureListener l) {
|
||
|
mListener = l;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onScale(ScaleGestureDetector detector) {
|
||
|
return mListener.onScale(detector);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class PinchToZoomGestureDetector extends ScaleGestureDetector
|
||
|
implements ScaleGestureDetector.OnScaleGestureListener {
|
||
|
private static final float SCALE_MULTIPIER = 0.75f;
|
||
|
private final BaseInterpolator mInterpolator = new DecelerateInterpolator(2f);
|
||
|
private float mNormalizedScaleFactor = 0;
|
||
|
|
||
|
PinchToZoomGestureDetector(Context context) {
|
||
|
this(context, new S());
|
||
|
}
|
||
|
|
||
|
PinchToZoomGestureDetector(Context context, S s) {
|
||
|
super(context, s);
|
||
|
s.setRealGestureDetector(this);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onScale(ScaleGestureDetector detector) {
|
||
|
mNormalizedScaleFactor += (detector.getScaleFactor() - 1f) * SCALE_MULTIPIER;
|
||
|
// Since the scale factor is normalized, it should always be in the range [0, 1]
|
||
|
mNormalizedScaleFactor = rangeLimit(mNormalizedScaleFactor, 1f, 0);
|
||
|
|
||
|
// Apply decelerate interpolation. This will cause the differences to seem less
|
||
|
// pronounced
|
||
|
// at higher zoom levels.
|
||
|
float transformedScale = mInterpolator.getInterpolation(mNormalizedScaleFactor);
|
||
|
|
||
|
// Transform back from normalized coordinates to the zoom scale
|
||
|
float zoomLevel =
|
||
|
(getMaxZoomLevel() == getMinZoomLevel())
|
||
|
? getMinZoomLevel()
|
||
|
: getMinZoomLevel()
|
||
|
+ transformedScale * (getMaxZoomLevel() - getMinZoomLevel());
|
||
|
|
||
|
setZoomLevel(rangeLimit(zoomLevel, getMaxZoomLevel(), getMinZoomLevel()));
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||
|
float initialZoomLevel = getZoomLevel();
|
||
|
mNormalizedScaleFactor =
|
||
|
(getMaxZoomLevel() == getMinZoomLevel())
|
||
|
? 0
|
||
|
: (initialZoomLevel - getMinZoomLevel())
|
||
|
/ (getMaxZoomLevel() - getMinZoomLevel());
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onScaleEnd(ScaleGestureDetector detector) {
|
||
|
}
|
||
|
}
|
||
|
}
|