blob: 3b730095864cef50b3da7f20f0a93363f5ee9743 [file] [log] [blame]
/*
* Copyright 2022 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.previewview;
import static androidx.camera.previewview.CameraViewFinder.ScaleType.FIT_CENTER;
import static androidx.camera.previewview.CameraViewFinder.ScaleType.FIT_END;
import static androidx.camera.previewview.CameraViewFinder.ScaleType.FIT_START;
import static androidx.camera.previewview.internal.utils.TransformUtils.getRectToRect;
import static androidx.camera.previewview.internal.utils.TransformUtils.is90or270;
import static androidx.camera.previewview.internal.utils.TransformUtils.isAspectRatioMatchingWithRoundingError;
import static androidx.camera.previewview.internal.utils.TransformUtils.surfaceRotationToRotationDegrees;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.LayoutDirection;
import android.util.Size;
import android.view.Display;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.previewview.CameraViewFinder.ScaleType;
import androidx.camera.previewview.internal.transform.TransformationInfo;
import androidx.camera.previewview.internal.utils.Logger;
import androidx.core.util.Preconditions;
/**
* Handles {@link CameraViewFinder} transformation.
*
* <p> This class transforms the camera output and display it in a {@link CameraViewFinder}.
* The goal is to transform it in a way so that the entire area of
* {@link TransformationInfo#getCropRect()} is 1) visible to end users, and 2)
* displayed as large as possible.
*
* <p> The inputs for the calculation are 1) the dimension of the Surface, 2) the crop rect, 3) the
* dimension of the PreviewView and 4) rotation degrees:
*
* <pre>
* Source: +-----Surface-----+ Destination: +-----PreviewView----+
* | | | |
* | +-crop rect-+ | | |
* | | | | +--------------------+
* | | | |
* | | --> | | Rotation: <-----+
* | | | | 270°|
* | | | | |
* | +-----------+ |
* +-----------------+
*
* By mapping the Surface crop rect to match the PreviewView, we have:
*
* +------transformed Surface-------+
* | |
* | +----PreviewView-----+ |
* | | ^ | |
* | | | | |
* | +--------------------+ |
* | |
* +--------------------------------+
* </pre>
*
* <p> The transformed Surface is how the PreviewView's inner view should behave, to make the
* crop rect matches the PreviewView.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
final class PreviewTransformation {
private static final String TAG = "PreviewTransform";
private static final ScaleType DEFAULT_SCALE_TYPE =
ScaleType.FILL_CENTER;
@Nullable
private Size mResolution;
// This represents the area of the Surface that should be visible to end users. The area is
// defined by the Viewport class.
@Nullable
private Rect mSurfaceCropRect;
private int mPreviewRotationDegrees;
private int mTargetRotation;
private boolean mIsFrontCamera;
private ScaleType mScaleType = DEFAULT_SCALE_TYPE;
PreviewTransformation() {
}
/**
* Sets the inputs.
*
* <p> All the values originally come from a
* {@link PreviewSurfaceRequest}.
*/
void setTransformationInfo(@NonNull TransformationInfo transformationInfo,
Size resolution, boolean isFrontCamera) {
mSurfaceCropRect = transformationInfo.getCropRect();
mPreviewRotationDegrees = transformationInfo.getRotationDegrees();
mTargetRotation = transformationInfo.getTargetRotation();
mResolution = resolution;
mIsFrontCamera = isFrontCamera;
}
/**
* Calculates the transformation and applies it to the inner view of {@link CameraViewFinder}.
*
* <p> The inner view could be {@link SurfaceView} or a {@link TextureView}.
* {@link TextureView} needs a preliminary correction since it doesn't handle the
* display rotation.
*/
void transformView(Size previewViewSize, int layoutDirection, @NonNull View preview) {
if (previewViewSize.getHeight() == 0 || previewViewSize.getWidth() == 0) {
Logger.w(TAG, "Transform not applied due to PreviewView size: " + previewViewSize);
return;
}
if (!isTransformationInfoReady()) {
return;
}
if (preview instanceof TextureView) {
// For TextureView, correct the orientation to match the target rotation.
((TextureView) preview).setTransform(getTextureViewCorrectionMatrix());
} else {
// Logs an error if non-display rotation is used with SurfaceView.
Display display = preview.getDisplay();
if (display != null && display.getRotation() != mTargetRotation) {
Logger.e(TAG, "Non-display rotation not supported with SurfaceView / PERFORMANCE "
+ "mode.");
}
}
RectF surfaceRectInPreviewView = getTransformedSurfaceRect(previewViewSize,
layoutDirection);
preview.setPivotX(0);
preview.setPivotY(0);
preview.setScaleX(surfaceRectInPreviewView.width() / mResolution.getWidth());
preview.setScaleY(surfaceRectInPreviewView.height() / mResolution.getHeight());
preview.setTranslationX(surfaceRectInPreviewView.left - preview.getLeft());
preview.setTranslationY(surfaceRectInPreviewView.top - preview.getTop());
}
/**
* Sets the {@link ScaleType}.
*/
void setScaleType(ScaleType scaleType) {
mScaleType = scaleType;
}
/**
* Gets the {@link ScaleType}.
*/
ScaleType getScaleType() {
return mScaleType;
}
@VisibleForTesting
Matrix getTextureViewCorrectionMatrix() {
Preconditions.checkState(isTransformationInfoReady());
RectF surfaceRect = new RectF(0, 0, mResolution.getWidth(), mResolution.getHeight());
int rotationDegrees = -surfaceRotationToRotationDegrees(mTargetRotation);
return getRectToRect(surfaceRect, surfaceRect, rotationDegrees);
}
private boolean isTransformationInfoReady() {
return mSurfaceCropRect != null && mResolution != null;
}
private RectF getTransformedSurfaceRect(Size previewViewSize, int layoutDirection) {
Preconditions.checkState(isTransformationInfoReady());
Matrix surfaceToPreviewView =
getSurfaceToPreviewViewMatrix(previewViewSize, layoutDirection);
RectF rect = new RectF(0, 0, mResolution.getWidth(), mResolution.getHeight());
surfaceToPreviewView.mapRect(rect);
return rect;
}
private Matrix getSurfaceToPreviewViewMatrix(Size previewViewSize, int layoutDirection) {
Preconditions.checkState(isTransformationInfoReady());
// Get the target of the mapping, the coordinates of the crop rect in PreviewView.
RectF previewViewCropRect;
if (isViewportAspectRatioMatchPreviewView(previewViewSize)) {
// If crop rect has the same aspect ratio as PreviewView, scale the crop rect to fill
// the entire PreviewView. This happens if the scale type is FILL_* AND a
// PreviewView-based viewport is used.
previewViewCropRect = new RectF(0, 0, previewViewSize.getWidth(),
previewViewSize.getHeight());
} else {
// If the aspect ratios don't match, it could be 1) scale type is FIT_*, 2) the
// Viewport is not based on the PreviewView or 3) both.
previewViewCropRect = getPreviewViewViewportRectForMismatchedAspectRatios(
previewViewSize, layoutDirection);
}
Matrix matrix = getRectToRect(new RectF(mSurfaceCropRect), previewViewCropRect,
mPreviewRotationDegrees);
if (mIsFrontCamera) {
// SurfaceView/TextureView automatically mirrors the Surface for front camera, which
// needs to be compensated by mirroring the Surface around the upright direction of the
// output image.
if (is90or270(mPreviewRotationDegrees)) {
// If the rotation is 90/270, the Surface should be flipped vertically.
// +---+ 90 +---+ 270 +---+
// | ^ | --> | < | | > |
// +---+ +---+ +---+
matrix.preScale(1F, -1F, mSurfaceCropRect.centerX(), mSurfaceCropRect.centerY());
} else {
// If the rotation is 0/180, the Surface should be flipped horizontally.
// +---+ 0 +---+ 180 +---+
// | ^ | --> | ^ | | v |
// +---+ +---+ +---+
matrix.preScale(-1F, 1F, mSurfaceCropRect.centerX(), mSurfaceCropRect.centerY());
}
}
return matrix;
}
@VisibleForTesting
boolean isViewportAspectRatioMatchPreviewView(Size previewViewSize) {
// Using viewport rect to check if the viewport is based on the PreviewView.
Size rotatedViewportSize = getRotatedViewportSize();
return isAspectRatioMatchingWithRoundingError(
previewViewSize, /* isAccurate1= */ true,
rotatedViewportSize, /* isAccurate2= */ false);
}
private Size getRotatedViewportSize() {
if (is90or270(mPreviewRotationDegrees)) {
return new Size(mSurfaceCropRect.height(), mSurfaceCropRect.width());
}
return new Size(mSurfaceCropRect.width(), mSurfaceCropRect.height());
}
private RectF getPreviewViewViewportRectForMismatchedAspectRatios(Size previewViewSize,
int layoutDirection) {
RectF previewViewRect = new RectF(0, 0, previewViewSize.getWidth(),
previewViewSize.getHeight());
Size rotatedViewportSize = getRotatedViewportSize();
RectF rotatedViewportRect = new RectF(0, 0, rotatedViewportSize.getWidth(),
rotatedViewportSize.getHeight());
Matrix matrix = new Matrix();
setMatrixRectToRect(matrix, rotatedViewportRect, previewViewRect, mScaleType);
matrix.mapRect(rotatedViewportRect);
if (layoutDirection == LayoutDirection.RTL) {
return flipHorizontally(rotatedViewportRect, (float) previewViewSize.getWidth() / 2);
}
return rotatedViewportRect;
}
private static void setMatrixRectToRect(Matrix matrix,
RectF source,
RectF destination,
ScaleType scaleType) {
Matrix.ScaleToFit matrixScaleType;
switch (scaleType) {
case FIT_CENTER:
// Fallthrough.
case FILL_CENTER:
matrixScaleType = Matrix.ScaleToFit.CENTER;
break;
case FIT_END:
// Fallthrough.
case FILL_END:
matrixScaleType = Matrix.ScaleToFit.END;
break;
case FIT_START:
// Fallthrough.
case FILL_START:
matrixScaleType = Matrix.ScaleToFit.START;
break;
default:
Logger.e(TAG, "Unexpected crop rect: " + scaleType);
matrixScaleType = Matrix.ScaleToFit.FILL;
}
boolean isFitTypes =
scaleType == FIT_CENTER || scaleType == FIT_START || scaleType == FIT_END;
if (isFitTypes) {
matrix.setRectToRect(source, destination, matrixScaleType);
} else {
// android.graphics.Matrix doesn't support fill scale types. The workaround is
// mapping inversely from destination to source, then invert the matrix.
matrix.setRectToRect(destination, source, matrixScaleType);
matrix.invert(matrix);
}
}
private static RectF flipHorizontally(RectF original, float flipLineX) {
return new RectF(
flipLineX + flipLineX - original.right,
original.top,
flipLineX + flipLineX - original.left,
original.bottom);
}
}