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 TextureViewHelper(TextureView preview, CaptureLayoutHelper helper, CameraProvider cameraProvider, AppController appController)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 */ setAutoAdjustTransform(boolean enable)97 public void setAutoAdjustTransform(boolean enable) { 98 mAutoAdjustTransform = enable; 99 } 100 101 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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 */ clearTransform()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 updateAspectRatio(float aspectRatio)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 setAspectRatio(float aspectRatio)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 onAspectRatioChanged()156 private void onAspectRatioChanged() { 157 mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio); 158 for (PreviewStatusListener.PreviewAspectRatioChangedListener listener 159 : mAspectRatioChangedListeners) { 160 listener.onPreviewAspectRatioChanged(mAspectRatio); 161 } 162 } 163 addAspectRatioChangedListener( PreviewStatusListener.PreviewAspectRatioChangedListener listener)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 */ getFullscreenRect()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 */ updateTransformFullScreen(Matrix matrix, float aspectRatio)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