CameraAppUI.java revision e0aff89f3e05eb6008651b290ba79d484de55970
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.app; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.Matrix; 22import android.graphics.SurfaceTexture; 23import android.util.Log; 24import android.view.GestureDetector; 25import android.view.LayoutInflater; 26import android.view.MotionEvent; 27import android.view.TextureView; 28import android.view.View; 29import android.view.ViewConfiguration; 30import android.view.ViewGroup; 31import android.widget.FrameLayout; 32import android.widget.FrameLayout.LayoutParams; 33import android.widget.ImageView; 34 35import com.android.camera.AnimationManager; 36import com.android.camera.filmstrip.FilmstripContentPanel; 37import com.android.camera.ui.BottomBar; 38import com.android.camera.widget.FilmstripLayout; 39import com.android.camera.ui.MainActivityLayout; 40import com.android.camera.ui.ModeListView; 41import com.android.camera.ui.ModeTransitionView; 42import com.android.camera.ui.PreviewOverlay; 43import com.android.camera.ui.PreviewStatusListener; 44import com.android.camera2.R; 45 46/** 47 * CameraAppUI centralizes control of views shared across modules. Whereas module 48 * specific views will be handled in each Module UI. For example, we can now 49 * bring the flash animation and capture animation up from each module to app 50 * level, as these animations are largely the same for all modules. 51 * 52 * This class also serves to disambiguate touch events. It recognizes all the 53 * swipe gestures that happen on the preview by attaching a touch listener to 54 * a full-screen view on top of preview TextureView. Since CameraAppUI has knowledge 55 * of how swipe from each direction should be handled, it can then redirect these 56 * events to appropriate recipient views. 57 */ 58public class CameraAppUI implements ModeListView.ModeSwitchListener, 59 TextureView.SurfaceTextureListener { 60 61 /** 62 * The bottom controls on the filmstrip. 63 */ 64 public static interface BottomControls { 65 /** Values for the view state of the button. */ 66 public final int VIEW_NONE = 0; 67 public final int VIEW_PHOTO_SPHERE = 1; 68 public final int VIEW_RGBZ = 2; 69 70 /** 71 * Sets a new or replaces an existing listener for bottom control events. 72 */ 73 void setListener(Listener listener); 74 75 /** 76 * Set if the bottom controls are visible. 77 * @param visible {@code true} if visible. 78 */ 79 void setVisible(boolean visible); 80 81 /** 82 * @param visible Whether the button is visible. 83 */ 84 void setEditButtonVisibility(boolean visible); 85 86 /** 87 * Sets the visibility of the view-photosphere button. 88 * 89 * @param state one of {@link #VIEW_NONE}, {@link #VIEW_PHOTO_SPHERE}, 90 * {@link #VIEW_RGBZ}. 91 */ 92 void setViewButtonVisibility(int state); 93 94 /** 95 * @param visible Whether the button is visible. 96 */ 97 void setTinyPlanetButtonVisibility(boolean visible); 98 99 /** 100 * @param visible Whether the button is visible. 101 */ 102 void setDeleteButtonVisibility(boolean visible); 103 104 /** 105 * @param visible Whether the button is visible. 106 */ 107 void setShareButtonVisibility(boolean visible); 108 109 /** 110 * @param visible Whether the button is visible. 111 */ 112 void setGalleryButtonVisibility(boolean visible); 113 114 /** 115 * Classes implementing this interface can listen for events on the bottom 116 * controls. 117 */ 118 public static interface Listener { 119 /** 120 * Called when the user pressed the "view" button to e.g. view a photo 121 * sphere or RGBZ image. 122 */ 123 public void onView(); 124 125 /** 126 * Called when the "edit" button is pressed. 127 */ 128 public void onEdit(); 129 130 /** 131 * Called when the "tiny planet" button is pressed. 132 */ 133 public void onTinyPlanet(); 134 135 /** 136 * Called when the "delete" button is pressed. 137 */ 138 public void onDelete(); 139 140 /** 141 * Called when the "share" button is pressed. 142 */ 143 public void onShare(); 144 145 /** 146 * Called when the "gallery" button is pressed. 147 */ 148 public void onGallery(); 149 } 150 } 151 152 private final static String TAG = "CameraAppUI"; 153 154 private final AppController mController; 155 private final boolean mIsCaptureIntent; 156 private final boolean mIsSecureCamera; 157 private final AnimationManager mAnimationManager; 158 159 // Swipe states: 160 private final static int IDLE = 0; 161 private final static int SWIPE_UP = 1; 162 private final static int SWIPE_DOWN = 2; 163 private final static int SWIPE_LEFT = 3; 164 private final static int SWIPE_RIGHT = 4; 165 166 // Touch related measures: 167 private final int mSlop; 168 private final static int SWIPE_TIME_OUT_MS = 500; 169 170 private final static int SHIMMY_DELAY_MS = 1000; 171 172 // Mode cover states: 173 private final static int COVER_HIDDEN = 0; 174 private final static int COVER_SHOWN = 1; 175 private final static int COVER_WILL_HIDE_AT_NEXT_FRAME = 2; 176 177 // App level views: 178 private final FrameLayout mCameraRootView; 179 private final ModeTransitionView mModeTransitionView; 180 private final MainActivityLayout mAppRootView; 181 private final ModeListView mModeListView; 182 private final FilmstripLayout mFilmstripLayout; 183 private TextureView mTextureView; 184 private View mFlashOverlay; 185 private FrameLayout mModuleUI; 186 187 private GestureDetector mGestureDetector; 188 private int mSwipeState = IDLE; 189 private ImageView mPreviewThumbView; 190 private PreviewOverlay mPreviewOverlay; 191 private PreviewStatusListener mPreviewStatusListener; 192 private int mModeCoverState = COVER_HIDDEN; 193 private FilmstripBottomControls mFilmstripBottomControls; 194 private FilmstripContentPanel mFilmstripPanel; 195 196 // TODO this isn't used by all modules universally, should be part of a util class or something 197 /** 198 * Resizes the preview texture and given bottom bar for 100% preview size 199 */ 200 public void adjustPreviewAndBottomBarSize(int width, int height, 201 BottomBar bottomBar, float aspectRatio, 202 int bottomBarMinHeight, int bottomBarOptimalHeight) { 203 Matrix matrix = mTextureView.getTransform(null); 204 205 float scaleX = 1f, scaleY = 1f; 206 float scaledTextureWidth, scaledTextureHeight; 207 if (width > height) { 208 scaledTextureWidth = Math.min(width, 209 (int) (height * aspectRatio)); 210 scaledTextureHeight = Math.min(height, 211 (int) (width / aspectRatio)); 212 } else { 213 scaledTextureWidth = Math.min(width, 214 (int) (height / aspectRatio)); 215 scaledTextureHeight = Math.min(height, 216 (int) (width * aspectRatio)); 217 } 218 219 scaleX = scaledTextureWidth / width; 220 scaleY = scaledTextureHeight / height; 221 222 // TODO: Need a better way to find out whether currently in landscape 223 boolean landscape = width > height; 224 if (landscape) { 225 matrix.setScale(scaleX, scaleY, 0f, (float) height / 2); 226 } else { 227 matrix.setScale(scaleX, scaleY, (float) width / 2, 0.0f); 228 } 229 setPreviewTransformMatrix(matrix); 230 231 float previewAspectRatio = 232 (float)scaledTextureWidth / (float)scaledTextureHeight; 233 if (previewAspectRatio < 1.0) { 234 previewAspectRatio = 1.0f/previewAspectRatio; 235 } 236 float screenAspectRatio = (float)width / (float)height; 237 if (screenAspectRatio < 1.0) { 238 screenAspectRatio = 1.0f/screenAspectRatio; 239 } 240 241 if(bottomBar != null) { 242 LayoutParams lp = (LayoutParams) bottomBar.getLayoutParams(); 243 // TODO accoount for cases where resizes bar height would be < bottomBarMinHeight 244 if (previewAspectRatio >= screenAspectRatio) { 245 bottomBar.setAlpha(0.5f); 246 if (landscape) { 247 lp.width = bottomBarOptimalHeight; 248 lp.height = LayoutParams.MATCH_PARENT; 249 } else { 250 lp.height = bottomBarOptimalHeight; 251 lp.width = LayoutParams.MATCH_PARENT; 252 } 253 } else { 254 bottomBar.setAlpha(1.0f); 255 if (landscape) { 256 lp.width = (int)((float) width - scaledTextureWidth); 257 lp.height = LayoutParams.MATCH_PARENT; 258 } else { 259 lp.height = (int)((float) height - scaledTextureHeight); 260 lp.width = LayoutParams.MATCH_PARENT; 261 } 262 } 263 bottomBar.setLayoutParams(lp); 264 } 265 } 266 267 public interface AnimationFinishedListener { 268 public void onAnimationFinished(boolean success); 269 } 270 271 private class MyTouchListener implements View.OnTouchListener { 272 private boolean mScaleStarted = false; 273 @Override 274 public boolean onTouch(View v, MotionEvent event) { 275 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 276 mScaleStarted = false; 277 } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { 278 mScaleStarted = true; 279 } 280 return (!mScaleStarted) && mGestureDetector.onTouchEvent(event); 281 } 282 } 283 284 /** 285 * This gesture listener finds out the direction of the scroll gestures and 286 * sends them to CameraAppUI to do further handling. 287 */ 288 private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { 289 private MotionEvent mDown; 290 291 @Override 292 public boolean onScroll(MotionEvent e1, MotionEvent ev, float distanceX, float distanceY) { 293 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT_MS 294 || mSwipeState != IDLE) { 295 return true; 296 } 297 298 int deltaX = (int) (ev.getX() - mDown.getX()); 299 int deltaY = (int) (ev.getY() - mDown.getY()); 300 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) { 301 if (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop) { 302 // Calculate the direction of the swipe. 303 if (deltaX >= Math.abs(deltaY)) { 304 // Swipe right. 305 setSwipeState(SWIPE_RIGHT); 306 } else if (deltaX <= -Math.abs(deltaY)) { 307 // Swipe left. 308 setSwipeState(SWIPE_LEFT); 309 } else if (deltaY >= Math.abs(deltaX)) { 310 // Swipe down. 311 setSwipeState(SWIPE_DOWN); 312 } else if (deltaY <= -Math.abs(deltaX)) { 313 // Swipe up. 314 setSwipeState(SWIPE_UP); 315 } 316 } 317 } 318 return true; 319 } 320 321 private void setSwipeState(int swipeState) { 322 mSwipeState = swipeState; 323 // Notify new swipe detected. 324 onSwipeDetected(swipeState); 325 } 326 327 @Override 328 public boolean onDown(MotionEvent ev) { 329 mDown = MotionEvent.obtain(ev); 330 mSwipeState = IDLE; 331 return false; 332 } 333 } 334 335 public CameraAppUI(AppController controller, MainActivityLayout appRootView, 336 boolean isSecureCamera, boolean isCaptureIntent) { 337 mSlop = ViewConfiguration.get(controller.getAndroidContext()).getScaledTouchSlop(); 338 mController = controller; 339 mIsSecureCamera = isSecureCamera; 340 mIsCaptureIntent = isCaptureIntent; 341 342 mAppRootView = appRootView; 343 mFilmstripLayout = (FilmstripLayout) appRootView.findViewById(R.id.filmstrip_layout); 344 mCameraRootView = (FrameLayout) appRootView.findViewById(R.id.camera_app_root); 345 mModeTransitionView = (ModeTransitionView) 346 mAppRootView.findViewById(R.id.mode_transition_view); 347 mFilmstripBottomControls = new FilmstripBottomControls( 348 (ViewGroup) mAppRootView.findViewById(R.id.filmstrip_bottom_controls)); 349 mFilmstripPanel = (FilmstripContentPanel) mAppRootView.findViewById(R.id.filmstrip_layout); 350 mGestureDetector = new GestureDetector(controller.getAndroidContext(), 351 new MyGestureListener()); 352 mModeListView = (ModeListView) appRootView.findViewById(R.id.mode_list_layout); 353 if (mModeListView != null) { 354 mModeListView.setModeSwitchListener(this); 355 } else { 356 Log.e(TAG, "Cannot find mode list in the view hierarchy"); 357 } 358 mAnimationManager = new AnimationManager(); 359 } 360 361 /** 362 * Redirects touch events to appropriate recipient views based on swipe direction. 363 * More specifically, swipe up and swipe down will be handled by the view that handles 364 * mode transition; swipe left will be send to filmstrip; swipe right will be redirected 365 * to mode list in order to bring up mode list. 366 */ 367 private void onSwipeDetected(int swipeState) { 368 if (swipeState == SWIPE_UP || swipeState == SWIPE_DOWN) { 369 // Quick switch between photo/video. 370 if (mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO || 371 mController.getCurrentModuleIndex() == ModeListView.MODE_VIDEO) { 372 mAppRootView.redirectTouchEventsTo(mModeTransitionView); 373 374 final int moduleToTransitionTo = 375 mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ? 376 ModeListView.MODE_VIDEO : ModeListView.MODE_PHOTO; 377 int shadeColorId = ModeListView.getModeThemeColor(moduleToTransitionTo); 378 int iconRes = ModeListView.getModeIconResourceId(moduleToTransitionTo); 379 380 AnimationFinishedListener listener = new AnimationFinishedListener() { 381 public void onAnimationFinished(boolean success) { 382 if (success) { 383 // Go to new module when the previous operation is successful. 384 mController.onModeSelected(moduleToTransitionTo); 385 mModeTransitionView.startPeepHoleAnimation(); 386 } 387 } 388 }; 389 if (mSwipeState == SWIPE_UP) { 390 mModeTransitionView.prepareToPullUpShade(shadeColorId, iconRes, listener); 391 } else { 392 mModeTransitionView.prepareToPullDownShade(shadeColorId, iconRes, listener); 393 } 394 } 395 } else if (swipeState == SWIPE_LEFT) { 396 // Pass the touch sequence to filmstrip layout. 397 mAppRootView.redirectTouchEventsTo(mFilmstripLayout); 398 399 } else if (swipeState == SWIPE_RIGHT) { 400 // Pass the touch to mode switcher 401 mAppRootView.redirectTouchEventsTo(mModeListView); 402 } 403 } 404 405 /** 406 * Gets called when activity resumes in preview. 407 */ 408 public void resume() { 409 if (mTextureView == null || mTextureView.getSurfaceTexture() != null) { 410 mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS); 411 } else { 412 // Show mode theme cover until preview is ready 413 showModeCoverUntilPreviewReady(); 414 } 415 // Hide action bar first since we are in full screen mode first, and 416 // switch the system UI to lights-out mode. 417 mFilmstripPanel.hide(); 418 } 419 420 /** 421 * A cover view showing the mode theme color and mode icon will be visible on 422 * top of preview until preview is ready (i.e. camera preview is started and 423 * the first frame has been received). 424 */ 425 private void showModeCoverUntilPreviewReady() { 426 int modeId = mController.getCurrentModuleIndex(); 427 int colorId = ModeListView.getModeThemeColor(modeId); 428 int iconId = ModeListView.getModeIconResourceId(modeId); 429 mModeTransitionView.setupModeCover(colorId, iconId); 430 mModeCoverState = COVER_SHOWN; 431 } 432 433 private void hideModeCover() { 434 mModeTransitionView.hideModeCover(new AnimationFinishedListener() { 435 @Override 436 public void onAnimationFinished(boolean success) { 437 if (success) { 438 // Show shimmy in SHIMMY_DELAY_MS 439 mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS); 440 } 441 } 442 }); 443 } 444 445 /** 446 * Called when the back key is pressed. 447 * 448 * @return Whether the UI responded to the key event. 449 */ 450 public boolean onBackPressed() { 451 return mFilmstripLayout.onBackPressed(); 452 } 453 454 /** 455 * Sets a {@link com.android.camera.ui.PreviewStatusListener} that 456 * listens to SurfaceTexture changes. In addition, the listener will also provide 457 * a {@link android.view.GestureDetector.OnGestureListener}, which will listen to 458 * gestures that happen on camera preview. 459 * 460 * @param previewStatusListener the listener that gets notified when SurfaceTexture 461 * changes 462 */ 463 public void setPreviewStatusListener(PreviewStatusListener previewStatusListener) { 464 mPreviewStatusListener = previewStatusListener; 465 if (mPreviewStatusListener != null) { 466 GestureDetector.OnGestureListener gestureListener 467 = mPreviewStatusListener.getGestureListener(); 468 if (gestureListener != null) { 469 mPreviewOverlay.setGestureListener(gestureListener); 470 } 471 } 472 } 473 474 /** 475 * This inflates generic_module layout, which contains all the shared views across 476 * modules. Then each module inflates their own views in the given view group. For 477 * now, this is called every time switching from a not-yet-refactored module to a 478 * refactored module. In the future, this should only need to be done once per app 479 * start. 480 */ 481 public void prepareModuleUI() { 482 mCameraRootView.removeAllViews(); 483 LayoutInflater inflater = (LayoutInflater) mController.getAndroidContext() 484 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 485 inflater.inflate(R.layout.generic_module, mCameraRootView, true); 486 487 mModuleUI = (FrameLayout) mCameraRootView.findViewById(R.id.module_layout); 488 mTextureView = (TextureView) mCameraRootView.findViewById(R.id.preview_content); 489 mTextureView.setSurfaceTextureListener(this); 490 mPreviewOverlay = (PreviewOverlay) mCameraRootView.findViewById(R.id.preview_overlay); 491 mPreviewOverlay.setOnTouchListener(new MyTouchListener()); 492 mFlashOverlay = mCameraRootView.findViewById(R.id.flash_overlay); 493 mPreviewThumbView = (ImageView) mCameraRootView.findViewById(R.id.preview_thumb); 494 495 } 496 497 // TODO: Remove this when refactor is done. 498 // This is here to ensure refactored modules can work with not-yet-refactored ones. 499 public void clearCameraUI() { 500 mCameraRootView.removeAllViews(); 501 mModuleUI = null; 502 mTextureView = null; 503 mPreviewOverlay = null; 504 mFlashOverlay = null; 505 } 506 507 /** 508 * Called indirectly from each module in their initialization to get a view group 509 * to inflate the module specific views in. 510 * 511 * @return a view group for modules to attach views to 512 */ 513 public FrameLayout getModuleRootView() { 514 // TODO: Change it to mModuleUI when refactor is done 515 return mCameraRootView; 516 } 517 518 /** 519 * Remove all the module specific views. 520 */ 521 public void clearModuleUI() { 522 if (mModuleUI != null) { 523 mModuleUI.removeAllViews(); 524 } 525 526 // TODO: Bring TextureView up to the app level 527 mTextureView.removeOnLayoutChangeListener(null); 528 529 mPreviewStatusListener = null; 530 mPreviewOverlay.reset(); 531 } 532 533 /** 534 * Gets called when preview is started. 535 */ 536 public void onPreviewStarted() { 537 if (mModeCoverState == COVER_SHOWN) { 538 mModeCoverState = COVER_WILL_HIDE_AT_NEXT_FRAME; 539 } 540 } 541 542 @Override 543 public void onModeSelected(int modeIndex) { 544 mController.onModeSelected(modeIndex); 545 } 546 547 /** 548 * Sets the transform matrix on the preview TextureView 549 */ 550 public void setPreviewTransformMatrix(Matrix transformMatrix) { 551 if (mTextureView == null) { 552 throw new UnsupportedOperationException("Cannot set transform matrix on a null" + 553 " TextureView"); 554 } 555 mTextureView.setTransform(transformMatrix); 556 } 557 558 559 /********************** Capture animation **********************/ 560 /* TODO: This session is subject to UX changes. In addition to the generic 561 flash animation and post capture animation, consider designating a parameter 562 for specifying the type of animation, as well as an animation finished listener 563 so that modules can have more knowledge of the status of the animation. */ 564 565 /** 566 * Starts the pre-capture animation. 567 */ 568 public void startPreCaptureAnimation() { 569 mAnimationManager.startFlashAnimation(mFlashOverlay); 570 } 571 572 /** 573 * Cancels the pre-capture animation. 574 */ 575 public void cancelPreCaptureAnimation() { 576 mAnimationManager.cancelAnimations(); 577 } 578 579 /** 580 * Starts the post-capture animation with the current preview image. 581 */ 582 public void startPostCaptureAnimation() { 583 if (mTextureView == null) { 584 Log.e(TAG, "Cannot get a frame from a null TextureView for animation"); 585 return; 586 } 587 // TODO: Down sample bitmap 588 startPostCaptureAnimation(mTextureView.getBitmap()); 589 } 590 591 /** 592 * Starts the post-capture animation with the given thumbnail. 593 * 594 * @param thumbnail The thumbnail for the animation. 595 */ 596 public void startPostCaptureAnimation(Bitmap thumbnail) { 597 mPreviewThumbView.setImageBitmap(thumbnail); 598 mAnimationManager.startCaptureAnimation(mPreviewThumbView); 599 } 600 601 /** 602 * Cancels the post-capture animation. 603 */ 604 public void cancelPostCaptureAnimation() { 605 mAnimationManager.cancelAnimations(); 606 } 607 608 public FilmstripContentPanel getFilmstripContentPanel() { 609 return mFilmstripPanel; 610 } 611 612 /** 613 * @return The {@link com.android.camera.app.CameraAppUI.BottomControls} on the 614 * bottom of the filmstrip. 615 */ 616 public BottomControls getFilmstripBottomControls() { 617 return mFilmstripBottomControls; 618 } 619 620 /** 621 * @param listener The listener for bottom controls. 622 */ 623 public void setFilmstripBottomControlsListener(BottomControls.Listener listener) { 624 mFilmstripBottomControls.setListener(listener); 625 } 626 627 /***************************SurfaceTexture Listener*********************************/ 628 629 @Override 630 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 631 Log.v(TAG, "SurfaceTexture is available"); 632 if (mPreviewStatusListener != null) { 633 mPreviewStatusListener.onSurfaceTextureAvailable(surface, width, height); 634 } 635 } 636 637 @Override 638 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 639 if (mPreviewStatusListener != null) { 640 mPreviewStatusListener.onSurfaceTextureSizeChanged(surface, width, height); 641 } 642 } 643 644 @Override 645 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 646 Log.v(TAG, "SurfaceTexture is destroyed"); 647 if (mPreviewStatusListener != null) { 648 return mPreviewStatusListener.onSurfaceTextureDestroyed(surface); 649 } 650 return false; 651 } 652 653 @Override 654 public void onSurfaceTextureUpdated(SurfaceTexture surface) { 655 if (mModeCoverState == COVER_WILL_HIDE_AT_NEXT_FRAME) { 656 hideModeCover(); 657 mModeCoverState = COVER_HIDDEN; 658 } 659 if (mPreviewStatusListener != null) { 660 mPreviewStatusListener.onSurfaceTextureUpdated(surface); 661 } 662 } 663} 664