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