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