2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.camera;
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;
27 import com.android.camera.app.CameraProvider;
28 import com.android.camera.app.OrientationManager;
29 import com.android.camera.debug.Log;
30 import com.android.camera.device.CameraId;
31 import com.android.camera.ui.PreviewStatusListener;
32 import com.android.camera.util.ApiHelper;
33 import com.android.camera.util.CameraUtil;
34 import com.android.ex.camera2.portability.CameraDeviceInfo;
36 import java.util.ArrayList;
37 import java.util.List;
40 * This class aims to automate TextureView transform change and notify listeners
41 * (e.g. bottom bar) of the preview size change.
43 public class TextureViewHelper implements TextureView.SurfaceTextureListener,
44 OnLayoutChangeListener {
46 private static final Log.Tag TAG = new Log.Tag("TexViewHelper");
47 public static final float MATCH_SCREEN = 0f;
48 private static final int UNSET = -1;
49 private final TextureView mPreview;
50 private final CameraProvider mCameraProvider;
51 private int mWidth = 0;
52 private int mHeight = 0;
53 private RectF mPreviewArea = new RectF();
54 private float mAspectRatio = MATCH_SCREEN;
55 private boolean mAutoAdjustTransform = true;
56 private TextureView.SurfaceTextureListener mSurfaceTextureListener;
58 private final ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>
59 mAspectRatioChangedListeners =
60 new ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>();
62 private final ArrayList<PreviewStatusListener.PreviewAreaChangedListener>
63 mPreviewSizeChangedListeners =
64 new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>();
65 private OnLayoutChangeListener mOnLayoutChangeListener = null;
66 private CaptureLayoutHelper mCaptureLayoutHelper = null;
67 private int mOrientation = UNSET;
69 public TextureViewHelper(TextureView preview, CaptureLayoutHelper helper,
70 CameraProvider cameraProvider) {
72 mCameraProvider = cameraProvider;
73 mPreview.addOnLayoutChangeListener(this);
74 mPreview.setSurfaceTextureListener(this);
75 mCaptureLayoutHelper = helper;
79 * If auto adjust transform is enabled, when there is a layout change, the
80 * transform matrix will be automatically adjusted based on the preview
81 * stream aspect ratio in the new layout.
83 * @param enable whether or not auto adjustment should be enabled
85 public void setAutoAdjustTransform(boolean enable) {
86 mAutoAdjustTransform = enable;
90 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
91 int oldTop, int oldRight, int oldBottom) {
92 Log.v(TAG, "onLayoutChange");
93 int width = right - left;
94 int height = bottom - top;
95 int rotation = CameraUtil.getDisplayRotation();
96 if (mWidth != width || mHeight != height || mOrientation != rotation) {
99 mOrientation = rotation;
100 if (!updateTransform()) {
104 if (mOnLayoutChangeListener != null) {
105 mOnLayoutChangeListener.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop,
106 oldRight, oldBottom);
111 * Transforms the preview with the identity matrix, ensuring there is no
112 * scaling on the preview. It also calls onPreviewSizeChanged, to trigger
113 * any necessary preview size changing callbacks.
115 public void clearTransform() {
116 mPreview.setTransform(new Matrix());
117 mPreviewArea.set(0, 0, mWidth, mHeight);
118 onPreviewAreaChanged(mPreviewArea);
119 setAspectRatio(MATCH_SCREEN);
122 public void updateAspectRatio(float aspectRatio) {
123 Log.v(TAG, "updateAspectRatio " + aspectRatio);
124 if (aspectRatio <= 0) {
125 Log.e(TAG, "Invalid aspect ratio: " + aspectRatio);
128 if (aspectRatio < 1f) {
129 aspectRatio = 1f / aspectRatio;
131 setAspectRatio(aspectRatio);
135 private void setAspectRatio(float aspectRatio) {
136 Log.v(TAG, "setAspectRatio: " + aspectRatio);
137 if (mAspectRatio != aspectRatio) {
138 Log.v(TAG, "aspect ratio changed from: " + mAspectRatio);
139 mAspectRatio = aspectRatio;
140 onAspectRatioChanged();
144 private void onAspectRatioChanged() {
145 mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio);
146 for (PreviewStatusListener.PreviewAspectRatioChangedListener listener
147 : mAspectRatioChangedListeners) {
148 listener.onPreviewAspectRatioChanged(mAspectRatio);
152 public void addAspectRatioChangedListener(
153 PreviewStatusListener.PreviewAspectRatioChangedListener listener) {
154 if (listener != null && !mAspectRatioChangedListeners.contains(listener)) {
155 mAspectRatioChangedListeners.add(listener);
160 * This returns the rect that is available to display the preview, and
165 public RectF getFullscreenRect() {
166 return mCaptureLayoutHelper.getFullscreenRect();
170 * This takes a matrix to apply to the texture view and uses the screen
171 * aspect ratio as the target aspect ratio
173 * @param matrix the matrix to apply
174 * @param aspectRatio the aspectRatio that the preview should be
176 public void updateTransformFullScreen(Matrix matrix, float aspectRatio) {
177 aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
178 if (aspectRatio != mAspectRatio) {
179 setAspectRatio(aspectRatio);
182 mPreview.setTransform(matrix);
183 mPreviewArea = mCaptureLayoutHelper.getPreviewRect();
184 onPreviewAreaChanged(mPreviewArea);
188 public void updateTransform(Matrix matrix) {
189 RectF previewRect = new RectF(0, 0, mWidth, mHeight);
190 matrix.mapRect(previewRect);
192 float previewWidth = previewRect.width();
193 float previewHeight = previewRect.height();
194 if (previewHeight == 0 || previewWidth == 0) {
195 Log.e(TAG, "Invalid preview size: " + previewWidth + " x " + previewHeight);
198 float aspectRatio = previewWidth / previewHeight;
199 aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
200 if (aspectRatio != mAspectRatio) {
201 setAspectRatio(aspectRatio);
204 RectF previewAreaBasedOnAspectRatio = mCaptureLayoutHelper.getPreviewRect();
205 Matrix addtionalTransform = new Matrix();
206 addtionalTransform.setRectToRect(previewRect, previewAreaBasedOnAspectRatio,
207 Matrix.ScaleToFit.CENTER);
208 matrix.postConcat(addtionalTransform);
209 mPreview.setTransform(matrix);
210 updatePreviewArea(matrix);
214 * Calculates and updates the preview area rect using the latest transform
217 private void updatePreviewArea(Matrix matrix) {
218 mPreviewArea.set(0, 0, mWidth, mHeight);
219 matrix.mapRect(mPreviewArea);
220 onPreviewAreaChanged(mPreviewArea);
223 public void setOnLayoutChangeListener(OnLayoutChangeListener listener) {
224 mOnLayoutChangeListener = listener;
227 public void setSurfaceTextureListener(TextureView.SurfaceTextureListener listener) {
228 mSurfaceTextureListener = listener;
232 * Returns a transformation matrix that implements rotation that is
233 * consistent with CaptureLayoutHelper and TextureViewHelper. The magical
234 * invariant for CaptureLayoutHelper and TextureViewHelper that must be
235 * obeyed is that the bounding box of the view must be EXACTLY the bounding
236 * box of the surfaceDimensions AFTER the transformation has been applied.
238 * @param currentDisplayOrientation The current display orientation,
239 * measured counterclockwise from to the device's natural
240 * orientation (in degrees, always a multiple of 90, and between
241 * 0 and 270, inclusive).
242 * @param surfaceDimensions The dimensions of the
243 * {@link android.view.Surface} on which the preview image is
244 * being rendered. It usually only makes sense for the upper-left
245 * corner to be at the origin.
246 * @param desiredBounds The boundaries within the
247 * {@link android.view.Surface} where the final image should
248 * appear. These can be used to translate and scale the output,
249 * but note that the image will be stretched to fit, possibly
250 * changing its aspect ratio.
251 * @return The transform matrix that should be applied to the
252 * {@link android.view.Surface} in order for the image to display
253 * properly in the device's current orientation.
255 public Matrix getPreviewRotationalTransform(int currentDisplayOrientation,
256 RectF surfaceDimensions,
257 RectF desiredBounds) {
258 if (surfaceDimensions.equals(desiredBounds)) {
262 Matrix transform = new Matrix();
263 transform.setRectToRect(surfaceDimensions, desiredBounds, Matrix.ScaleToFit.FILL);
265 RectF normalRect = surfaceDimensions;
266 // Bounding box of 90 or 270 degree rotation.
267 RectF rotatedRect = new RectF(normalRect.width() / 2 - normalRect.height() / 2,
268 normalRect.height() / 2 - normalRect.width() / 2,
269 normalRect.width() / 2 + normalRect.height() / 2,
270 normalRect.height() / 2 + normalRect.width() / 2);
272 OrientationManager.DeviceOrientation deviceOrientation =
273 OrientationManager.DeviceOrientation.from(currentDisplayOrientation);
275 // This rotation code assumes that the aspect ratio of the content
276 // (not of necessarily the surface) equals the aspect ratio of view that is receiving
277 // the preview. So, a 4:3 surface that contains 16:9 data will look correct as
278 // long as the view is also 16:9.
279 switch (deviceOrientation) {
281 transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL);
282 transform.preRotate(270, mWidth / 2, mHeight / 2);
285 transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL);
286 transform.preRotate(180, mWidth / 2, mHeight / 2);
289 transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL);
290 transform.preRotate(90, mWidth / 2, mHeight / 2);
294 transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL);
302 * Updates the transform matrix based current width and height of
303 * TextureView and preview stream aspect ratio.
305 * If not {@code mAutoAdjustTransform}, this does nothing except return
306 * {@code false}. In all other cases, it returns {@code true}, regardless of
307 * whether the transform was changed.
309 * In {@code mAutoAdjustTransform} and the CameraProvder is invalid, it is assumed
310 * that the CaptureModule/PhotoModule is Camera2 API-based and must implements its
311 * rotation via matrix transformation implemented in getPreviewRotationalTransform.
313 * @return Whether {@code mAutoAdjustTransform}.
315 private boolean updateTransform() {
316 Log.v(TAG, "updateTransform");
317 if (!mAutoAdjustTransform) {
321 if (mAspectRatio == MATCH_SCREEN || mAspectRatio < 0 || mWidth == 0 || mHeight == 0) {
326 CameraId cameraKey = mCameraProvider.getCurrentCameraId();
330 cameraId = cameraKey.getLegacyValue();
331 } catch (UnsupportedOperationException ignored) {
332 Log.e(TAG, "TransformViewHelper does not support Camera API2");
335 if (cameraId >= 0 && !ApiHelper.IS_NEXUS_4) {
336 CameraDeviceInfo.Characteristics info = mCameraProvider.getCharacteristics(cameraId);
337 matrix = info.getPreviewTransform(mOrientation, new RectF(0, 0, mWidth, mHeight),
338 mCaptureLayoutHelper.getPreviewRect());
341 "CameraProvider Invalid. Implementation rotation via Matrix Transformation."
342 + " Expected for Camera2 Implementations.");
343 // Assumed at this point, we are in a Camera2-based implementation.
344 mOrientation = CameraUtil.getDisplayRotation();
345 matrix = getPreviewRotationalTransform(mOrientation, new RectF(0, 0, mWidth, mHeight),
346 mCaptureLayoutHelper.getPreviewRect());
349 mPreview.setTransform(matrix);
350 updatePreviewArea(matrix);
354 private void onPreviewAreaChanged(final RectF previewArea) {
355 // Notify listeners of preview area change
356 final List<PreviewStatusListener.PreviewAreaChangedListener> listeners =
357 new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>(
358 mPreviewSizeChangedListeners);
359 // This method can be called during layout pass. We post a Runnable so
360 // that the callbacks won't happen during the layout pass.
361 mPreview.post(new Runnable() {
364 for (PreviewStatusListener.PreviewAreaChangedListener listener : listeners) {
365 listener.onPreviewAreaChanged(previewArea);
372 * Returns a new copy of the preview area, to avoid internal data being
373 * modified from outside of the class.
375 public RectF getPreviewArea() {
376 return new RectF(mPreviewArea);
380 * Returns a copy of the area of the whole preview, including bits clipped
383 public RectF getTextureArea() {
385 if (mPreview == null) {
388 Matrix matrix = new Matrix();
389 RectF area = new RectF(0, 0, mWidth, mHeight);
390 mPreview.getTransform(matrix).mapRect(area);
394 public Bitmap getPreviewBitmap(int downsample) {
395 RectF textureArea = getTextureArea();
396 int width = (int) textureArea.width() / downsample;
397 int height = (int) textureArea.height() / downsample;
398 Bitmap preview = mPreview.getBitmap(width, height);
399 return Bitmap.createBitmap(preview, 0, 0, width, height, mPreview.getTransform(null), true);
403 * Adds a listener that will get notified when the preview area changed.
404 * This can be useful for UI elements or focus overlay to adjust themselves
405 * according to the preview area change.
407 * Note that a listener will only be added once. A newly added listener will
408 * receive a notification of current preview area immediately after being
411 * This function should be called on the UI thread and listeners will be
412 * notified on the UI thread.
414 * @param listener the listener that will get notified of preview area
417 public void addPreviewAreaSizeChangedListener(
418 PreviewStatusListener.PreviewAreaChangedListener listener) {
419 if (listener != null && !mPreviewSizeChangedListeners.contains(listener)) {
420 mPreviewSizeChangedListeners.add(listener);
421 if (mPreviewArea.width() == 0 || mPreviewArea.height() == 0) {
422 listener.onPreviewAreaChanged(new RectF(0, 0, mWidth, mHeight));
424 listener.onPreviewAreaChanged(new RectF(mPreviewArea));
430 * Removes a listener that gets notified when the preview area changed.
432 * @param listener the listener that gets notified of preview area change
434 public void removePreviewAreaSizeChangedListener(
435 PreviewStatusListener.PreviewAreaChangedListener listener) {
436 if (listener != null && mPreviewSizeChangedListeners.contains(listener)) {
437 mPreviewSizeChangedListeners.remove(listener);
442 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
443 // Workaround for b/11168275, see b/10981460 for more details
444 if (mWidth != 0 && mHeight != 0) {
445 // Re-apply transform matrix for new surface texture
448 if (mSurfaceTextureListener != null) {
449 mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height);
454 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
455 if (mSurfaceTextureListener != null) {
456 mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height);
461 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
462 if (mSurfaceTextureListener != null) {
463 mSurfaceTextureListener.onSurfaceTextureDestroyed(surface);
469 public void onSurfaceTextureUpdated(SurfaceTexture surface) {
470 if (mSurfaceTextureListener != null) {
471 mSurfaceTextureListener.onSurfaceTextureUpdated(surface);