blob: d39bb63e184f585557cccc97b30fc0ead21923a7 [file] [log] [blame]
/*
* Copyright 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.
*/
package androidx.camera.view;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.view.TransformUtils.getNormalizedToBuffer;
import static androidx.core.content.ContextCompat.getMainExecutor;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Rational;
import android.util.Size;
import android.view.Display;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.FrameLayout;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.Logger;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.core.Preview;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.UseCase;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.core.ViewPort;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
import androidx.camera.view.internal.compat.quirk.SurfaceViewNotCroppedByParentQuirk;
import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
import androidx.camera.view.transform.CoordinateTransform;
import androidx.camera.view.transform.OutputTransform;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
/**
* Custom View that displays the camera feed for CameraX's {@link Preview} use case.
*
* <p> This class manages the preview {@link Surface}'s lifecycle. It internally uses either a
* {@link TextureView} or {@link SurfaceView} to display the camera feed, and applies required
* transformations on them to correctly display the preview, this involves correcting their
* aspect ratio, scale and rotation.
*
* <p> If {@link PreviewView} uses a {@link SurfaceView} to display the preview
* stream, be careful when overlapping a {@link View} that's initially not visible (either
* {@link View#INVISIBLE} or {@link View#GONE}) on top of it. When the {@link SurfaceView} is
* attached to the display window, it calls
* {@link android.view.ViewParent#requestTransparentRegion(View)} which requests a computation of
* the transparent regions on the display. At this point, the {@link View} isn't visible, causing
* the overlapped region between the {@link SurfaceView} and the {@link View} to be
* considered transparent. Later if the {@link View} becomes {@linkplain View#VISIBLE visible}, it
* will not be displayed on top of {@link SurfaceView}. A way around this is to call
* {@link android.view.ViewParent#requestTransparentRegion(View)} right after making the
* {@link View} visible, or initially hiding the {@link View} by setting its
* {@linkplain View#setAlpha(float) opacity} to 0, then setting it to 1.0F to show it.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class PreviewView extends FrameLayout {
private static final String TAG = "PreviewView";
@ColorRes
static final int DEFAULT_BACKGROUND_COLOR = android.R.color.black;
private static final ImplementationMode DEFAULT_IMPL_MODE = ImplementationMode.PERFORMANCE;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
ImplementationMode mImplementationMode = DEFAULT_IMPL_MODE;
@VisibleForTesting
@Nullable
PreviewViewImplementation mImplementation;
@NonNull
final PreviewTransformation mPreviewTransform = new PreviewTransformation();
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
final MutableLiveData<StreamState> mPreviewStreamStateLiveData =
new MutableLiveData<>(StreamState.IDLE);
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
final AtomicReference<PreviewStreamStateObserver> mActiveStreamStateObserver =
new AtomicReference<>();
// Synthetic access
@SuppressWarnings("WeakerAccess")
CameraController mCameraController;
@NonNull
PreviewViewMeteringPointFactory mPreviewViewMeteringPointFactory =
new PreviewViewMeteringPointFactory(mPreviewTransform);
// Detector for zoom-to-scale.
@NonNull
private final ScaleGestureDetector mScaleGestureDetector;
@Nullable
private MotionEvent mTouchUpEvent;
private final OnLayoutChangeListener mOnLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
boolean isSizeChanged =
right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop;
if (isSizeChanged) {
redrawPreview();
attachToControllerIfReady(true);
}
};
// Synthetic access
@SuppressWarnings("WeakerAccess")
final Preview.SurfaceProvider mSurfaceProvider = new Preview.SurfaceProvider() {
// TODO(b/185869869) Remove the UnsafeOptInUsageError once view's version matches core's.
@SuppressLint("UnsafeOptInUsageError")
@Override
@AnyThread
public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
if (!Threads.isMainThread()) {
// Post on main thread to ensure thread safety.
getMainExecutor(getContext()).execute(
() -> mSurfaceProvider.onSurfaceRequested(surfaceRequest));
return;
}
Logger.d(TAG, "Surface requested by Preview.");
CameraInternal camera = surfaceRequest.getCamera();
surfaceRequest.setTransformationInfoListener(
getMainExecutor(getContext()),
transformationInfo -> {
Logger.d(TAG,
"Preview transformation info updated. " + transformationInfo);
// TODO(b/159127402): maybe switch to COMPATIBLE mode if target
// rotation is not display rotation.
boolean isFrontCamera =
camera.getCameraInfoInternal().getLensFacing()
== CameraSelector.LENS_FACING_FRONT;
mPreviewTransform.setTransformationInfo(transformationInfo,
surfaceRequest.getResolution(), isFrontCamera);
redrawPreview();
});
mImplementation = shouldUseTextureView(surfaceRequest, mImplementationMode)
? new TextureViewImplementation(PreviewView.this, mPreviewTransform)
: new SurfaceViewImplementation(PreviewView.this, mPreviewTransform);
PreviewStreamStateObserver streamStateObserver =
new PreviewStreamStateObserver(camera.getCameraInfoInternal(),
mPreviewStreamStateLiveData, mImplementation);
mActiveStreamStateObserver.set(streamStateObserver);
camera.getCameraState().addObserver(
getMainExecutor(getContext()), streamStateObserver);
mImplementation.onSurfaceRequested(surfaceRequest, () -> {
// We've no longer needed this observer, if there is no new StreamStateObserver
// (another SurfaceRequest), reset the streamState to IDLE.
// This is needed for the case when unbinding preview while other use cases are
// still bound.
if (mActiveStreamStateObserver.compareAndSet(streamStateObserver, null)) {
streamStateObserver.updatePreviewStreamState(StreamState.IDLE);
}
streamStateObserver.clear();
camera.getCameraState().removeObserver(streamStateObserver);
});
}
};
@UiThread
public PreviewView(@NonNull Context context) {
this(context, null);
}
@UiThread
public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@UiThread
public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
@UiThread
public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
checkMainThread();
final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.PreviewView, defStyleAttr, defStyleRes);
ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.PreviewView, attrs,
attributes, defStyleAttr, defStyleRes);
try {
final int scaleTypeId = attributes.getInteger(
R.styleable.PreviewView_scaleType,
mPreviewTransform.getScaleType().getId());
setScaleType(ScaleType.fromId(scaleTypeId));
int implementationModeId =
attributes.getInteger(R.styleable.PreviewView_implementationMode,
DEFAULT_IMPL_MODE.getId());
setImplementationMode(ImplementationMode.fromId(implementationModeId));
} finally {
attributes.recycle();
}
mScaleGestureDetector = new ScaleGestureDetector(
context, new PinchToZoomOnScaleGestureListener());
// Set background only if it wasn't already set. A default background prevents the content
// behind the PreviewView from being visible before the preview starts streaming.
if (getBackground() == null) {
setBackgroundColor(ContextCompat.getColor(getContext(), DEFAULT_BACKGROUND_COLOR));
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addOnLayoutChangeListener(mOnLayoutChangeListener);
if (mImplementation != null) {
mImplementation.onAttachedToWindow();
}
attachToControllerIfReady(true);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnLayoutChangeListener(mOnLayoutChangeListener);
if (mImplementation != null) {
mImplementation.onDetachedFromWindow();
}
if (mCameraController != null) {
mCameraController.clearPreviewSurface();
}
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (mCameraController == null) {
// Do not consume events if controller is not set.
return super.onTouchEvent(event);
}
boolean isSingleTouch = event.getPointerCount() == 1;
boolean isUpEvent = event.getAction() == MotionEvent.ACTION_UP;
boolean notALongPress = event.getEventTime() - event.getDownTime()
< ViewConfiguration.getLongPressTimeout();
if (isSingleTouch && isUpEvent && notALongPress) {
// If the event is a click, invoke tap-to-focus and forward it to user's
// OnClickListener#onClick.
mTouchUpEvent = event;
performClick();
// A click has been detected and forwarded. Consume the event so onClick won't be
// invoked twice.
return true;
}
return mScaleGestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
}
@Override
public boolean performClick() {
if (mCameraController != null) {
// mTouchUpEvent == null means it's an accessibility click. Focus at the center instead.
float x = mTouchUpEvent != null ? mTouchUpEvent.getX() : getWidth() / 2f;
float y = mTouchUpEvent != null ? mTouchUpEvent.getY() : getHeight() / 2f;
mCameraController.onTapToFocus(mPreviewViewMeteringPointFactory, x, y);
}
mTouchUpEvent = null;
return super.performClick();
}
/**
* Sets the {@link ImplementationMode} for the {@link PreviewView}.
*
* <p> {@link PreviewView} displays the preview with a {@link TextureView} when the
* mode is {@link ImplementationMode#COMPATIBLE}, and tries to use a {@link SurfaceView} if
* it is {@link ImplementationMode#PERFORMANCE} when possible, which depends on the device's
* attributes (e.g. API level, camera hardware support level). If not set, the default mode
* is {@link ImplementationMode#PERFORMANCE}.
*
* <p> This method needs to be called before the {@link Preview.SurfaceProvider} is set on
* {@link Preview}. Once changed, {@link Preview.SurfaceProvider} needs to be set again. e.g.
* {@code preview.setSurfaceProvider(previewView.getSurfaceProvider())}.
*/
@UiThread
public void setImplementationMode(@NonNull final ImplementationMode implementationMode) {
checkMainThread();
mImplementationMode = implementationMode;
}
/**
* Returns the {@link ImplementationMode}.
*
* <p> If nothing is set via {@link #setImplementationMode}, the default
* value is {@link ImplementationMode#PERFORMANCE}.
*
* @return The {@link ImplementationMode} for {@link PreviewView}.
*/
@UiThread
@NonNull
public ImplementationMode getImplementationMode() {
checkMainThread();
return mImplementationMode;
}
/**
* Gets a {@link Preview.SurfaceProvider} to be used with
* {@link Preview#setSurfaceProvider(Executor, Preview.SurfaceProvider)}. This allows the
* camera feed to start when the {@link Preview} use case is bound to a lifecycle.
*
* <p> The returned {@link Preview.SurfaceProvider} will provide a preview {@link Surface} to
* the camera that's either managed by a {@link TextureView} or {@link SurfaceView} depending
* on the {@link ImplementationMode} and the device's attributes (e.g. API level, camera
* hardware support level).
*
* @return A {@link Preview.SurfaceProvider} to attach to a {@link Preview} use case.
* @see ImplementationMode
*/
@UiThread
@NonNull
public Preview.SurfaceProvider getSurfaceProvider() {
checkMainThread();
return mSurfaceProvider;
}
/**
* Applies a {@link ScaleType} to the preview.
*
* <p> If a {@link CameraController} is attached to {@link PreviewView}, the change will take
* immediate effect. It also takes immediate effect if {@link #getViewPort()} is not set in
* the bound {@link UseCaseGroup}. Otherwise, the {@link UseCase}s need to be bound again
* with the latest value of {@link #getViewPort()}.
*
* <p> This value can also be set in the layout XML file via the {@code app:scaleType}
* attribute.
*
* <p> The default value is {@link ScaleType#FILL_CENTER}.
*
* @param scaleType A {@link ScaleType} to apply to the preview.
* @attr name app:scaleType
*/
@UiThread
public void setScaleType(@NonNull final ScaleType scaleType) {
checkMainThread();
mPreviewTransform.setScaleType(scaleType);
redrawPreview();
// Notify controller to re-calculate the crop rect.
attachToControllerIfReady(false);
}
/**
* Returns the {@link ScaleType} currently applied to the preview.
*
* <p> The default value is {@link ScaleType#FILL_CENTER}.
*
* @return The {@link ScaleType} currently applied to the preview.
*/
@UiThread
@NonNull
public ScaleType getScaleType() {
checkMainThread();
return mPreviewTransform.getScaleType();
}
/**
* Gets the {@link MeteringPointFactory} for the camera currently connected to the
* {@link PreviewView}, if any.
*
* <p> The returned {@link MeteringPointFactory} is capable of creating {@link MeteringPoint}s
* from (x, y) coordinates in the {@link PreviewView}. This conversion takes into account its
* {@link ScaleType}. The {@link MeteringPointFactory} is automatically adjusted if the
* {@link PreviewView} layout or the {@link ScaleType} changes.
*
* <p> The {@link MeteringPointFactory} returns invalid {@link MeteringPoint} if the
* preview is not ready, or the {@link PreviewView} dimension is zero. The invalid
* {@link MeteringPoint} will cause
* {@link CameraControl#startFocusAndMetering(FocusMeteringAction)} to fail but it won't
* crash the application. Wait for the {@link StreamState#STREAMING} state to make sure the
* preview is ready.
*
* @return a {@link MeteringPointFactory}
* @see #getPreviewStreamState()
*/
@UiThread
@NonNull
public MeteringPointFactory getMeteringPointFactory() {
checkMainThread();
return mPreviewViewMeteringPointFactory;
}
/**
* Gets a {@link LiveData} for the preview {@link StreamState}.
*
* <p>There are two preview stream states, {@link StreamState#IDLE} and
* {@link StreamState#STREAMING}. {@link StreamState#IDLE} indicates the preview is currently
* not visible and streaming is stopped. {@link StreamState#STREAMING} means the preview is
* streaming or is about to start streaming. This state guarantees the preview is visible
* only when the {@link ImplementationMode} is {@link ImplementationMode#COMPATIBLE}. When in
* {@link ImplementationMode#PERFORMANCE} mode, it is possible the preview becomes
* visible slightly after the state changes to {@link StreamState#STREAMING}.
*
* <p>Apps that require a precise signal for when the preview starts should
* {@linkplain #setImplementationMode(ImplementationMode) set} the implementation mode to
* {@link ImplementationMode#COMPATIBLE}.
*
* @return A {@link LiveData} of the preview's {@link StreamState}. Apps can get the current
* state with {@link LiveData#getValue()}, or register an observer with
* {@link LiveData#observe} .
*/
@NonNull
public LiveData<StreamState> getPreviewStreamState() {
return mPreviewStreamStateLiveData;
}
/**
* Returns a {@link Bitmap} representation of the content displayed on the
* {@link PreviewView}, or {@code null} if the camera preview hasn't started yet.
* <p>
* The returned {@link Bitmap} uses the {@link Bitmap.Config#ARGB_8888} pixel format and its
* dimensions are the same as this view's.
* <p>
* <strong>Do not</strong> invoke this method from a drawing method
* ({@link View#onDraw(Canvas)} for instance).
* <p>
* If an error occurs during the copy, an empty {@link Bitmap} will be returned.
* <p>
* If the preview hasn't started yet, the method may return null or an empty {@link Bitmap}. Use
* {@link #getPreviewStreamState()} to get the {@link StreamState} and wait for
* {@link StreamState#STREAMING} to make sure the preview is started.
*
* @return A {@link Bitmap.Config#ARGB_8888} {@link Bitmap} representing the content
* displayed on the {@link PreviewView}, or null if the camera preview hasn't started yet.
*/
@UiThread
@Nullable
public Bitmap getBitmap() {
checkMainThread();
return mImplementation == null ? null : mImplementation.getBitmap();
}
/**
* Gets a {@link ViewPort} based on the current status of {@link PreviewView}.
*
* <p> Returns a {@link ViewPort} instance based on the {@link PreviewView}'s current width,
* height, layout direction, scale type and display rotation. By using the {@link ViewPort}, all
* the {@link UseCase}s in the {@link UseCaseGroup} will have the same output image that also
* matches the aspect ratio of the {@link PreviewView}.
*
* @return null if the view is not currently attached or the view's width/height is zero.
* @see ViewPort
* @see UseCaseGroup
*/
@UiThread
@Nullable
public ViewPort getViewPort() {
checkMainThread();
if (getDisplay() == null) {
// Returns null if the layout is not ready.
return null;
}
return getViewPort(getDisplay().getRotation());
}
/**
* Gets a {@link ViewPort} with custom target rotation.
*
* <p>Returns a {@link ViewPort} instance based on the {@link PreviewView}'s current width,
* height, layout direction, scale type and the given target rotation.
*
* <p>Use this method if {@link Preview}'s desired rotation is not the default display
* rotation. For example, when remote display is in use and the desired rotation for the
* remote display is based on the accelerometer reading. In that case, use
* {@link android.view.OrientationEventListener} to obtain the target rotation and create
* {@link ViewPort} as following:
* <p>{@link android.view.OrientationEventListener#ORIENTATION_UNKNOWN}: orientation == -1
* <p>{@link Surface#ROTATION_0}: orientation >= 315 || orientation < 45
* <p>{@link Surface#ROTATION_90}: orientation >= 225 && orientation < 315
* <p>{@link Surface#ROTATION_180}: orientation >= 135 && orientation < 225
* <p>{@link Surface#ROTATION_270}: orientation >= 45 && orientation < 135
*
* <p> Once the target rotation is obtained, use it with {@link Preview#setTargetRotation} to
* update the rotation. Example:
*
* <pre><code>
* Preview preview = new Preview.Builder().setTargetRotation(targetRotation).build();
* ViewPort viewPort = previewView.getViewPort(targetRotation);
* UseCaseGroup useCaseGroup =
* new UseCaseGroup.Builder().setViewPort(viewPort).addUseCase(preview).build();
* cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup);
* </code></pre>
*
* <p> Note that for non-display rotation to work, the mode must be set to
* {@link ImplementationMode#COMPATIBLE}.
*
* @param targetRotation A rotation value, expressed as one of
* {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
* {@link Surface#ROTATION_180}, or
* {@link Surface#ROTATION_270}.
* @return null if the view's width/height is zero.
* @see ImplementationMode
*/
// TODO(b/185869869) Remove the UnsafeOptInUsageError once view's version matches core's.
@UiThread
@SuppressLint({"WrongConstant", "UnsafeOptInUsageError"})
@Nullable
public ViewPort getViewPort(@ImageOutputConfig.RotationValue int targetRotation) {
checkMainThread();
if (getWidth() == 0 || getHeight() == 0) {
return null;
}
return new ViewPort.Builder(new Rational(getWidth(), getHeight()), targetRotation)
.setScaleType(getViewPortScaleType())
.setLayoutDirection(getLayoutDirection())
.build();
}
/**
* Converts {@link PreviewView.ScaleType} to {@link ViewPort.ScaleType}.
*/
private int getViewPortScaleType() {
switch (getScaleType()) {
case FILL_END:
return ViewPort.FILL_END;
case FILL_CENTER:
return ViewPort.FILL_CENTER;
case FILL_START:
return ViewPort.FILL_START;
case FIT_END:
// Fallthrough
case FIT_CENTER:
// Fallthrough
case FIT_START:
return ViewPort.FIT;
default:
throw new IllegalStateException("Unexpected scale type: " + getScaleType());
}
}
// Synthetic access
@SuppressWarnings("WeakerAccess")
@MainThread
@OptIn(markerClass = TransformExperimental.class)
void redrawPreview() {
checkMainThread();
if (mImplementation != null) {
mImplementation.redrawPreview();
}
mPreviewViewMeteringPointFactory.recalculate(new Size(getWidth(), getHeight()),
getLayoutDirection());
if (mCameraController != null) {
mCameraController.updatePreviewViewTransform(getOutputTransform());
}
}
// Synthetic access
@SuppressWarnings("WeakerAccess")
static boolean shouldUseTextureView(@NonNull SurfaceRequest surfaceRequest,
@NonNull final ImplementationMode implementationMode) {
// TODO(b/159127402): use TextureView if target rotation is not display rotation.
boolean isLegacyDevice = surfaceRequest.getCamera().getCameraInfoInternal()
.getImplementationType().equals(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
|| DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice
|| hasSurfaceViewQuirk) {
// Force to use TextureView when the device is running android 7.0 and below, legacy
// level, RGBA8888 is required or SurfaceView has quirks.
return true;
}
switch (implementationMode) {
case COMPATIBLE:
return true;
case PERFORMANCE:
return false;
default:
throw new IllegalArgumentException(
"Invalid implementation mode: " + implementationMode);
}
}
/**
* The implementation mode of a {@link PreviewView}.
*
* <p> User preference on how the {@link PreviewView} should render the preview.
* {@link PreviewView} displays the preview with either a {@link SurfaceView} or a
* {@link TextureView}. A {@link SurfaceView} is generally better than a {@link TextureView}
* when it comes to certain key metrics, including power and latency. On the other hand,
* {@link TextureView} is better supported by a wider range of devices. The option is used by
* {@link PreviewView} to decide what is the best internal implementation given the device
* capabilities and user configurations.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public enum ImplementationMode {
/**
* Use a {@link SurfaceView} for the preview when possible. If the device
* doesn't support {@link SurfaceView}, {@link PreviewView} will fall back to use a
* {@link TextureView} instead.
*
* <p>{@link PreviewView} falls back to {@link TextureView} when the API level is 24 or
* lower, the camera hardware support level is
* {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY}, or
* {@link Preview#getTargetRotation()} is different from {@link PreviewView}'s display
* rotation.
*
* <p>Do not use this mode if {@link Preview.Builder#setTargetRotation(int)} is set
* to a value different than the display's rotation, because {@link SurfaceView} does not
* support arbitrary rotations. Do not use this mode if the {@link PreviewView}
* needs to be animated. {@link SurfaceView} animation is not supported on API level 24
* or lower. Also, for the preview's streaming state provided in
* {@link #getPreviewStreamState}, the {@link StreamState#STREAMING} state might happen
* prematurely if this mode is used.
*
* @see Preview.Builder#setTargetRotation(int)
* @see Preview.Builder#getTargetRotation()
* @see Display#getRotation()
* @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
* @see StreamState#STREAMING
*/
PERFORMANCE(0),
/**
* Use a {@link TextureView} for the preview.
*/
COMPATIBLE(1);
private final int mId;
ImplementationMode(int id) {
mId = id;
}
int getId() {
return mId;
}
static ImplementationMode fromId(int id) {
for (ImplementationMode implementationMode : values()) {
if (implementationMode.mId == id) {
return implementationMode;
}
}
throw new IllegalArgumentException("Unknown implementation mode id " + id);
}
}
/** Options for scaling the preview vis-à-vis its container {@link PreviewView}. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public enum ScaleType {
/**
* Scale the preview, maintaining the source aspect ratio, so it fills the entire
* {@link PreviewView}, and align it to the start of the view, which is the top left
* corner in a left-to-right (LTR) layout, or the top right corner in a right-to-left
* (RTL) layout.
* <p>
* This may cause the preview to be cropped if the camera preview aspect ratio does not
* match that of its container {@link PreviewView}.
*/
FILL_START(0),
/**
* Scale the preview, maintaining the source aspect ratio, so it fills the entire
* {@link PreviewView}, and center it in the view.
* <p>
* This may cause the preview to be cropped if the camera preview aspect ratio does not
* match that of its container {@link PreviewView}.
*/
FILL_CENTER(1),
/**
* Scale the preview, maintaining the source aspect ratio, so it fills the entire
* {@link PreviewView}, and align it to the end of the view, which is the bottom right
* corner in a left-to-right (LTR) layout, or the bottom left corner in a right-to-left
* (RTL) layout.
* <p>
* This may cause the preview to be cropped if the camera preview aspect ratio does not
* match that of its container {@link PreviewView}.
*/
FILL_END(2),
/**
* Scale the preview, maintaining the source aspect ratio, so it is entirely contained
* within the {@link PreviewView}, and align it to the start of the view, which is the
* top left corner in a left-to-right (LTR) layout, or the top right corner in a
* right-to-left (RTL) layout. The background area not covered by the preview stream
* will be black or the background of the {@link PreviewView}
* <p>
* Both dimensions of the preview will be equal or less than the corresponding dimensions
* of its container {@link PreviewView}.
*/
FIT_START(3),
/**
* Scale the preview, maintaining the source aspect ratio, so it is entirely contained
* within the {@link PreviewView}, and center it inside the view. The background area not
* covered by the preview stream will be black or the background of the {@link PreviewView}.
* <p>
* Both dimensions of the preview will be equal or less than the corresponding dimensions
* of its container {@link PreviewView}.
*/
FIT_CENTER(4),
/**
* Scale the preview, maintaining the source aspect ratio, so it is entirely contained
* within the {@link PreviewView}, and align it to the end of the view, which is the
* bottom right corner in a left-to-right (LTR) layout, or the bottom left corner in a
* right-to-left (RTL) layout. The background area not covered by the preview stream
* will be black or the background of the {@link PreviewView}.
* <p>
* Both dimensions of the preview will be equal or less than the corresponding dimensions
* of its container {@link PreviewView}.
*/
FIT_END(5);
private final int mId;
ScaleType(int id) {
mId = id;
}
int getId() {
return mId;
}
static ScaleType fromId(int id) {
for (ScaleType scaleType : values()) {
if (scaleType.mId == id) {
return scaleType;
}
}
throw new IllegalArgumentException("Unknown scale type id " + id);
}
}
/**
* Definitions for the preview stream state.
*/
public enum StreamState {
/** Preview is not visible yet. */
IDLE,
/**
* Preview is streaming.
*
* <p>This state only guarantees the preview is streaming when the implementation mode is
* {@link ImplementationMode#COMPATIBLE}. When in {@link ImplementationMode#PERFORMANCE}
* mode, it is possible that the preview becomes visible slightly after the state has
* changed. For apps requiring a precise signal for when the preview starts, please set
* {@link ImplementationMode#COMPATIBLE} mode via {@link #setImplementationMode}.
*/
STREAMING
}
/**
* GestureListener that speeds up scale factor and sends it to controller.
*/
class PinchToZoomOnScaleGestureListener extends
ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mCameraController != null) {
mCameraController.onPinchToZoom(detector.getScaleFactor());
}
return true;
}
}
/**
* Sets the {@link CameraController}.
*
* <p> Once set, the controller will use {@link PreviewView} to display camera preview feed.
* It also uses the {@link PreviewView}'s layout dimension to set the crop rect for all the use
* cases so that the output from other use cases match what the end user sees in
* {@link PreviewView}. It also enables features like tap-to-focus and pinch-to-zoom.
*
* <p> Setting it to {@code null} or to a different {@link CameraController} stops the previous
* {@link CameraController} from working. The previous {@link CameraController} will remain
* detached until it's set on the {@link PreviewView} again.
*
* @throws IllegalArgumentException If the {@link CameraController}'s camera selector
* is unable to resolve a camera to be used for the enabled
* use cases.
* @see CameraController
*/
@UiThread
public void setController(@Nullable CameraController cameraController) {
checkMainThread();
if (mCameraController != null && mCameraController != cameraController) {
// If already bound to a different controller, ask the old controller to stop
// using this PreviewView.
mCameraController.clearPreviewSurface();
}
mCameraController = cameraController;
attachToControllerIfReady(/*shouldFailSilently=*/false);
}
/**
* Get the {@link CameraController}.
*/
@Nullable
@UiThread
public CameraController getController() {
checkMainThread();
return mCameraController;
}
/**
* Gets the {@link OutputTransform} associated with the {@link PreviewView}.
*
* <p> Returns a {@link OutputTransform} object that represents the transform being applied to
* the associated {@link Preview} use case. Returns null if the transform info is not ready.
* For example, when the associated {@link Preview} has not been bound or the
* {@link PreviewView}'s layout is not ready.
*
* <p> {@link PreviewView} needs to be in {@link ImplementationMode#COMPATIBLE} mode for the
* transform to work correctly. For example, the returned {@link OutputTransform} may
* not respect the value of {@link #getMatrix()} when {@link ImplementationMode#PERFORMANCE}
* mode is used.
*
* @return the transform applied on the preview by this {@link PreviewView}.
* @see CoordinateTransform
*/
@TransformExperimental
@Nullable
public OutputTransform getOutputTransform() {
checkMainThread();
Matrix matrix = null;
try {
matrix = mPreviewTransform.getSurfaceToPreviewViewMatrix(
new Size(getWidth(), getHeight()), getLayoutDirection());
} catch (IllegalStateException ex) {
// Fall-through. It will be handled below.
}
Rect surfaceCropRect = mPreviewTransform.getSurfaceCropRect();
if (matrix == null || surfaceCropRect == null) {
Logger.d(TAG, "Transform info is not ready");
return null;
}
// Map it to the normalized space (-1, -1) - (1, 1).
matrix.preConcat(getNormalizedToBuffer(surfaceCropRect));
// Add the custom transform applied by the app. e.g. View#setScaleX.
if (mImplementation instanceof TextureViewImplementation) {
matrix.postConcat(getMatrix());
} else {
Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
+ " to work correctly.");
}
return new OutputTransform(matrix, new Size(surfaceCropRect.width(),
surfaceCropRect.height()));
}
@MainThread
private void attachToControllerIfReady(boolean shouldFailSilently) {
checkMainThread();
Display display = getDisplay();
ViewPort viewPort = getViewPort();
if (mCameraController != null && viewPort != null && isAttachedToWindow()
&& display != null) {
try {
mCameraController.attachPreviewSurface(getSurfaceProvider(), viewPort, display);
} catch (IllegalStateException ex) {
if (shouldFailSilently) {
// Swallow the exception and fail silently if the method is invoked by View
// events.
Logger.e(TAG, ex.toString(), ex);
} else {
throw ex;
}
}
}
}
}