Home | History | Annotate | Download | only in camera
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.camera;
     18 
     19 import android.graphics.Bitmap;
     20 import android.graphics.Matrix;
     21 import android.graphics.RectF;
     22 import android.graphics.SurfaceTexture;
     23 import android.view.TextureView;
     24 import android.view.View;
     25 import android.view.View.OnLayoutChangeListener;
     26 
     27 import com.android.camera.app.AppController;
     28 import com.android.camera.app.CameraProvider;
     29 import com.android.camera.app.OrientationManager;
     30 import com.android.camera.debug.Log;
     31 import com.android.camera.device.CameraId;
     32 import com.android.camera.ui.PreviewStatusListener;
     33 import com.android.camera.util.ApiHelper;
     34 import com.android.camera.util.CameraUtil;
     35 import com.android.camera2.R;
     36 import com.android.ex.camera2.portability.CameraDeviceInfo;
     37 
     38 import java.util.ArrayList;
     39 import java.util.List;
     40 
     41 /**
     42  * This class aims to automate TextureView transform change and notify listeners
     43  * (e.g. bottom bar) of the preview size change.
     44  */
     45 public class TextureViewHelper implements TextureView.SurfaceTextureListener,
     46         OnLayoutChangeListener {
     47 
     48     private static final Log.Tag TAG = new Log.Tag("TexViewHelper");
     49     public static final float MATCH_SCREEN = 0f;
     50     private static final int UNSET = -1;
     51     private final TextureView mPreview;
     52     private final CameraProvider mCameraProvider;
     53     private int mWidth = 0;
     54     private int mHeight = 0;
     55     private RectF mPreviewArea = new RectF();
     56     private float mAspectRatio = MATCH_SCREEN;
     57     private boolean mAutoAdjustTransform = true;
     58     private TextureView.SurfaceTextureListener mSurfaceTextureListener;
     59 
     60     private final ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>
     61             mAspectRatioChangedListeners =
     62             new ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>();
     63 
     64     private final ArrayList<PreviewStatusListener.PreviewAreaChangedListener>
     65             mPreviewSizeChangedListeners =
     66             new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>();
     67     private OnLayoutChangeListener mOnLayoutChangeListener = null;
     68     private CaptureLayoutHelper mCaptureLayoutHelper = null;
     69     private int mOrientation = UNSET;
     70 
     71     // Hack to allow to know which module is running for b/20694189
     72     private final AppController mAppController;
     73     private final int mCameraModeId;
     74     private final int mCaptureIntentModeId;
     75 
     76     public TextureViewHelper(TextureView preview, CaptureLayoutHelper helper,
     77             CameraProvider cameraProvider, AppController appController) {
     78         mPreview = preview;
     79         mCameraProvider = cameraProvider;
     80         mPreview.addOnLayoutChangeListener(this);
     81         mPreview.setSurfaceTextureListener(this);
     82         mCaptureLayoutHelper = helper;
     83         mAppController = appController;
     84         mCameraModeId = appController.getAndroidContext().getResources()
     85                 .getInteger(R.integer.camera_mode_photo);
     86         mCaptureIntentModeId = appController.getAndroidContext().getResources()
     87                 .getInteger(R.integer.camera_mode_capture_intent);
     88     }
     89 
     90     /**
     91      * If auto adjust transform is enabled, when there is a layout change, the
     92      * transform matrix will be automatically adjusted based on the preview
     93      * stream aspect ratio in the new layout.
     94      *
     95      * @param enable whether or not auto adjustment should be enabled
     96      */
     97     public void setAutoAdjustTransform(boolean enable) {
     98         mAutoAdjustTransform = enable;
     99     }
    100 
    101     @Override
    102     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
    103             int oldTop, int oldRight, int oldBottom) {
    104         Log.v(TAG, "onLayoutChange");
    105         int width = right - left;
    106         int height = bottom - top;
    107         int rotation = CameraUtil.getDisplayRotation();
    108         if (mWidth != width || mHeight != height || mOrientation != rotation) {
    109             mWidth = width;
    110             mHeight = height;
    111             mOrientation = rotation;
    112             if (!updateTransform()) {
    113                 clearTransform();
    114             }
    115         }
    116         if (mOnLayoutChangeListener != null) {
    117             mOnLayoutChangeListener.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop,
    118                     oldRight, oldBottom);
    119         }
    120     }
    121 
    122     /**
    123      * Transforms the preview with the identity matrix, ensuring there is no
    124      * scaling on the preview. It also calls onPreviewSizeChanged, to trigger
    125      * any necessary preview size changing callbacks.
    126      */
    127     public void clearTransform() {
    128         mPreview.setTransform(new Matrix());
    129         mPreviewArea.set(0, 0, mWidth, mHeight);
    130         onPreviewAreaChanged(mPreviewArea);
    131         setAspectRatio(MATCH_SCREEN);
    132     }
    133 
    134     public void updateAspectRatio(float aspectRatio) {
    135         Log.v(TAG, "updateAspectRatio " + aspectRatio);
    136         if (aspectRatio <= 0) {
    137             Log.e(TAG, "Invalid aspect ratio: " + aspectRatio);
    138             return;
    139         }
    140         if (aspectRatio < 1f) {
    141             aspectRatio = 1f / aspectRatio;
    142         }
    143         setAspectRatio(aspectRatio);
    144         updateTransform();
    145     }
    146 
    147     private void setAspectRatio(float aspectRatio) {
    148         Log.v(TAG, "setAspectRatio: " + aspectRatio);
    149         if (mAspectRatio != aspectRatio) {
    150             Log.v(TAG, "aspect ratio changed from: " + mAspectRatio);
    151             mAspectRatio = aspectRatio;
    152             onAspectRatioChanged();
    153         }
    154     }
    155 
    156     private void onAspectRatioChanged() {
    157         mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio);
    158         for (PreviewStatusListener.PreviewAspectRatioChangedListener listener
    159                 : mAspectRatioChangedListeners) {
    160             listener.onPreviewAspectRatioChanged(mAspectRatio);
    161         }
    162     }
    163 
    164     public void addAspectRatioChangedListener(
    165             PreviewStatusListener.PreviewAspectRatioChangedListener listener) {
    166         if (listener != null && !mAspectRatioChangedListeners.contains(listener)) {
    167             mAspectRatioChangedListeners.add(listener);
    168         }
    169     }
    170 
    171     /**
    172      * This returns the rect that is available to display the preview, and
    173      * capture buttons
    174      *
    175      * @return the rect.
    176      */
    177     public RectF getFullscreenRect() {
    178         return mCaptureLayoutHelper.getFullscreenRect();
    179     }
    180 
    181     /**
    182      * This takes a matrix to apply to the texture view and uses the screen
    183      * aspect ratio as the target aspect ratio
    184      *
    185      * @param matrix the matrix to apply
    186      * @param aspectRatio the aspectRatio that the preview should be
    187      */
    188     public void updateTransformFullScreen(Matrix matrix, float aspectRatio) {
    189         aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
    190         if (aspectRatio != mAspectRatio) {
    191             setAspectRatio(aspectRatio);
    192         }
    193 
    194         mPreview.setTransform(matrix);
    195         mPreviewArea = mCaptureLayoutHelper.getPreviewRect();
    196         onPreviewAreaChanged(mPreviewArea);
    197 
    198     }
    199 
    200     public void updateTransform(Matrix matrix) {
    201         RectF previewRect = new RectF(0, 0, mWidth, mHeight);
    202         matrix.mapRect(previewRect);
    203 
    204         float previewWidth = previewRect.width();
    205         float previewHeight = previewRect.height();
    206         if (previewHeight == 0 || previewWidth == 0) {
    207             Log.e(TAG, "Invalid preview size: " + previewWidth + " x " + previewHeight);
    208             return;
    209         }
    210         float aspectRatio = previewWidth / previewHeight;
    211         aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
    212         if (aspectRatio != mAspectRatio) {
    213             setAspectRatio(aspectRatio);
    214         }
    215 
    216         RectF previewAreaBasedOnAspectRatio = mCaptureLayoutHelper.getPreviewRect();
    217         Matrix addtionalTransform = new Matrix();
    218         addtionalTransform.setRectToRect(previewRect, previewAreaBasedOnAspectRatio,
    219                 Matrix.ScaleToFit.CENTER);
    220         matrix.postConcat(addtionalTransform);
    221         mPreview.setTransform(matrix);
    222         updatePreviewArea(matrix);
    223     }
    224 
    225     /**
    226      * Calculates and updates the preview area rect using the latest transform
    227      * matrix.
    228      */
    229     private void updatePreviewArea(Matrix matrix) {
    230         mPreviewArea.set(0, 0, mWidth, mHeight);
    231         matrix.mapRect(mPreviewArea);
    232         onPreviewAreaChanged(mPreviewArea);
    233     }
    234 
    235     public void setOnLayoutChangeListener(OnLayoutChangeListener listener) {
    236         mOnLayoutChangeListener = listener;
    237     }
    238 
    239     public void setSurfaceTextureListener(TextureView.SurfaceTextureListener listener) {
    240         mSurfaceTextureListener = listener;
    241     }
    242 
    243     /**
    244      * Returns a transformation matrix that implements rotation that is
    245      * consistent with CaptureLayoutHelper and TextureViewHelper. The magical
    246      * invariant for CaptureLayoutHelper and TextureViewHelper that must be
    247      * obeyed is that the bounding box of the view must be EXACTLY the bounding
    248      * box of the surfaceDimensions AFTER the transformation has been applied.
    249      *
    250      * @param currentDisplayOrientation The current display orientation,
    251      *            measured counterclockwise from to the device's natural
    252      *            orientation (in degrees, always a multiple of 90, and between
    253      *            0 and 270, inclusive).
    254      * @param surfaceDimensions The dimensions of the
    255      *            {@link android.view.Surface} on which the preview image is
    256      *            being rendered. It usually only makes sense for the upper-left
    257      *            corner to be at the origin.
    258      * @param desiredBounds The boundaries within the
    259      *            {@link android.view.Surface} where the final image should
    260      *            appear. These can be used to translate and scale the output,
    261      *            but note that the image will be stretched to fit, possibly
    262      *            changing its aspect ratio.
    263      * @return The transform matrix that should be applied to the
    264      *         {@link android.view.Surface} in order for the image to display
    265      *         properly in the device's current orientation.
    266      */
    267     public Matrix getPreviewRotationalTransform(int currentDisplayOrientation,
    268             RectF surfaceDimensions,
    269             RectF desiredBounds) {
    270         if (surfaceDimensions.equals(desiredBounds)) {
    271             return new Matrix();
    272         }
    273 
    274         Matrix transform = new Matrix();
    275         transform.setRectToRect(surfaceDimensions, desiredBounds, Matrix.ScaleToFit.FILL);
    276 
    277         RectF normalRect = surfaceDimensions;
    278         // Bounding box of 90 or 270 degree rotation.
    279         RectF rotatedRect = new RectF(normalRect.width() / 2 - normalRect.height() / 2,
    280                 normalRect.height() / 2 - normalRect.width() / 2,
    281                 normalRect.width() / 2 + normalRect.height() / 2,
    282                 normalRect.height() / 2 + normalRect.width() / 2);
    283 
    284         OrientationManager.DeviceOrientation deviceOrientation =
    285                 OrientationManager.DeviceOrientation.from(currentDisplayOrientation);
    286 
    287         // This rotation code assumes that the aspect ratio of the content
    288         // (not of necessarily the surface) equals the aspect ratio of view that is receiving
    289         // the preview.  So, a 4:3 surface that contains 16:9 data will look correct as
    290         // long as the view is also 16:9.
    291         switch (deviceOrientation) {
    292             case CLOCKWISE_90:
    293                 transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL);
    294                 transform.preRotate(270, mWidth / 2, mHeight / 2);
    295                 break;
    296             case CLOCKWISE_180:
    297                 transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL);
    298                 transform.preRotate(180, mWidth / 2, mHeight / 2);
    299                 break;
    300             case CLOCKWISE_270:
    301                 transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL);
    302                 transform.preRotate(90, mWidth / 2, mHeight / 2);
    303                 break;
    304             case CLOCKWISE_0:
    305             default:
    306                 transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL);
    307                 break;
    308         }
    309 
    310         return transform;
    311     }
    312 
    313     /**
    314      * Updates the transform matrix based current width and height of
    315      * TextureView and preview stream aspect ratio.
    316      * <p>
    317      * If not {@code mAutoAdjustTransform}, this does nothing except return
    318      * {@code false}. In all other cases, it returns {@code true}, regardless of
    319      * whether the transform was changed.
    320      * </p>
    321      * In {@code mAutoAdjustTransform} and the CameraProvder is invalid, it is assumed
    322      * that the CaptureModule/PhotoModule is Camera2 API-based and must implements its
    323      * rotation via matrix transformation implemented in getPreviewRotationalTransform.
    324      *
    325      * @return Whether {@code mAutoAdjustTransform}.
    326      */
    327     private boolean updateTransform() {
    328         Log.v(TAG, "updateTransform");
    329         if (!mAutoAdjustTransform) {
    330             return false;
    331         }
    332 
    333         if (mAspectRatio == MATCH_SCREEN || mAspectRatio < 0 || mWidth == 0 || mHeight == 0) {
    334             return true;
    335         }
    336 
    337         Matrix matrix = new Matrix();
    338         CameraId cameraKey = mCameraProvider.getCurrentCameraId();
    339         int cameraId = -1;
    340 
    341         try {
    342             cameraId = cameraKey.getLegacyValue();
    343         } catch (UnsupportedOperationException ignored) {
    344             Log.e(TAG, "TransformViewHelper does not support Camera API2");
    345         }
    346 
    347 
    348         // Only apply this fix when Current Active Module is Photo module AND
    349         // Phone is Nexus4 The enhancement fix b/20694189 to original fix to
    350         // b/19271661 ensures that the fix should only be applied when:
    351         // 1) the phone is a Nexus4 which requires the specific workaround
    352         // 2) CaptureModule is enabled.
    353         // 3) the Camera Photo Mode Or Capture Intent Photo Mode is active
    354         if (ApiHelper.IS_NEXUS_4 && mAppController.getCameraFeatureConfig().isUsingCaptureModule()
    355                 && (mAppController.getCurrentModuleIndex() == mCameraModeId ||
    356                 mAppController.getCurrentModuleIndex() == mCaptureIntentModeId)) {
    357             Log.v(TAG, "Applying Photo Mode, Capture Module, Nexus-4 specific fix for b/19271661");
    358             mOrientation = CameraUtil.getDisplayRotation();
    359             matrix = getPreviewRotationalTransform(mOrientation,
    360                     new RectF(0, 0, mWidth, mHeight),
    361                     mCaptureLayoutHelper.getPreviewRect());
    362         } else if (cameraId >= 0) {
    363             // Otherwise, do the default, legacy action.
    364             CameraDeviceInfo.Characteristics info = mCameraProvider.getCharacteristics(cameraId);
    365             matrix = info.getPreviewTransform(mOrientation, new RectF(0, 0, mWidth, mHeight),
    366                     mCaptureLayoutHelper.getPreviewRect());
    367         } else {
    368             // Do Nothing
    369         }
    370 
    371         mPreview.setTransform(matrix);
    372         updatePreviewArea(matrix);
    373         return true;
    374     }
    375 
    376     private void onPreviewAreaChanged(final RectF previewArea) {
    377         // Notify listeners of preview area change
    378         final List<PreviewStatusListener.PreviewAreaChangedListener> listeners =
    379                 new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>(
    380                         mPreviewSizeChangedListeners);
    381         // This method can be called during layout pass. We post a Runnable so
    382         // that the callbacks won't happen during the layout pass.
    383         mPreview.post(new Runnable() {
    384             @Override
    385             public void run() {
    386                 for (PreviewStatusListener.PreviewAreaChangedListener listener : listeners) {
    387                     listener.onPreviewAreaChanged(previewArea);
    388                 }
    389             }
    390         });
    391     }
    392 
    393     /**
    394      * Returns a new copy of the preview area, to avoid internal data being
    395      * modified from outside of the class.
    396      */
    397     public RectF getPreviewArea() {
    398         return new RectF(mPreviewArea);
    399     }
    400 
    401     /**
    402      * Returns a copy of the area of the whole preview, including bits clipped
    403      * by the view
    404      */
    405     public RectF getTextureArea() {
    406 
    407         if (mPreview == null) {
    408             return new RectF();
    409         }
    410         Matrix matrix = new Matrix();
    411         RectF area = new RectF(0, 0, mWidth, mHeight);
    412         mPreview.getTransform(matrix).mapRect(area);
    413         return area;
    414     }
    415 
    416     public Bitmap getPreviewBitmap(int downsample) {
    417         RectF textureArea = getTextureArea();
    418         int width = (int) textureArea.width() / downsample;
    419         int height = (int) textureArea.height() / downsample;
    420         Bitmap preview = mPreview.getBitmap(width, height);
    421         return Bitmap.createBitmap(preview, 0, 0, width, height, mPreview.getTransform(null), true);
    422     }
    423 
    424     /**
    425      * Adds a listener that will get notified when the preview area changed.
    426      * This can be useful for UI elements or focus overlay to adjust themselves
    427      * according to the preview area change.
    428      * <p/>
    429      * Note that a listener will only be added once. A newly added listener will
    430      * receive a notification of current preview area immediately after being
    431      * added.
    432      * <p/>
    433      * This function should be called on the UI thread and listeners will be
    434      * notified on the UI thread.
    435      *
    436      * @param listener the listener that will get notified of preview area
    437      *            change
    438      */
    439     public void addPreviewAreaSizeChangedListener(
    440             PreviewStatusListener.PreviewAreaChangedListener listener) {
    441         if (listener != null && !mPreviewSizeChangedListeners.contains(listener)) {
    442             mPreviewSizeChangedListeners.add(listener);
    443             if (mPreviewArea.width() == 0 || mPreviewArea.height() == 0) {
    444                 listener.onPreviewAreaChanged(new RectF(0, 0, mWidth, mHeight));
    445             } else {
    446                 listener.onPreviewAreaChanged(new RectF(mPreviewArea));
    447             }
    448         }
    449     }
    450 
    451     /**
    452      * Removes a listener that gets notified when the preview area changed.
    453      *
    454      * @param listener the listener that gets notified of preview area change
    455      */
    456     public void removePreviewAreaSizeChangedListener(
    457             PreviewStatusListener.PreviewAreaChangedListener listener) {
    458         if (listener != null && mPreviewSizeChangedListeners.contains(listener)) {
    459             mPreviewSizeChangedListeners.remove(listener);
    460         }
    461     }
    462 
    463     @Override
    464     public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    465         // Workaround for b/11168275, see b/10981460 for more details
    466         if (mWidth != 0 && mHeight != 0) {
    467             // Re-apply transform matrix for new surface texture
    468             updateTransform();
    469         }
    470         if (mSurfaceTextureListener != null) {
    471             mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height);
    472         }
    473     }
    474 
    475     @Override
    476     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    477         if (mSurfaceTextureListener != null) {
    478             mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height);
    479         }
    480     }
    481 
    482     @Override
    483     public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    484         if (mSurfaceTextureListener != null) {
    485             mSurfaceTextureListener.onSurfaceTextureDestroyed(surface);
    486         }
    487         return false;
    488     }
    489 
    490     @Override
    491     public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    492         if (mSurfaceTextureListener != null) {
    493             mSurfaceTextureListener.onSurfaceTextureUpdated(surface);
    494         }
    495 
    496     }
    497 }
    498