PhotoUI.java revision 987ee64612e2510004fdf08536746c87234d01c1
1/* 2 * Copyright (C) 2012 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.app.Dialog; 20import android.content.DialogInterface; 21import android.graphics.Bitmap; 22import android.graphics.Matrix; 23import android.graphics.RectF; 24import android.graphics.SurfaceTexture; 25import android.hardware.Camera.Face; 26import android.os.AsyncTask; 27import android.view.GestureDetector; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.ViewGroup; 31import android.widget.FrameLayout; 32import android.widget.ImageView; 33 34import com.android.camera.debug.DebugPropertyHelper; 35import com.android.camera.debug.Log; 36import com.android.camera.ui.CountDownView; 37import com.android.camera.ui.FaceView; 38import com.android.camera.ui.PreviewOverlay; 39import com.android.camera.ui.PreviewStatusListener; 40import com.android.camera.ui.focus.FocusRing; 41import com.android.camera.util.ApiHelper; 42import com.android.camera.util.CameraUtil; 43import com.android.camera.util.GservicesHelper; 44import com.android.camera.widget.AspectRatioDialogLayout; 45import com.android.camera.widget.AspectRatioSelector; 46import com.android.camera.widget.LocationDialogLayout; 47import com.android.camera2.R; 48import com.android.ex.camera2.portability.CameraAgent; 49import com.android.ex.camera2.portability.CameraCapabilities; 50import com.android.ex.camera2.portability.CameraSettings; 51 52public class PhotoUI implements PreviewStatusListener, 53 CameraAgent.CameraFaceDetectionCallback, PreviewStatusListener.PreviewAreaChangedListener { 54 55 private static final Log.Tag TAG = new Log.Tag("PhotoUI"); 56 private static final int DOWN_SAMPLE_FACTOR = 4; 57 private static final float UNSET = 0f; 58 59 private final PreviewOverlay mPreviewOverlay; 60 private final FocusRing mFocusRing; 61 private final CameraActivity mActivity; 62 private final PhotoController mController; 63 64 private final View mRootView; 65 private Dialog mDialog = null; 66 67 // TODO: Remove face view logic if UX does not bring it back within a month. 68 private final FaceView mFaceView; 69 private DecodeImageForReview mDecodeTaskForReview = null; 70 71 private float mZoomMax; 72 73 private int mPreviewWidth = 0; 74 private int mPreviewHeight = 0; 75 private float mAspectRatio = UNSET; 76 77 private ImageView mIntentReviewImageView; 78 79 private final GestureDetector.OnGestureListener mPreviewGestureListener 80 = new GestureDetector.SimpleOnGestureListener() { 81 @Override 82 public boolean onSingleTapUp(MotionEvent ev) { 83 mController.onSingleTapUp(null, (int) ev.getX(), (int) ev.getY()); 84 return true; 85 } 86 }; 87 private final DialogInterface.OnDismissListener mOnDismissListener 88 = new DialogInterface.OnDismissListener() { 89 @Override 90 public void onDismiss(DialogInterface dialog) { 91 mDialog = null; 92 } 93 }; 94 private Runnable mRunnableForNextFrame = null; 95 private final CountDownView mCountdownView; 96 97 @Override 98 public GestureDetector.OnGestureListener getGestureListener() { 99 return mPreviewGestureListener; 100 } 101 102 @Override 103 public View.OnTouchListener getTouchListener() { 104 return null; 105 } 106 107 @Override 108 public void onPreviewLayoutChanged(View v, int left, int top, int right, 109 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 110 int width = right - left; 111 int height = bottom - top; 112 if (mPreviewWidth != width || mPreviewHeight != height) { 113 mPreviewWidth = width; 114 mPreviewHeight = height; 115 } 116 } 117 118 @Override 119 public boolean shouldAutoAdjustTransformMatrixOnLayout() { 120 return true; 121 } 122 123 @Override 124 public boolean shouldAutoAdjustBottomBar() { 125 return true; 126 } 127 128 @Override 129 public void onPreviewFlipped() { 130 mController.updateCameraOrientation(); 131 } 132 133 /** 134 * Sets the runnable to run when the next frame comes in. 135 */ 136 public void setRunnableForNextFrame(Runnable runnable) { 137 mRunnableForNextFrame = runnable; 138 } 139 140 /** 141 * Starts the countdown timer. 142 * 143 * @param sec seconds to countdown 144 */ 145 public void startCountdown(int sec) { 146 mCountdownView.startCountDown(sec); 147 } 148 149 /** 150 * Sets a listener that gets notified when the countdown is finished. 151 */ 152 public void setCountdownFinishedListener(CountDownView.OnCountDownStatusListener listener) { 153 mCountdownView.setCountDownStatusListener(listener); 154 } 155 156 /** 157 * Returns whether the countdown is on-going. 158 */ 159 public boolean isCountingDown() { 160 return mCountdownView.isCountingDown(); 161 } 162 163 /** 164 * Cancels the on-going countdown, if any. 165 */ 166 public void cancelCountDown() { 167 mCountdownView.cancelCountDown(); 168 } 169 170 @Override 171 public void onPreviewAreaChanged(RectF previewArea) { 172 if (mFaceView != null) { 173 mFaceView.onPreviewAreaChanged(previewArea); 174 } 175 mCountdownView.onPreviewAreaChanged(previewArea); 176 } 177 178 private class DecodeTask extends AsyncTask<Void, Void, Bitmap> { 179 private final byte [] mData; 180 private final int mOrientation; 181 private final boolean mMirror; 182 183 public DecodeTask(byte[] data, int orientation, boolean mirror) { 184 mData = data; 185 mOrientation = orientation; 186 mMirror = mirror; 187 } 188 189 @Override 190 protected Bitmap doInBackground(Void... params) { 191 // Decode image in background. 192 Bitmap bitmap = CameraUtil.downSample(mData, DOWN_SAMPLE_FACTOR); 193 if (mOrientation != 0 || mMirror) { 194 Matrix m = new Matrix(); 195 if (mMirror) { 196 // Flip horizontally 197 m.setScale(-1f, 1f); 198 } 199 m.preRotate(mOrientation); 200 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, 201 false); 202 } 203 return bitmap; 204 } 205 } 206 207 private class DecodeImageForReview extends DecodeTask { 208 public DecodeImageForReview(byte[] data, int orientation, boolean mirror) { 209 super(data, orientation, mirror); 210 } 211 212 @Override 213 protected void onPostExecute(Bitmap bitmap) { 214 if (isCancelled()) { 215 return; 216 } 217 218 mIntentReviewImageView.setImageBitmap(bitmap); 219 showIntentReviewImageView(); 220 221 mDecodeTaskForReview = null; 222 } 223 } 224 225 public PhotoUI(CameraActivity activity, PhotoController controller, View parent) { 226 mActivity = activity; 227 mController = controller; 228 mRootView = parent; 229 230 ViewGroup moduleRoot = (ViewGroup) mRootView.findViewById(R.id.module_layout); 231 mActivity.getLayoutInflater().inflate(R.layout.photo_module, 232 moduleRoot, true); 233 initIndicators(); 234 mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring); 235 mPreviewOverlay = (PreviewOverlay) mRootView.findViewById(R.id.preview_overlay); 236 mCountdownView = (CountDownView) mRootView.findViewById(R.id.count_down_view); 237 // Show faces if we are in debug mode. 238 if (DebugPropertyHelper.showCaptureDebugUI()) { 239 mFaceView = (FaceView) mRootView.findViewById(R.id.face_view); 240 } else { 241 mFaceView = null; 242 } 243 244 if (mController.isImageCaptureIntent()) { 245 initIntentReviewImageView(); 246 } 247 } 248 249 private void initIntentReviewImageView() { 250 mIntentReviewImageView = (ImageView) mRootView.findViewById(R.id.intent_review_imageview); 251 mActivity.getCameraAppUI().addPreviewAreaChangedListener( 252 new PreviewStatusListener.PreviewAreaChangedListener() { 253 @Override 254 public void onPreviewAreaChanged(RectF previewArea) { 255 FrameLayout.LayoutParams params = 256 (FrameLayout.LayoutParams) mIntentReviewImageView.getLayoutParams(); 257 params.width = (int) previewArea.width(); 258 params.height = (int) previewArea.height(); 259 params.setMargins((int) previewArea.left, (int) previewArea.top, 0, 0); 260 mIntentReviewImageView.setLayoutParams(params); 261 } 262 }); 263 } 264 265 /** 266 * Show the image review over the live preview for intent captures. 267 */ 268 public void showIntentReviewImageView() { 269 if (mIntentReviewImageView != null) { 270 mIntentReviewImageView.setVisibility(View.VISIBLE); 271 } 272 } 273 274 /** 275 * Hide the image review over the live preview for intent captures. 276 */ 277 public void hideIntentReviewImageView() { 278 if (mIntentReviewImageView != null) { 279 mIntentReviewImageView.setVisibility(View.INVISIBLE); 280 } 281 } 282 283 284 public FocusRing getFocusRing() { 285 return mFocusRing; 286 } 287 288 public void updatePreviewAspectRatio(float aspectRatio) { 289 if (aspectRatio <= 0) { 290 Log.e(TAG, "Invalid aspect ratio: " + aspectRatio); 291 return; 292 } 293 if (aspectRatio < 1f) { 294 aspectRatio = 1f / aspectRatio; 295 } 296 297 if (mAspectRatio != aspectRatio) { 298 mAspectRatio = aspectRatio; 299 // Update transform matrix with the new aspect ratio. 300 mController.updatePreviewAspectRatio(mAspectRatio); 301 } 302 } 303 304 @Override 305 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 306 mController.onPreviewUIReady(); 307 } 308 309 @Override 310 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 311 // Ignored, Camera does all the work for us 312 } 313 314 @Override 315 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 316 mController.onPreviewUIDestroyed(); 317 return true; 318 } 319 320 @Override 321 public void onSurfaceTextureUpdated(SurfaceTexture surface) { 322 if (mRunnableForNextFrame != null) { 323 mRootView.post(mRunnableForNextFrame); 324 mRunnableForNextFrame = null; 325 } 326 } 327 328 public View getRootView() { 329 return mRootView; 330 } 331 332 private void initIndicators() { 333 // TODO init toggle buttons on bottom bar here 334 } 335 336 public void onCameraOpened(CameraCapabilities capabilities, CameraSettings settings) { 337 initializeZoom(capabilities, settings); 338 } 339 340 public void animateCapture(final byte[] jpegData, int orientation, boolean mirror) { 341 // Decode jpeg byte array and then animate the jpeg 342 DecodeTask task = new DecodeTask(jpegData, orientation, mirror); 343 task.execute(); 344 } 345 346 // called from onResume but only the first time 347 public void initializeFirstTime() { 348 349 } 350 351 // called from onResume every other time 352 public void initializeSecondTime(CameraCapabilities capabilities, CameraSettings settings) { 353 initializeZoom(capabilities, settings); 354 if (mController.isImageCaptureIntent()) { 355 hidePostCaptureAlert(); 356 } 357 } 358 359 public void showLocationAndAspectRatioDialog( 360 final PhotoModule.LocationDialogCallback locationCallback, 361 final PhotoModule.AspectRatioDialogCallback aspectRatioDialogCallback) { 362 setDialog(new Dialog(mActivity, 363 android.R.style.Theme_Black_NoTitleBar_Fullscreen)); 364 final LocationDialogLayout locationDialogLayout = (LocationDialogLayout) mActivity 365 .getLayoutInflater().inflate(R.layout.location_dialog_layout, null); 366 locationDialogLayout.setLocationTaggingSelectionListener( 367 new LocationDialogLayout.LocationTaggingSelectionListener() { 368 @Override 369 public void onLocationTaggingSelected(boolean selected) { 370 // Update setting. 371 locationCallback.onLocationTaggingSelected(selected); 372 373 if (showAspectRatioDialogOnThisDevice()) { 374 // Go to next page. 375 showAspectRatioDialog(aspectRatioDialogCallback, mDialog); 376 } else { 377 // If we don't want to show the aspect ratio dialog, 378 // dismiss the dialog right after the user chose the 379 // location setting. 380 if (mDialog != null) { 381 mDialog.dismiss(); 382 } 383 } 384 } 385 }); 386 mDialog.setContentView(locationDialogLayout, new ViewGroup.LayoutParams( 387 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 388 mDialog.show(); 389 } 390 391 /** 392 * Dismisses previous dialog if any, sets current dialog to the given dialog, 393 * and set the on dismiss listener for the given dialog. 394 * @param dialog dialog to show 395 */ 396 private void setDialog(Dialog dialog) { 397 if (mDialog != null) { 398 mDialog.setOnDismissListener(null); 399 mDialog.dismiss(); 400 } 401 mDialog = dialog; 402 if (mDialog != null) { 403 mDialog.setOnDismissListener(mOnDismissListener); 404 } 405 } 406 407 /** 408 * @return Whether the dialog was shown. 409 */ 410 public boolean showAspectRatioDialog(final PhotoModule.AspectRatioDialogCallback callback) { 411 if (showAspectRatioDialogOnThisDevice()) { 412 setDialog(new Dialog(mActivity, android.R.style.Theme_Black_NoTitleBar_Fullscreen)); 413 showAspectRatioDialog(callback, mDialog); 414 return true; 415 } else { 416 return false; 417 } 418 } 419 420 private boolean showAspectRatioDialog(final PhotoModule.AspectRatioDialogCallback callback, 421 final Dialog aspectRatioDialog) { 422 if (aspectRatioDialog == null) { 423 Log.e(TAG, "Dialog for aspect ratio is null."); 424 return false; 425 } 426 final AspectRatioDialogLayout aspectRatioDialogLayout = 427 (AspectRatioDialogLayout) mActivity 428 .getLayoutInflater().inflate(R.layout.aspect_ratio_dialog_layout, null); 429 aspectRatioDialogLayout.initialize( 430 new AspectRatioDialogLayout.AspectRatioChangedListener() { 431 @Override 432 public void onAspectRatioChanged(AspectRatioSelector.AspectRatio aspectRatio) { 433 // callback to set picture size. 434 callback.onAspectRatioSelected(aspectRatio, new Runnable() { 435 @Override 436 public void run() { 437 if (mDialog != null) { 438 mDialog.dismiss(); 439 } 440 } 441 }); 442 } 443 }, callback.getCurrentAspectRatio()); 444 aspectRatioDialog.setContentView(aspectRatioDialogLayout, new ViewGroup.LayoutParams( 445 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 446 aspectRatioDialog.show(); 447 return true; 448 } 449 450 /** 451 * @return Whether this is a device that we should show the aspect ratio 452 * intro dialog on. 453 */ 454 private boolean showAspectRatioDialogOnThisDevice() { 455 // We only want to show that dialog on N4/N5/N6 456 // Don't show if using API2 portability, b/17462976 457 return !GservicesHelper.useCamera2ApiThroughPortabilityLayer(mActivity) && 458 (ApiHelper.IS_NEXUS_4 || ApiHelper.IS_NEXUS_5 || ApiHelper.IS_NEXUS_6); 459 } 460 461 public void initializeZoom(CameraCapabilities capabilities, CameraSettings settings) { 462 if ((capabilities == null) || settings == null || 463 !capabilities.supports(CameraCapabilities.Feature.ZOOM)) { 464 return; 465 } 466 mZoomMax = capabilities.getMaxZoomRatio(); 467 // Currently we use immediate zoom for fast zooming to get better UX and 468 // there is no plan to take advantage of the smooth zoom. 469 // TODO: Need to setup a path to AppUI to do this 470 mPreviewOverlay.setupZoom(mZoomMax, settings.getCurrentZoomRatio(), 471 new ZoomChangeListener()); 472 } 473 474 public void animateFlash() { 475 mController.startPreCaptureAnimation(); 476 } 477 478 public boolean onBackPressed() { 479 // In image capture mode, back button should: 480 // 1) if there is any popup, dismiss them, 2) otherwise, get out of 481 // image capture 482 if (mController.isImageCaptureIntent()) { 483 mController.onCaptureCancelled(); 484 return true; 485 } else if (!mController.isCameraIdle()) { 486 // ignore backs while we're taking a picture 487 return true; 488 } else { 489 return false; 490 } 491 } 492 493 protected void showCapturedImageForReview(byte[] jpegData, int orientation, boolean mirror) { 494 mDecodeTaskForReview = new DecodeImageForReview(jpegData, orientation, mirror); 495 mDecodeTaskForReview.execute(); 496 497 mActivity.getCameraAppUI().transitionToIntentReviewLayout(); 498 pauseFaceDetection(); 499 } 500 501 protected void hidePostCaptureAlert() { 502 if (mDecodeTaskForReview != null) { 503 mDecodeTaskForReview.cancel(true); 504 } 505 resumeFaceDetection(); 506 } 507 508 public void setDisplayOrientation(int orientation) { 509 if (mFaceView != null) { 510 mFaceView.setDisplayOrientation(orientation); 511 } 512 } 513 514 private class ZoomChangeListener implements PreviewOverlay.OnZoomChangedListener { 515 @Override 516 public void onZoomValueChanged(float ratio) { 517 mController.onZoomChanged(ratio); 518 } 519 520 @Override 521 public void onZoomStart() { 522 } 523 524 @Override 525 public void onZoomEnd() { 526 } 527 } 528 529 public void setSwipingEnabled(boolean enable) { 530 mActivity.setSwipingEnabled(enable); 531 } 532 533 public void onPause() { 534 if (mFaceView != null) { 535 mFaceView.clear(); 536 } 537 if (mDialog != null) { 538 mDialog.dismiss(); 539 } 540 // recalculate aspect ratio when restarting. 541 mAspectRatio = 0.0f; 542 } 543 544 public void clearFaces() { 545 if (mFaceView != null) { 546 mFaceView.clear(); 547 } 548 } 549 550 public void pauseFaceDetection() { 551 if (mFaceView != null) { 552 mFaceView.pause(); 553 } 554 } 555 556 public void resumeFaceDetection() { 557 if (mFaceView != null) { 558 mFaceView.resume(); 559 } 560 } 561 562 public void onStartFaceDetection(int orientation, boolean mirror) { 563 if (mFaceView != null) { 564 mFaceView.clear(); 565 mFaceView.setVisibility(View.VISIBLE); 566 mFaceView.setDisplayOrientation(orientation); 567 mFaceView.setMirror(mirror); 568 mFaceView.resume(); 569 } 570 } 571 572 @Override 573 public void onFaceDetection(Face[] faces, CameraAgent.CameraProxy camera) { 574 if (mFaceView != null) { 575 mFaceView.setFaces(faces); 576 } 577 } 578 579} 580