java.lang.Object
↳FrameLayout
↳androidx.camera.view.PreviewView
Gradle dependencies
compile group: 'androidx.camera', name: 'camera-view', version: '1.5.0-alpha01'
- groupId: androidx.camera
- artifactId: camera-view
- version: 1.5.0-alpha01
Artifact androidx.camera:camera-view:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
Custom View that displays the camera feed for CameraX's Preview use case.
This class manages the preview 's lifecycle. It internally uses either a
TextureView
or 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.
If PreviewView uses a to display the preview
stream, be careful when overlapping a View
that's initially not visible (either
View
or View
) on top of it. When the is
attached to the display window, it calls
which requests a computation of
the transparent regions on the display. At this point, the View
isn't visible, causing
the overlapped region between the and the View
to be
considered transparent. Later if the View
becomes visible
, it
will not be displayed on top of . A way around this is to call
right after making the
View
visible, or initially hiding the View
by setting its
opacity
to 0, then setting it to 1.0F to show it.
There are some limitations of transition animations to and
TextureView
, which applies to PreviewView as well.
Summary
Constructors |
---|
public | PreviewView(Context context)
|
public | PreviewView(Context context, AttributeSet attrs)
|
public | PreviewView(Context context, AttributeSet attrs, int defStyleAttr)
|
public | PreviewView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
|
Methods |
---|
public Bitmap | getBitmap()
Returns a Bitmap representation of the content displayed on the
PreviewView, or null if the camera preview hasn't started yet. |
public CameraController | getController()
Get the CameraController. |
public PreviewView.ImplementationMode | getImplementationMode()
Returns the PreviewView.ImplementationMode. |
public MeteringPointFactory | getMeteringPointFactory()
Gets the MeteringPointFactory for the camera currently connected to the
PreviewView, if any. |
public OutputTransform | getOutputTransform()
Gets the OutputTransform associated with the PreviewView. |
public LiveData<PreviewView.StreamState> | getPreviewStreamState()
Gets a LiveData for the preview PreviewView.StreamState. |
public PreviewView.ScaleType | getScaleType()
Returns the PreviewView.ScaleType currently applied to the preview. |
public ImageCapture.ScreenFlash | getScreenFlash()
Returns an implementation based
on the Window instance set via PreviewView.setScreenFlashWindow(Window). |
public Matrix | getSensorToViewTransform()
Gets the transformation matrix from camera sensor to PreviewView. |
public Preview.SurfaceProvider | getSurfaceProvider()
Gets a to be used with
Preview.setSurfaceProvider(Executor, Preview.SurfaceProvider). |
public ViewPort | getViewPort()
Gets a ViewPort based on the current status of PreviewView. |
public ViewPort | getViewPort(int targetRotation)
Gets a ViewPort with custom target rotation. |
protected void | onAttachedToWindow()
|
protected void | onDetachedFromWindow()
|
public boolean | onTouchEvent(MotionEvent event)
|
public boolean | performClick()
|
public void | setController(CameraController cameraController)
Sets the CameraController. |
public void | setFrameUpdateListener(java.util.concurrent.Executor executor, PreviewView.OnFrameUpdateListener listener)
Sets a listener to receive frame update event with sensor timestamp. |
public void | setImplementationMode(PreviewView.ImplementationMode implementationMode)
Sets the PreviewView.ImplementationMode for the PreviewView. |
public void | setScaleType(PreviewView.ScaleType scaleType)
Applies a PreviewView.ScaleType to the preview. |
public void | setScreenFlashOverlayColor(int color)
Sets the color of the top overlay view during screen flash. |
public void | setScreenFlashWindow(Window screenFlashWindow)
Sets a Window instance for subsequent photo capture requests with
ImageCapture.FLASH_MODE_SCREEN set. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Constructors
public
PreviewView(Context context)
public
PreviewView(Context context, AttributeSet attrs)
public
PreviewView(Context context, AttributeSet attrs, int defStyleAttr)
public
PreviewView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
Methods
protected void
onAttachedToWindow()
protected void
onDetachedFromWindow()
public boolean
onTouchEvent(MotionEvent event)
public boolean
performClick()
Sets the PreviewView.ImplementationMode for the PreviewView.
PreviewView displays the preview with a TextureView
when the
mode is PreviewView.ImplementationMode.COMPATIBLE, and tries to use a if
it is PreviewView.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 PreviewView.ImplementationMode.PERFORMANCE.
This method needs to be called before the is set on
Preview. Once changed, needs to be set again. e.g.
preview.setSurfaceProvider(previewView.getSurfaceProvider()).
Returns the PreviewView.ImplementationMode.
If nothing is set via PreviewView.setImplementationMode(PreviewView.ImplementationMode), the default
value is PreviewView.ImplementationMode.PERFORMANCE.
Returns:
The PreviewView.ImplementationMode for PreviewView.
Gets a to be used with
Preview.setSurfaceProvider(Executor, Preview.SurfaceProvider). This allows the
camera feed to start when the Preview use case is bound to a lifecycle.
The returned will provide a preview to
the camera that's either managed by a TextureView
or depending
on the PreviewView.ImplementationMode and the device's attributes (e.g. API level, camera
hardware support level).
Returns:
A to attach to a Preview use case.
See also: PreviewView.ImplementationMode
Applies a PreviewView.ScaleType to the preview.
If a CameraController is attached to PreviewView, the change will take
immediate effect. It also takes immediate effect if PreviewView.getViewPort() is not set in
the bound UseCaseGroup. Otherwise, the UseCases need to be bound again
with the latest value of PreviewView.getViewPort().
This value can also be set in the layout XML file via the app:scaleType
attribute.
The default value is PreviewView.ScaleType.FILL_CENTER.
Parameters:
scaleType: A PreviewView.ScaleType to apply to the preview.
Returns the PreviewView.ScaleType currently applied to the preview.
The default value is PreviewView.ScaleType.FILL_CENTER.
Returns:
The PreviewView.ScaleType currently applied to the preview.
Gets the MeteringPointFactory for the camera currently connected to the
PreviewView, if any.
The returned MeteringPointFactory is capable of creating MeteringPoints
from (x, y) coordinates in the PreviewView. This conversion takes into account its
PreviewView.ScaleType. The MeteringPointFactory is automatically adjusted if the
PreviewView layout or the PreviewView.ScaleType changes.
The MeteringPointFactory returns invalid MeteringPoint if the
preview is not ready, or the PreviewView dimension is zero. The invalid
MeteringPoint will cause
CameraControl.startFocusAndMetering(FocusMeteringAction) to fail but it won't
crash the application. Wait for the PreviewView.StreamState.STREAMING state to make sure the
preview is ready.
Returns:
a MeteringPointFactory
See also: PreviewView.getPreviewStreamState()
public
LiveData<PreviewView.StreamState>
getPreviewStreamState()
Gets a LiveData for the preview PreviewView.StreamState.
There are two preview stream states, PreviewView.StreamState.IDLE and
PreviewView.StreamState.STREAMING. PreviewView.StreamState.IDLE indicates the preview is currently
not visible and streaming is stopped. PreviewView.StreamState.STREAMING means the preview is
streaming or is about to start streaming. This state guarantees the preview is visible
only when the PreviewView.ImplementationMode is PreviewView.ImplementationMode.COMPATIBLE. When in
PreviewView.ImplementationMode.PERFORMANCE mode, it is possible the preview becomes
visible slightly after the state changes to PreviewView.StreamState.STREAMING.
Apps that require a precise signal for when the preview starts should
set the implementation mode to
PreviewView.ImplementationMode.COMPATIBLE.
Returns:
A LiveData of the preview's PreviewView.StreamState. Apps can get the current
state with LiveData.getValue(), or register an observer with
LiveData.observe(LifecycleOwner, Observer super T>) .
public Bitmap
getBitmap()
Returns a Bitmap
representation of the content displayed on the
PreviewView, or null if the camera preview hasn't started yet.
The returned Bitmap
uses the pixel format and its
dimensions are the same as this view's.
Do not invoke this method from a drawing method
(View
for instance).
If an error occurs during the copy, an empty Bitmap
will be returned.
If the preview hasn't started yet, the method may return null or an empty Bitmap
. Use
PreviewView.getPreviewStreamState() to get the PreviewView.StreamState and wait for
PreviewView.StreamState.STREAMING to make sure the preview is started.
Returns:
A Bitmap
representing the content
displayed on the PreviewView, or null if the camera preview hasn't started yet.
Gets a ViewPort based on the current status of PreviewView.
Returns a ViewPort instance based on the PreviewView's current width,
height, layout direction, scale type and display rotation. By using the ViewPort, all
the UseCases in the UseCaseGroup will have the same output image that also
matches the aspect ratio of the PreviewView.
Returns:
null if the view is not currently attached or the view's width/height is zero.
See also: ViewPort, UseCaseGroup
public
ViewPort getViewPort(int targetRotation)
Gets a ViewPort with custom target rotation.
Returns a ViewPort instance based on the PreviewView's current width,
height, layout direction, scale type and the given target rotation.
Use this method if 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
to obtain the target rotation and create
ViewPort as following:
: orientation == -1
: orientation >= 315 || orientation < 45
: orientation >= 225 && orientation < 315
: orientation >= 135 && orientation < 225
: orientation >= 45 && orientation < 135
Once the target rotation is obtained, use it with Preview.setTargetRotation(int) to
update the rotation. Example:
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);
Note that for non-display rotation to work, the mode must be set to
PreviewView.ImplementationMode.COMPATIBLE.
Parameters:
targetRotation: A rotation value, expressed as one of
, ,
, or
.
Returns:
null if the view's width/height is zero.
See also: PreviewView.ImplementationMode
Sets a listener to receive frame update event with sensor timestamp.
Sets the CameraController.
Once set, the controller will use PreviewView to display camera preview feed.
It also uses the 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
PreviewView. It also enables features like tap-to-focus and pinch-to-zoom.
Setting it to null or to a different CameraController stops the previous
CameraController from working. The previous CameraController will remain
detached until it's set on the PreviewView again.
See also: CameraController
Get the CameraController.
Gets the OutputTransform associated with the PreviewView.
Returns a OutputTransform object that represents the transform being applied to
the associated Preview use case. Returns null if the transform info is not ready.
For example, when the associated Preview has not been bound or the
PreviewView's layout is not ready.
PreviewView needs to be in PreviewView.ImplementationMode.COMPATIBLE mode for the
transform to work correctly. For example, the returned OutputTransform may
not respect the value of PreviewView when PreviewView.ImplementationMode.PERFORMANCE
mode is used.
Returns:
the transform applied on the preview by this PreviewView.
See also: CoordinateTransform
public Matrix
getSensorToViewTransform()
Gets the transformation matrix from camera sensor to PreviewView.
The value is a mapping from sensor coordinates to PreviewView coordinates,
which is, from the rect of CameraCharacteristics
to the
rect defined by (0, 0, PreviewView#getWidth(), PreviewView#getHeight()). The app can
use the matrix to map the coordinates from one UseCase to another. For example,
detecting face with ImageAnalysis, and then highlighting the face in
PreviewView.
This method returns null if the transformation is not ready. It happens when
PreviewView layout has not been measured, or the associated Preview use case
is not yet bound to a camera. For the former case, the app can listen to the layout change
via e.g. PreviewView. For the latter case, the app wait until the
Preview or CameraController is bound and the LifecycleOwner is in
the Lifecycle.State.STARTED state. The app should call this
method to get the latest value before performing coordinates transformation.
The return value does not include the custom transform applied by the app via methods like
View
.
See also: , ImageInfo.getSensorToBufferTransformMatrix()
public void
setScreenFlashWindow(Window screenFlashWindow)
Sets a Window
instance for subsequent photo capture requests with
ImageCapture.FLASH_MODE_SCREEN set.
The calling of this API will take effect for ImageCapture.FLASH_MODE_SCREEN only
and the Window will be ignored for other flash modes. During screen flash photo
capture, the window is used for the purpose of changing brightness.
If the implementation provided by the user is no longer valid (e.g. due to any
or android.view.View
reference used in the
implementation becoming invalid), user needs to re-set a new valid window or
clear the previous one with setScreenFlashWindow(null), whichever appropriate.
For most app scenarios, a Window
instance can be obtained from
. In case of a fragment, Fragment.getActivity() can
first be used to get the activity instance.
Parameters:
screenFlashWindow: A Window
instance that is used to change the brightness
during screen flash photo capture.
Returns an implementation based
on the Window
instance set via PreviewView.setScreenFlashWindow(Window).
This API uses an internally managed ScreenFlashView to provide the
implementation which can be passed to the
ImageCapture.setScreenFlash(ImageCapture.ScreenFlash) API. The following example
shows the API usage.
mPreviewView.setScreenFlashWindow(activity.getWindow());
mImageCapture.setScreenFlash(mPreviewView.getScreenFlash());
mImageCapture.setFlashMode(ImageCapture.FLASH_MODE_SCREEN);
mImageCapture.takePhoto(mCameraExecutor, mOnImageSavedCallback);
Returns:
An implementation provided by
ScreenFlashView.getScreenFlash() which can be null if a non-null Window
instance hasn't been set.
See also: ScreenFlashView.getScreenFlash(), ImageCapture.FLASH_MODE_SCREEN
public void
setScreenFlashOverlayColor(int color)
Sets the color of the top overlay view during screen flash.
Parameters:
color: The color value of the top overlay.
See also: PreviewView.getScreenFlash(), ImageCapture.FLASH_MODE_SCREEN
Source
/*
* 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.ImageOutputConfig.ROTATION_NOT_SPECIFIED;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.getNormalizedToBuffer;
import static androidx.core.content.ContextCompat.getMainExecutor;
import android.annotation.SuppressLint;
import android.app.Activity;
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.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Rational;
import android.util.Size;
import android.view.Display;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
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.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageInfo;
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.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.view.impl.ZoomGestureDetector;
import androidx.camera.view.internal.ScreenFlashUiInfo;
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.fragment.app.Fragment;
import androidx.lifecycle.LifecycleOwner;
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.
*
* <p> There are some limitations of transition animations to {@link SurfaceView} and
* {@link TextureView}, which applies to {@link PreviewView} as well.
*
* @see <a href="https://developer.android.com/training/transitions#Limitations">Limitations</a>
*/
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 ScreenFlashView mScreenFlashView;
@NonNull
final PreviewTransformation mPreviewTransform = new PreviewTransformation();
boolean mUseDisplayRotation = true;
// 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;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
OnFrameUpdateListener mOnFrameUpdateListener;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
Executor mOnFrameUpdateListenerExecutor;
@NonNull
PreviewViewMeteringPointFactory mPreviewViewMeteringPointFactory =
new PreviewViewMeteringPointFactory(mPreviewTransform);
// Detector for zoom-to-scale.
@NonNull
private final ZoomGestureDetector mZoomGestureDetector;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
CameraInfoInternal mCameraInfoInternal;
@Nullable
private MotionEvent mTouchUpEvent;
@NonNull
private final DisplayRotationListener mDisplayRotationListener = new DisplayRotationListener();
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() {
@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();
mCameraInfoInternal = camera.getCameraInfoInternal();
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.
Integer lensFacing = camera.getCameraInfoInternal().getLensFacing();
boolean isFrontCamera;
if (lensFacing == null) {
// TODO(b/122975195): If the lens facing is null, it's probably an
// external camera. We treat it as like a front camera with
// unverified behaviors. Will have to define this later.
Logger.w(TAG, "The lens facing is null, probably an external.");
isFrontCamera = true;
} else {
isFrontCamera = lensFacing == CameraSelector.LENS_FACING_FRONT;
}
mPreviewTransform.setTransformationInfo(transformationInfo,
surfaceRequest.getResolution(), isFrontCamera);
// If targetRotation not specified or it's using SurfaceView, use current
// display rotation.
if (transformationInfo.getTargetRotation() == ROTATION_NOT_SPECIFIED
|| (mImplementation != null
&& mImplementation instanceof SurfaceViewImplementation)) {
mUseDisplayRotation = true;
} else {
mUseDisplayRotation = false;
}
redrawPreview();
});
if (!shouldReuseImplementation(mImplementation, surfaceRequest, mImplementationMode)) {
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);
});
// PreviewViewImplementation#onSurfaceRequested may remove all child views, check if
// ScreenFlashView needs to be re-added
if (PreviewView.this.indexOfChild(mScreenFlashView) == -1) {
PreviewView.this.addView(mScreenFlashView);
}
if (mOnFrameUpdateListener != null && mOnFrameUpdateListenerExecutor != null) {
mImplementation.setFrameUpdateListener(mOnFrameUpdateListenerExecutor,
mOnFrameUpdateListener);
}
}
};
@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();
}
mZoomGestureDetector = new ZoomGestureDetector(context,
zoomEvent -> {
if (zoomEvent instanceof ZoomGestureDetector.ZoomEvent.Move
&& mCameraController != null) {
mCameraController.onPinchToZoom(
((ZoomGestureDetector.ZoomEvent.Move) zoomEvent)
.getIncrementalScaleFactor());
}
return true;
});
// 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));
}
mScreenFlashView = new ScreenFlashView(context);
mScreenFlashView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startListeningToDisplayChange();
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();
}
stopListeningToDisplayChange();
}
@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 mZoomGestureDetector.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;
if (mImplementationMode == ImplementationMode.PERFORMANCE
&& mOnFrameUpdateListener != null) {
throw new IllegalArgumentException(
"PERFORMANCE mode doesn't support frame update listener");
}
}
/**
* 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
*/
@UiThread
@SuppressLint("WrongConstant")
@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) {
updateDisplayRotationIfNeeded();
mImplementation.redrawPreview();
}
mPreviewViewMeteringPointFactory.recalculate(new Size(getWidth(), getHeight()),
getLayoutDirection());
if (mCameraController != null) {
mCameraController.updatePreviewViewTransform(getSensorToViewTransform());
}
}
@VisibleForTesting
static boolean shouldReuseImplementation(@Nullable PreviewViewImplementation implementation,
@NonNull SurfaceRequest surfaceRequest, @NonNull ImplementationMode mode) {
return implementation instanceof SurfaceViewImplementation && !shouldUseTextureView(
surfaceRequest, mode);
}
// 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 (Build.VERSION.SDK_INT <= 24 || isLegacyDevice || hasSurfaceViewQuirk) {
// Force to use TextureView when the device is running android 7.0 and below, legacy
// level or SurfaceView has quirks.
return true;
}
switch (implementationMode) {
case COMPATIBLE:
return true;
case PERFORMANCE:
return false;
default:
throw new IllegalArgumentException(
"Invalid implementation mode: " + implementationMode);
}
}
// Synthetic access
@SuppressWarnings("WeakerAccess")
void updateDisplayRotationIfNeeded() {
if (mUseDisplayRotation) {
Display display = getDisplay();
if (display != null && mCameraInfoInternal != null) {
mPreviewTransform.overrideWithDisplayRotation(
mCameraInfoInternal.getSensorRotationDegrees(
display.getRotation()), display.getRotation());
}
}
}
/**
* Sets a listener to receive frame update event with sensor timestamp.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void setFrameUpdateListener(@NonNull Executor executor,
@NonNull OnFrameUpdateListener listener) {
// SurfaceView doesn't support frame update event.
if (mImplementationMode == ImplementationMode.PERFORMANCE) {
throw new IllegalArgumentException(
"PERFORMANCE mode doesn't support frame update listener");
}
mOnFrameUpdateListener = listener;
mOnFrameUpdateListenerExecutor = executor;
if (mImplementation != null) {
mImplementation.setFrameUpdateListener(executor, listener);
}
}
/**
* Listener to be notified when the frame is updated.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface OnFrameUpdateListener {
/**
* Invoked when frame updates.
*
* @param timestamp sensor timestamp of this frame.
*/
void onFrameUpdate(long timestamp);
}
/**
* 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.
*/
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}. */
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
}
/**
* 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();
setScreenFlashUiInfo(null);
}
mCameraController = cameraController;
attachToControllerIfReady(/*shouldFailSilently=*/false);
setScreenFlashUiInfo(getScreenFlashInternal());
}
/**
* 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 {
if (!getMatrix().isIdentity()) {
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()));
}
/**
* Gets the transformation matrix from camera sensor to {@link PreviewView}.
*
* <p>The value is a mapping from sensor coordinates to {@link PreviewView} coordinates,
* which is, from the rect of {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE} to the
* rect defined by {@code (0, 0, PreviewView#getWidth(), PreviewView#getHeight())}. The app can
* use the matrix to map the coordinates from one {@link UseCase} to another. For example,
* detecting face with {@link ImageAnalysis}, and then highlighting the face in
* {@link PreviewView}.
*
* <p>This method returns {@code null} if the transformation is not ready. It happens when
* {@link PreviewView} layout has not been measured, or the associated {@link Preview} use case
* is not yet bound to a camera. For the former case, the app can listen to the layout change
* via e.g. {@link #addOnLayoutChangeListener}. For the latter case, the app wait until the
* {@link Preview} or {@link CameraController} is bound and the {@link LifecycleOwner} is in
* the {@link androidx.lifecycle.Lifecycle.State#STARTED} state. The app should call this
* method to get the latest value before performing coordinates transformation.
*
* <p>The return value does not include the custom transform applied by the app via methods like
* {@link View#setScaleX(float)}.
*
* @see SurfaceRequest.TransformationInfo#getSensorToBufferTransform()
* @see ImageInfo#getSensorToBufferTransformMatrix()
*/
@UiThread
@Nullable
public Matrix getSensorToViewTransform() {
checkMainThread();
if (getWidth() == 0 || getHeight() == 0) {
return null;
}
return mPreviewTransform.getSensorToViewTransform(
new Size(getWidth(), getHeight()), getLayoutDirection());
}
@MainThread
private void attachToControllerIfReady(boolean shouldFailSilently) {
checkMainThread();
ViewPort viewPort = getViewPort();
if (mCameraController != null && viewPort != null && isAttachedToWindow()) {
try {
mCameraController.attachPreviewSurface(getSurfaceProvider(), viewPort);
} 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;
}
}
}
}
private void setScreenFlashUiInfo(ImageCapture.ScreenFlash control) {
if (mCameraController == null) {
Logger.d(TAG, "setScreenFlashUiInfo: mCameraController is null!");
return;
}
mCameraController.setScreenFlashUiInfo(new ScreenFlashUiInfo(
ScreenFlashUiInfo.ProviderType.PREVIEW_VIEW, control));
}
private void startListeningToDisplayChange() {
DisplayManager displayManager = getDisplayManager();
if (displayManager == null) {
return;
}
displayManager.registerDisplayListener(mDisplayRotationListener,
new Handler(Looper.getMainLooper()));
}
private void stopListeningToDisplayChange() {
DisplayManager displayManager = getDisplayManager();
if (displayManager == null) {
return;
}
displayManager.unregisterDisplayListener(mDisplayRotationListener);
}
@Nullable
private DisplayManager getDisplayManager() {
Context context = getContext();
if (context == null) {
return null;
}
return (DisplayManager) context.getApplicationContext()
.getSystemService(Context.DISPLAY_SERVICE);
}
/**
* Sets a {@link Window} instance for subsequent photo capture requests with
* {@link ImageCapture#FLASH_MODE_SCREEN} set.
*
* <p>The calling of this API will take effect for {@link ImageCapture#FLASH_MODE_SCREEN} only
* and the {@code Window} will be ignored for other flash modes. During screen flash photo
* capture, the window is used for the purpose of changing brightness.
*
* <p>If the implementation provided by the user is no longer valid (e.g. due to any
* {@link android.app.Activity} or {@link android.view.View} reference used in the
* implementation becoming invalid), user needs to re-set a new valid window or
* clear the previous one with {@code setScreenFlashWindow(null)}, whichever appropriate.
*
* <p>For most app scenarios, a {@link Window} instance can be obtained from
* {@link Activity#getWindow()}. In case of a fragment, {@link Fragment#getActivity()} can
* first be used to get the activity instance.
*
* @param screenFlashWindow A {@link Window} instance that is used to change the brightness
* during screen flash photo capture.
*/
@UiThread
public void setScreenFlashWindow(@Nullable Window screenFlashWindow) {
checkMainThread();
mScreenFlashView.setScreenFlashWindow(screenFlashWindow);
setScreenFlashUiInfo(getScreenFlashInternal());
}
// Workaround to expose getScreenFlash as experimental, so that other APIs already using it also
// don't need to be annotated with experimental (e.g. PreviewView.setController)
@UiThread
@Nullable
private ImageCapture.ScreenFlash getScreenFlashInternal() {
return mScreenFlashView.getScreenFlash();
}
/**
* Returns an {@link ImageCapture.ScreenFlash} implementation based
* on the {@link Window} instance set via {@link #setScreenFlashWindow(Window)}.
*
* <p> This API uses an internally managed {@link ScreenFlashView} to provide the
* {@link ImageCapture.ScreenFlash} implementation which can be passed to the
* {@link ImageCapture#setScreenFlash(ImageCapture.ScreenFlash)} API. The following example
* shows the API usage.
* <pre>{@code
* mPreviewView.setScreenFlashWindow(activity.getWindow());
* mImageCapture.setScreenFlash(mPreviewView.getScreenFlash());
* mImageCapture.setFlashMode(ImageCapture.FLASH_MODE_SCREEN);
* mImageCapture.takePhoto(mCameraExecutor, mOnImageSavedCallback);
* }</pre>
*
* @return An {@link ImageCapture.ScreenFlash} implementation provided by
* {@link ScreenFlashView#getScreenFlash()} which can be null if a non-null {@code Window}
* instance hasn't been set.
*
* @see ScreenFlashView#getScreenFlash()
* @see ImageCapture#FLASH_MODE_SCREEN
*/
@ExperimentalPreviewViewScreenFlash
@UiThread
@Nullable
public ImageCapture.ScreenFlash getScreenFlash() {
return getScreenFlashInternal();
}
/**
* Sets the color of the top overlay view during screen flash.
*
* @param color The color value of the top overlay.
*
* @see #getScreenFlash()
* @see ImageCapture#FLASH_MODE_SCREEN
*/
@ExperimentalPreviewViewScreenFlash
public void setScreenFlashOverlayColor(@ColorInt int color) {
mScreenFlashView.setBackgroundColor(color);
}
/**
* Listener for display rotation changes.
*
* <p> When the device is rotated 180° from side to side, the activity is not
* destroyed and recreated. In some foldable or large screen devices, when rotating devices
* in multi-window mode, it's also possible that activity is not recreated. This class is
* necessary to make sure preview's display rotation gets updated when that happens.
*/
// Synthetic access
@SuppressWarnings("WeakerAccess")
class DisplayRotationListener implements DisplayManager.DisplayListener {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
Display display = getDisplay();
if (display != null && display.getDisplayId() == displayId) {
redrawPreview();
}
}
}
}