| /* |
| * Copyright (C) 2013 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 com.android.camera; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Matrix; |
| import android.graphics.RectF; |
| import android.graphics.SurfaceTexture; |
| import android.view.TextureView; |
| import android.view.View; |
| import android.view.View.OnLayoutChangeListener; |
| |
| import com.android.camera.app.AppController; |
| import com.android.camera.app.CameraProvider; |
| import com.android.camera.app.OrientationManager; |
| import com.android.camera.debug.Log; |
| import com.android.camera.device.CameraId; |
| import com.android.camera.ui.PreviewStatusListener; |
| import com.android.camera.util.ApiHelper; |
| import com.android.camera.util.CameraUtil; |
| import com.android.camera2.R; |
| import com.android.ex.camera2.portability.CameraDeviceInfo; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * This class aims to automate TextureView transform change and notify listeners |
| * (e.g. bottom bar) of the preview size change. |
| */ |
| public class TextureViewHelper implements TextureView.SurfaceTextureListener, |
| OnLayoutChangeListener { |
| |
| private static final Log.Tag TAG = new Log.Tag("TexViewHelper"); |
| public static final float MATCH_SCREEN = 0f; |
| private static final int UNSET = -1; |
| private final TextureView mPreview; |
| private final CameraProvider mCameraProvider; |
| private int mWidth = 0; |
| private int mHeight = 0; |
| private RectF mPreviewArea = new RectF(); |
| private float mAspectRatio = MATCH_SCREEN; |
| private boolean mAutoAdjustTransform = true; |
| private TextureView.SurfaceTextureListener mSurfaceTextureListener; |
| |
| private final ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener> |
| mAspectRatioChangedListeners = |
| new ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>(); |
| |
| private final ArrayList<PreviewStatusListener.PreviewAreaChangedListener> |
| mPreviewSizeChangedListeners = |
| new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>(); |
| private OnLayoutChangeListener mOnLayoutChangeListener = null; |
| private CaptureLayoutHelper mCaptureLayoutHelper = null; |
| private int mOrientation = UNSET; |
| |
| // Hack to allow to know which module is running for b/20694189 |
| private final AppController mAppController; |
| private final int mCameraModeId; |
| private final int mCaptureIntentModeId; |
| |
| public TextureViewHelper(TextureView preview, CaptureLayoutHelper helper, |
| CameraProvider cameraProvider, AppController appController) { |
| mPreview = preview; |
| mCameraProvider = cameraProvider; |
| mPreview.addOnLayoutChangeListener(this); |
| mPreview.setSurfaceTextureListener(this); |
| mCaptureLayoutHelper = helper; |
| mAppController = appController; |
| mCameraModeId = appController.getAndroidContext().getResources() |
| .getInteger(R.integer.camera_mode_photo); |
| mCaptureIntentModeId = appController.getAndroidContext().getResources() |
| .getInteger(R.integer.camera_mode_capture_intent); |
| } |
| |
| /** |
| * If auto adjust transform is enabled, when there is a layout change, the |
| * transform matrix will be automatically adjusted based on the preview |
| * stream aspect ratio in the new layout. |
| * |
| * @param enable whether or not auto adjustment should be enabled |
| */ |
| public void setAutoAdjustTransform(boolean enable) { |
| mAutoAdjustTransform = enable; |
| } |
| |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, |
| int oldTop, int oldRight, int oldBottom) { |
| Log.v(TAG, "onLayoutChange"); |
| int width = right - left; |
| int height = bottom - top; |
| int rotation = CameraUtil.getDisplayRotation(); |
| if (mWidth != width || mHeight != height || mOrientation != rotation) { |
| mWidth = width; |
| mHeight = height; |
| mOrientation = rotation; |
| if (!updateTransform()) { |
| clearTransform(); |
| } |
| } |
| if (mOnLayoutChangeListener != null) { |
| mOnLayoutChangeListener.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, |
| oldRight, oldBottom); |
| } |
| } |
| |
| /** |
| * Transforms the preview with the identity matrix, ensuring there is no |
| * scaling on the preview. It also calls onPreviewSizeChanged, to trigger |
| * any necessary preview size changing callbacks. |
| */ |
| public void clearTransform() { |
| mPreview.setTransform(new Matrix()); |
| mPreviewArea.set(0, 0, mWidth, mHeight); |
| onPreviewAreaChanged(mPreviewArea); |
| setAspectRatio(MATCH_SCREEN); |
| } |
| |
| public void updateAspectRatio(float aspectRatio) { |
| Log.v(TAG, "updateAspectRatio " + aspectRatio); |
| if (aspectRatio <= 0) { |
| Log.e(TAG, "Invalid aspect ratio: " + aspectRatio); |
| return; |
| } |
| if (aspectRatio < 1f) { |
| aspectRatio = 1f / aspectRatio; |
| } |
| setAspectRatio(aspectRatio); |
| updateTransform(); |
| } |
| |
| private void setAspectRatio(float aspectRatio) { |
| Log.v(TAG, "setAspectRatio: " + aspectRatio); |
| if (mAspectRatio != aspectRatio) { |
| Log.v(TAG, "aspect ratio changed from: " + mAspectRatio); |
| mAspectRatio = aspectRatio; |
| onAspectRatioChanged(); |
| } |
| } |
| |
| private void onAspectRatioChanged() { |
| mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio); |
| for (PreviewStatusListener.PreviewAspectRatioChangedListener listener |
| : mAspectRatioChangedListeners) { |
| listener.onPreviewAspectRatioChanged(mAspectRatio); |
| } |
| } |
| |
| public void addAspectRatioChangedListener( |
| PreviewStatusListener.PreviewAspectRatioChangedListener listener) { |
| if (listener != null && !mAspectRatioChangedListeners.contains(listener)) { |
| mAspectRatioChangedListeners.add(listener); |
| } |
| } |
| |
| /** |
| * This returns the rect that is available to display the preview, and |
| * capture buttons |
| * |
| * @return the rect. |
| */ |
| public RectF getFullscreenRect() { |
| return mCaptureLayoutHelper.getFullscreenRect(); |
| } |
| |
| /** |
| * This takes a matrix to apply to the texture view and uses the screen |
| * aspect ratio as the target aspect ratio |
| * |
| * @param matrix the matrix to apply |
| * @param aspectRatio the aspectRatio that the preview should be |
| */ |
| public void updateTransformFullScreen(Matrix matrix, float aspectRatio) { |
| aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio; |
| if (aspectRatio != mAspectRatio) { |
| setAspectRatio(aspectRatio); |
| } |
| |
| mPreview.setTransform(matrix); |
| mPreviewArea = mCaptureLayoutHelper.getPreviewRect(); |
| onPreviewAreaChanged(mPreviewArea); |
| |
| } |
| |
| public void updateTransform(Matrix matrix) { |
| RectF previewRect = new RectF(0, 0, mWidth, mHeight); |
| matrix.mapRect(previewRect); |
| |
| float previewWidth = previewRect.width(); |
| float previewHeight = previewRect.height(); |
| if (previewHeight == 0 || previewWidth == 0) { |
| Log.e(TAG, "Invalid preview size: " + previewWidth + " x " + previewHeight); |
| return; |
| } |
| float aspectRatio = previewWidth / previewHeight; |
| aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio; |
| if (aspectRatio != mAspectRatio) { |
| setAspectRatio(aspectRatio); |
| } |
| |
| RectF previewAreaBasedOnAspectRatio = mCaptureLayoutHelper.getPreviewRect(); |
| Matrix addtionalTransform = new Matrix(); |
| addtionalTransform.setRectToRect(previewRect, previewAreaBasedOnAspectRatio, |
| Matrix.ScaleToFit.CENTER); |
| matrix.postConcat(addtionalTransform); |
| mPreview.setTransform(matrix); |
| updatePreviewArea(matrix); |
| } |
| |
| /** |
| * Calculates and updates the preview area rect using the latest transform |
| * matrix. |
| */ |
| private void updatePreviewArea(Matrix matrix) { |
| mPreviewArea.set(0, 0, mWidth, mHeight); |
| matrix.mapRect(mPreviewArea); |
| onPreviewAreaChanged(mPreviewArea); |
| } |
| |
| public void setOnLayoutChangeListener(OnLayoutChangeListener listener) { |
| mOnLayoutChangeListener = listener; |
| } |
| |
| public void setSurfaceTextureListener(TextureView.SurfaceTextureListener listener) { |
| mSurfaceTextureListener = listener; |
| } |
| |
| /** |
| * Returns a transformation matrix that implements rotation that is |
| * consistent with CaptureLayoutHelper and TextureViewHelper. The magical |
| * invariant for CaptureLayoutHelper and TextureViewHelper that must be |
| * obeyed is that the bounding box of the view must be EXACTLY the bounding |
| * box of the surfaceDimensions AFTER the transformation has been applied. |
| * |
| * @param currentDisplayOrientation The current display orientation, |
| * measured counterclockwise from to the device's natural |
| * orientation (in degrees, always a multiple of 90, and between |
| * 0 and 270, inclusive). |
| * @param surfaceDimensions The dimensions of the |
| * {@link android.view.Surface} on which the preview image is |
| * being rendered. It usually only makes sense for the upper-left |
| * corner to be at the origin. |
| * @param desiredBounds The boundaries within the |
| * {@link android.view.Surface} where the final image should |
| * appear. These can be used to translate and scale the output, |
| * but note that the image will be stretched to fit, possibly |
| * changing its aspect ratio. |
| * @return The transform matrix that should be applied to the |
| * {@link android.view.Surface} in order for the image to display |
| * properly in the device's current orientation. |
| */ |
| public Matrix getPreviewRotationalTransform(int currentDisplayOrientation, |
| RectF surfaceDimensions, |
| RectF desiredBounds) { |
| if (surfaceDimensions.equals(desiredBounds)) { |
| return new Matrix(); |
| } |
| |
| Matrix transform = new Matrix(); |
| transform.setRectToRect(surfaceDimensions, desiredBounds, Matrix.ScaleToFit.FILL); |
| |
| RectF normalRect = surfaceDimensions; |
| // Bounding box of 90 or 270 degree rotation. |
| RectF rotatedRect = new RectF(normalRect.width() / 2 - normalRect.height() / 2, |
| normalRect.height() / 2 - normalRect.width() / 2, |
| normalRect.width() / 2 + normalRect.height() / 2, |
| normalRect.height() / 2 + normalRect.width() / 2); |
| |
| OrientationManager.DeviceOrientation deviceOrientation = |
| OrientationManager.DeviceOrientation.from(currentDisplayOrientation); |
| |
| // This rotation code assumes that the aspect ratio of the content |
| // (not of necessarily the surface) equals the aspect ratio of view that is receiving |
| // the preview. So, a 4:3 surface that contains 16:9 data will look correct as |
| // long as the view is also 16:9. |
| switch (deviceOrientation) { |
| case CLOCKWISE_90: |
| transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL); |
| transform.preRotate(270, mWidth / 2, mHeight / 2); |
| break; |
| case CLOCKWISE_180: |
| transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL); |
| transform.preRotate(180, mWidth / 2, mHeight / 2); |
| break; |
| case CLOCKWISE_270: |
| transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL); |
| transform.preRotate(90, mWidth / 2, mHeight / 2); |
| break; |
| case CLOCKWISE_0: |
| default: |
| transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL); |
| break; |
| } |
| |
| return transform; |
| } |
| |
| /** |
| * Updates the transform matrix based current width and height of |
| * TextureView and preview stream aspect ratio. |
| * <p> |
| * If not {@code mAutoAdjustTransform}, this does nothing except return |
| * {@code false}. In all other cases, it returns {@code true}, regardless of |
| * whether the transform was changed. |
| * </p> |
| * In {@code mAutoAdjustTransform} and the CameraProvder is invalid, it is assumed |
| * that the CaptureModule/PhotoModule is Camera2 API-based and must implements its |
| * rotation via matrix transformation implemented in getPreviewRotationalTransform. |
| * |
| * @return Whether {@code mAutoAdjustTransform}. |
| */ |
| private boolean updateTransform() { |
| Log.v(TAG, "updateTransform"); |
| if (!mAutoAdjustTransform) { |
| return false; |
| } |
| |
| if (mAspectRatio == MATCH_SCREEN || mAspectRatio < 0 || mWidth == 0 || mHeight == 0) { |
| return true; |
| } |
| |
| Matrix matrix = new Matrix(); |
| CameraId cameraKey = mCameraProvider.getCurrentCameraId(); |
| int cameraId = -1; |
| |
| try { |
| cameraId = cameraKey.getLegacyValue(); |
| } catch (UnsupportedOperationException ignored) { |
| Log.e(TAG, "TransformViewHelper does not support Camera API2"); |
| } |
| |
| |
| // Only apply this fix when Current Active Module is Photo module AND |
| // Phone is Nexus4 The enhancement fix b/20694189 to original fix to |
| // b/19271661 ensures that the fix should only be applied when: |
| // 1) the phone is a Nexus4 which requires the specific workaround |
| // 2) CaptureModule is enabled. |
| // 3) the Camera Photo Mode Or Capture Intent Photo Mode is active |
| if (ApiHelper.IS_NEXUS_4 && mAppController.getCameraFeatureConfig().isUsingCaptureModule() |
| && (mAppController.getCurrentModuleIndex() == mCameraModeId || |
| mAppController.getCurrentModuleIndex() == mCaptureIntentModeId)) { |
| Log.v(TAG, "Applying Photo Mode, Capture Module, Nexus-4 specific fix for b/19271661"); |
| mOrientation = CameraUtil.getDisplayRotation(); |
| matrix = getPreviewRotationalTransform(mOrientation, |
| new RectF(0, 0, mWidth, mHeight), |
| mCaptureLayoutHelper.getPreviewRect()); |
| } else if (cameraId >= 0) { |
| // Otherwise, do the default, legacy action. |
| CameraDeviceInfo.Characteristics info = mCameraProvider.getCharacteristics(cameraId); |
| matrix = info.getPreviewTransform(mOrientation, new RectF(0, 0, mWidth, mHeight), |
| mCaptureLayoutHelper.getPreviewRect()); |
| } else { |
| // Do Nothing |
| } |
| |
| mPreview.setTransform(matrix); |
| updatePreviewArea(matrix); |
| return true; |
| } |
| |
| private void onPreviewAreaChanged(final RectF previewArea) { |
| // Notify listeners of preview area change |
| final List<PreviewStatusListener.PreviewAreaChangedListener> listeners = |
| new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>( |
| mPreviewSizeChangedListeners); |
| // This method can be called during layout pass. We post a Runnable so |
| // that the callbacks won't happen during the layout pass. |
| mPreview.post(new Runnable() { |
| @Override |
| public void run() { |
| for (PreviewStatusListener.PreviewAreaChangedListener listener : listeners) { |
| listener.onPreviewAreaChanged(previewArea); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Returns a new copy of the preview area, to avoid internal data being |
| * modified from outside of the class. |
| */ |
| public RectF getPreviewArea() { |
| return new RectF(mPreviewArea); |
| } |
| |
| /** |
| * Returns a copy of the area of the whole preview, including bits clipped |
| * by the view |
| */ |
| public RectF getTextureArea() { |
| |
| if (mPreview == null) { |
| return new RectF(); |
| } |
| Matrix matrix = new Matrix(); |
| RectF area = new RectF(0, 0, mWidth, mHeight); |
| mPreview.getTransform(matrix).mapRect(area); |
| return area; |
| } |
| |
| public Bitmap getPreviewBitmap(int downsample) { |
| RectF textureArea = getTextureArea(); |
| int width = (int) textureArea.width() / downsample; |
| int height = (int) textureArea.height() / downsample; |
| Bitmap preview = mPreview.getBitmap(width, height); |
| return Bitmap.createBitmap(preview, 0, 0, width, height, mPreview.getTransform(null), true); |
| } |
| |
| /** |
| * Adds a listener that will get notified when the preview area changed. |
| * This can be useful for UI elements or focus overlay to adjust themselves |
| * according to the preview area change. |
| * <p/> |
| * Note that a listener will only be added once. A newly added listener will |
| * receive a notification of current preview area immediately after being |
| * added. |
| * <p/> |
| * This function should be called on the UI thread and listeners will be |
| * notified on the UI thread. |
| * |
| * @param listener the listener that will get notified of preview area |
| * change |
| */ |
| public void addPreviewAreaSizeChangedListener( |
| PreviewStatusListener.PreviewAreaChangedListener listener) { |
| if (listener != null && !mPreviewSizeChangedListeners.contains(listener)) { |
| mPreviewSizeChangedListeners.add(listener); |
| if (mPreviewArea.width() == 0 || mPreviewArea.height() == 0) { |
| listener.onPreviewAreaChanged(new RectF(0, 0, mWidth, mHeight)); |
| } else { |
| listener.onPreviewAreaChanged(new RectF(mPreviewArea)); |
| } |
| } |
| } |
| |
| /** |
| * Removes a listener that gets notified when the preview area changed. |
| * |
| * @param listener the listener that gets notified of preview area change |
| */ |
| public void removePreviewAreaSizeChangedListener( |
| PreviewStatusListener.PreviewAreaChangedListener listener) { |
| if (listener != null && mPreviewSizeChangedListeners.contains(listener)) { |
| mPreviewSizeChangedListeners.remove(listener); |
| } |
| } |
| |
| @Override |
| public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { |
| // Workaround for b/11168275, see b/10981460 for more details |
| if (mWidth != 0 && mHeight != 0) { |
| // Re-apply transform matrix for new surface texture |
| updateTransform(); |
| } |
| if (mSurfaceTextureListener != null) { |
| mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height); |
| } |
| } |
| |
| @Override |
| public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { |
| if (mSurfaceTextureListener != null) { |
| mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height); |
| } |
| } |
| |
| @Override |
| public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { |
| if (mSurfaceTextureListener != null) { |
| mSurfaceTextureListener.onSurfaceTextureDestroyed(surface); |
| } |
| return false; |
| } |
| |
| @Override |
| public void onSurfaceTextureUpdated(SurfaceTexture surface) { |
| if (mSurfaceTextureListener != null) { |
| mSurfaceTextureListener.onSurfaceTextureUpdated(surface); |
| } |
| |
| } |
| } |