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 17package com.android.camera; 18 19import android.graphics.Bitmap; 20import android.graphics.Matrix; 21import android.graphics.RectF; 22import android.graphics.SurfaceTexture; 23import android.view.TextureView; 24import android.view.View; 25import android.view.View.OnLayoutChangeListener; 26 27import com.android.camera.app.AppController; 28import com.android.camera.app.CameraProvider; 29import com.android.camera.app.OrientationManager; 30import com.android.camera.debug.Log; 31import com.android.camera.device.CameraId; 32import com.android.camera.ui.PreviewStatusListener; 33import com.android.camera.util.ApiHelper; 34import com.android.camera.util.CameraUtil; 35import com.android.camera2.R; 36import com.android.ex.camera2.portability.CameraDeviceInfo; 37 38import java.util.ArrayList; 39import 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 */ 45public 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