| /* |
| * 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; |
| } |
| } |
| } |
| } |
| } |