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