CameraAppUI.java revision 2b906b8c9754b564d5113c7a342654c82f97f180
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.widget.FrameLayout; 31import android.widget.ImageView; 32 33import com.android.camera.AnimationManager; 34import com.android.camera.widget.FilmstripLayout; 35import com.android.camera.ui.MainActivityLayout; 36import com.android.camera.ui.ModeListView; 37import com.android.camera.ui.ModeTransitionView; 38import com.android.camera.ui.PreviewOverlay; 39import com.android.camera.ui.PreviewStatusListener; 40import com.android.camera2.R; 41 42/** 43 * CameraAppUI centralizes control of views shared across modules. Whereas module 44 * specific views will be handled in each Module UI. For example, we can now 45 * bring the flash animation and capture animation up from each module to app 46 * level, as these animations are largely the same for all modules. 47 * 48 * This class also serves to disambiguate touch events. It recognizes all the 49 * swipe gestures that happen on the preview by attaching a touch listener to 50 * a full-screen view on top of preview TextureView. Since CameraAppUI has knowledge 51 * of how swipe from each direction should be handled, it can then redirect these 52 * events to appropriate recipient views. 53 */ 54public class CameraAppUI implements ModeListView.ModeSwitchListener, 55 TextureView.SurfaceTextureListener { 56 private final static String TAG = "CameraAppUI"; 57 58 private final AppController mController; 59 private final boolean mIsCaptureIntent; 60 private final boolean mIsSecureCamera; 61 private final AnimationManager mAnimationManager; 62 63 // Swipe states: 64 private final static int IDLE = 0; 65 private final static int SWIPE_UP = 1; 66 private final static int SWIPE_DOWN = 2; 67 private final static int SWIPE_LEFT = 3; 68 private final static int SWIPE_RIGHT = 4; 69 70 // Touch related measures: 71 private final int mSlop; 72 private final static int SWIPE_TIME_OUT_MS = 500; 73 74 private final static int SHIMMY_DELAY_MS = 1000; 75 76 // Mode cover states: 77 private final static int COVER_HIDDEN = 0; 78 private final static int COVER_SHOWN = 1; 79 private final static int COVER_WILL_HIDE_AT_NEXT_FRAME = 2; 80 81 // App level views: 82 private final FrameLayout mCameraRootView; 83 private final ModeTransitionView mModeTransitionView; 84 private final MainActivityLayout mAppRootView; 85 private final ModeListView mModeListView; 86 private final FilmstripLayout mFilmstripLayout; 87 private TextureView mTextureView; 88 private View mFlashOverlay; 89 private FrameLayout mModuleUI; 90 91 private GestureDetector mGestureDetector; 92 private int mSwipeState = IDLE; 93 private ImageView mPreviewThumbView; 94 private PreviewOverlay mPreviewOverlay; 95 private PreviewStatusListener mPreviewStatusListener; 96 private int mModeCoverState = COVER_HIDDEN; 97 98 public interface AnimationFinishedListener { 99 public void onAnimationFinished(boolean success); 100 } 101 102 private class MyTouchListener implements View.OnTouchListener { 103 private boolean mScaleStarted = false; 104 @Override 105 public boolean onTouch(View v, MotionEvent event) { 106 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 107 mScaleStarted = false; 108 } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { 109 mScaleStarted = true; 110 } 111 return (!mScaleStarted) && mGestureDetector.onTouchEvent(event); 112 } 113 } 114 115 /** 116 * This gesture listener finds out the direction of the scroll gestures and 117 * sends them to CameraAppUI to do further handling. 118 */ 119 private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { 120 private MotionEvent mDown; 121 122 @Override 123 public boolean onScroll(MotionEvent e1, MotionEvent ev, float distanceX, float distanceY) { 124 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT_MS 125 || mSwipeState != IDLE) { 126 return true; 127 } 128 129 int deltaX = (int) (ev.getX() - mDown.getX()); 130 int deltaY = (int) (ev.getY() - mDown.getY()); 131 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) { 132 if (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop) { 133 // Calculate the direction of the swipe. 134 if (deltaX >= Math.abs(deltaY)) { 135 // Swipe right. 136 setSwipeState(SWIPE_RIGHT); 137 } else if (deltaX <= -Math.abs(deltaY)) { 138 // Swipe left. 139 setSwipeState(SWIPE_LEFT); 140 } else if (deltaY >= Math.abs(deltaX)) { 141 // Swipe down. 142 setSwipeState(SWIPE_DOWN); 143 } else if (deltaY <= -Math.abs(deltaX)) { 144 // Swipe up. 145 setSwipeState(SWIPE_UP); 146 } 147 } 148 } 149 return true; 150 } 151 152 private void setSwipeState(int swipeState) { 153 mSwipeState = swipeState; 154 // Notify new swipe detected. 155 onSwipeDetected(swipeState); 156 } 157 158 @Override 159 public boolean onDown(MotionEvent ev) { 160 mDown = MotionEvent.obtain(ev); 161 mSwipeState = IDLE; 162 return false; 163 } 164 } 165 166 public CameraAppUI(AppController controller, MainActivityLayout appRootView, 167 boolean isSecureCamera, boolean isCaptureIntent) { 168 mSlop = ViewConfiguration.get(controller.getAndroidContext()).getScaledTouchSlop(); 169 mController = controller; 170 mIsSecureCamera = isSecureCamera; 171 mIsCaptureIntent = isCaptureIntent; 172 173 mAppRootView = appRootView; 174 mFilmstripLayout = (FilmstripLayout) appRootView.findViewById(R.id.filmstrip_layout); 175 mCameraRootView = (FrameLayout) appRootView.findViewById(R.id.camera_app_root); 176 mModeTransitionView = (ModeTransitionView) 177 mAppRootView.findViewById(R.id.mode_transition_view); 178 mGestureDetector = new GestureDetector(controller.getAndroidContext(), 179 new MyGestureListener()); 180 mModeListView = (ModeListView) appRootView.findViewById(R.id.mode_list_layout); 181 if (mModeListView != null) { 182 mModeListView.setModeSwitchListener(this); 183 } else { 184 Log.e(TAG, "Cannot find mode list in the view hierarchy"); 185 } 186 mAnimationManager = new AnimationManager(); 187 } 188 189 /** 190 * Redirects touch events to appropriate recipient views based on swipe direction. 191 * More specifically, swipe up and swipe down will be handled by the view that handles 192 * mode transition; swipe left will be send to filmstrip; swipe right will be redirected 193 * to mode list in order to bring up mode list. 194 */ 195 private void onSwipeDetected(int swipeState) { 196 if (swipeState == SWIPE_UP || swipeState == SWIPE_DOWN) { 197 // Quick switch between photo/video. 198 if (mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO || 199 mController.getCurrentModuleIndex() == ModeListView.MODE_VIDEO) { 200 mAppRootView.redirectTouchEventsTo(mModeTransitionView); 201 202 final int moduleToTransitionTo = 203 mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ? 204 ModeListView.MODE_VIDEO : ModeListView.MODE_PHOTO; 205 int shadeColorId = ModeListView.getModeThemeColor(moduleToTransitionTo); 206 int iconRes = ModeListView.getModeIconResourceId(moduleToTransitionTo); 207 208 AnimationFinishedListener listener = new AnimationFinishedListener() { 209 public void onAnimationFinished(boolean success) { 210 if (success) { 211 // Go to new module when the previous operation is successful. 212 mController.onModeSelected(moduleToTransitionTo); 213 mModeTransitionView.startPeepHoleAnimation(); 214 } 215 } 216 }; 217 if (mSwipeState == SWIPE_UP) { 218 mModeTransitionView.prepareToPullUpShade(shadeColorId, iconRes, listener); 219 } else { 220 mModeTransitionView.prepareToPullDownShade(shadeColorId, iconRes, listener); 221 } 222 } 223 } else if (swipeState == SWIPE_LEFT) { 224 // Pass the touch sequence to filmstrip layout. 225 mAppRootView.redirectTouchEventsTo(mFilmstripLayout); 226 227 } else if (swipeState == SWIPE_RIGHT) { 228 // Pass the touch to mode switcher 229 mAppRootView.redirectTouchEventsTo(mModeListView); 230 } 231 } 232 233 /** 234 * Gets called when activity resumes in preview. 235 */ 236 public void resume() { 237 if (mTextureView == null || mTextureView.getSurfaceTexture() != null) { 238 mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS); 239 } else { 240 // Show mode theme cover until preview is ready 241 showModeCoverUntilPreviewReady(); 242 } 243 } 244 245 /** 246 * A cover view showing the mode theme color and mode icon will be visible on 247 * top of preview until preview is ready (i.e. camera preview is started and 248 * the first frame has been received). 249 */ 250 private void showModeCoverUntilPreviewReady() { 251 int modeId = mController.getCurrentModuleIndex(); 252 int colorId = ModeListView.getModeThemeColor(modeId); 253 int iconId = ModeListView.getModeIconResourceId(modeId); 254 mModeTransitionView.setupModeCover(colorId, iconId); 255 mModeCoverState = COVER_SHOWN; 256 } 257 258 private void hideModeCover() { 259 mModeTransitionView.hideModeCover(new AnimationFinishedListener() { 260 @Override 261 public void onAnimationFinished(boolean success) { 262 if (success) { 263 // Show shimmy in SHIMMY_DELAY_MS 264 mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS); 265 } 266 } 267 }); 268 } 269 270 /** 271 * Called when the back key is pressed. 272 * 273 * @return Whether the UI responded to the key event. 274 */ 275 public boolean onBackPressed() { 276 return mFilmstripLayout.onBackPressed(); 277 } 278 279 /** 280 * Sets a {@link com.android.camera.ui.PreviewStatusListener} that 281 * listens to SurfaceTexture changes. In addition, the listener will also provide 282 * a {@link android.view.GestureDetector.OnGestureListener}, which will listen to 283 * gestures that happen on camera preview. 284 * 285 * @param previewStatusListener the listener that gets notified when SurfaceTexture 286 * changes 287 */ 288 public void setPreviewStatusListener(PreviewStatusListener previewStatusListener) { 289 mPreviewStatusListener = previewStatusListener; 290 if (mPreviewStatusListener != null) { 291 GestureDetector.OnGestureListener gestureListener 292 = mPreviewStatusListener.getGestureListener(); 293 if (gestureListener != null) { 294 mPreviewOverlay.setGestureListener(gestureListener); 295 } 296 } 297 } 298 299 /** 300 * This inflates generic_module layout, which contains all the shared views across 301 * modules. Then each module inflates their own views in the given view group. For 302 * now, this is called every time switching from a not-yet-refactored module to a 303 * refactored module. In the future, this should only need to be done once per app 304 * start. 305 */ 306 public void prepareModuleUI() { 307 mCameraRootView.removeAllViews(); 308 LayoutInflater inflater = (LayoutInflater) mController.getAndroidContext() 309 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 310 inflater.inflate(R.layout.generic_module, mCameraRootView, true); 311 312 mModuleUI = (FrameLayout) mCameraRootView.findViewById(R.id.module_layout); 313 mTextureView = (TextureView) mCameraRootView.findViewById(R.id.preview_content); 314 mTextureView.setSurfaceTextureListener(this); 315 mPreviewOverlay = (PreviewOverlay) mCameraRootView.findViewById(R.id.preview_overlay); 316 mPreviewOverlay.setOnTouchListener(new MyTouchListener()); 317 mFlashOverlay = mCameraRootView.findViewById(R.id.flash_overlay); 318 mPreviewThumbView = (ImageView) mCameraRootView.findViewById(R.id.preview_thumb); 319 320 } 321 322 // TODO: Remove this when refactor is done. 323 // This is here to ensure refactored modules can work with not-yet-refactored ones. 324 public void clearCameraUI() { 325 mCameraRootView.removeAllViews(); 326 mModuleUI = null; 327 mTextureView = null; 328 mPreviewOverlay = null; 329 mFlashOverlay = null; 330 } 331 332 /** 333 * Called indirectly from each module in their initialization to get a view group 334 * to inflate the module specific views in. 335 * 336 * @return a view group for modules to attach views to 337 */ 338 public FrameLayout getModuleRootView() { 339 // TODO: Change it to mModuleUI when refactor is done 340 return mCameraRootView; 341 } 342 343 /** 344 * Remove all the module specific views. 345 */ 346 public void clearModuleUI() { 347 if (mModuleUI != null) { 348 mModuleUI.removeAllViews(); 349 } 350 351 // TODO: Bring TextureView up to the app level 352 mTextureView.removeOnLayoutChangeListener(null); 353 354 mPreviewStatusListener = null; 355 mPreviewOverlay.reset(); 356 } 357 358 /** 359 * Gets called when preview is started. 360 */ 361 public void onPreviewStarted() { 362 if (mModeCoverState == COVER_SHOWN) { 363 mModeCoverState = COVER_WILL_HIDE_AT_NEXT_FRAME; 364 } 365 } 366 367 @Override 368 public void onModeSelected(int modeIndex) { 369 mController.onModeSelected(modeIndex); 370 } 371 372 /** 373 * Sets the transform matrix on the preview TextureView 374 */ 375 public void setPreviewTransformMatrix(Matrix transformMatrix) { 376 if (mTextureView == null) { 377 throw new UnsupportedOperationException("Cannot set transform matrix on a null" + 378 " TextureView"); 379 } 380 mTextureView.setTransform(transformMatrix); 381 } 382 383 384 /********************** Capture animation **********************/ 385 /* TODO: This session is subject to UX changes. In addition to the generic 386 flash animation and post capture animation, consider designating a parameter 387 for specifying the type of animation, as well as an animation finished listener 388 so that modules can have more knowledge of the status of the animation. */ 389 390 /** 391 * Starts the pre-capture animation. 392 */ 393 public void startPreCaptureAnimation() { 394 mAnimationManager.startFlashAnimation(mFlashOverlay); 395 } 396 397 /** 398 * Cancels the pre-capture animation. 399 */ 400 public void cancelPreCaptureAnimation() { 401 mAnimationManager.cancelAnimations(); 402 } 403 404 /** 405 * Starts the post-capture animation with the current preview image. 406 */ 407 public void startPostCaptureAnimation() { 408 if (mTextureView == null) { 409 Log.e(TAG, "Cannot get a frame from a null TextureView for animation"); 410 return; 411 } 412 // TODO: Down sample bitmap 413 startPostCaptureAnimation(mTextureView.getBitmap()); 414 } 415 416 /** 417 * Starts the post-capture animation with the given thumbnail. 418 * 419 * @param thumbnail The thumbnail for the animation. 420 */ 421 public void startPostCaptureAnimation(Bitmap thumbnail) { 422 mPreviewThumbView.setImageBitmap(thumbnail); 423 mAnimationManager.startCaptureAnimation(mPreviewThumbView); 424 } 425 426 /** 427 * Cancels the post-capture animation. 428 */ 429 public void cancelPostCaptureAnimation() { 430 mAnimationManager.cancelAnimations(); 431 } 432 433 /***************************SurfaceTexture Listener*********************************/ 434 435 @Override 436 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 437 Log.v(TAG, "SurfaceTexture is available"); 438 if (mPreviewStatusListener != null) { 439 mPreviewStatusListener.onSurfaceTextureAvailable(surface, width, height); 440 } 441 } 442 443 @Override 444 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 445 if (mPreviewStatusListener != null) { 446 mPreviewStatusListener.onSurfaceTextureSizeChanged(surface, width, height); 447 } 448 } 449 450 @Override 451 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 452 Log.v(TAG, "SurfaceTexture is destroyed"); 453 if (mPreviewStatusListener != null) { 454 return mPreviewStatusListener.onSurfaceTextureDestroyed(surface); 455 } 456 return false; 457 } 458 459 @Override 460 public void onSurfaceTextureUpdated(SurfaceTexture surface) { 461 if (mModeCoverState == COVER_WILL_HIDE_AT_NEXT_FRAME) { 462 hideModeCover(); 463 mModeCoverState = COVER_HIDDEN; 464 } 465 if (mPreviewStatusListener != null) { 466 mPreviewStatusListener.onSurfaceTextureUpdated(surface); 467 } 468 } 469} 470