/* * 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.app.Activity; 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 mAspectRatioChangedListeners = new ArrayList(); private final ArrayList mPreviewSizeChangedListeners = new ArrayList(); 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((Activity) mPreview.getContext()); 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. *

* 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. *

* 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((Activity) mPreview.getContext()); 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 listeners = new ArrayList( 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. *

* 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. *

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