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.ui; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorSet; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Path; 28 import android.graphics.PorterDuff; 29 import android.graphics.PorterDuffXfermode; 30 import android.graphics.Rect; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.util.AttributeSet; 34 import android.view.GestureDetector; 35 import android.view.MotionEvent; 36 import android.view.View; 37 38 import com.android.camera.app.CameraAppUI; 39 import com.android.camera.debug.Log; 40 import com.android.camera.util.Gusterpolator; 41 import com.android.camera2.R; 42 43 /** 44 * This view is designed to handle all the animations during camera mode transition. 45 * It should only be visible during mode switch. 46 */ 47 public class ModeTransitionView extends View { 48 private static final Log.Tag TAG = new Log.Tag("ModeTransView"); 49 50 private static final int PEEP_HOLE_ANIMATION_DURATION_MS = 300; 51 private static final int ICON_FADE_OUT_DURATION_MS = 850; 52 private static final int FADE_OUT_DURATION_MS = 250; 53 54 private static final int IDLE = 0; 55 private static final int PULL_UP_SHADE = 1; 56 private static final int PULL_DOWN_SHADE = 2; 57 private static final int PEEP_HOLE_ANIMATION = 3; 58 private static final int FADE_OUT = 4; 59 private static final int SHOW_STATIC_IMAGE = 5; 60 61 private static final float SCROLL_DISTANCE_MULTIPLY_FACTOR = 2f; 62 private static final int ALPHA_FULLY_TRANSPARENT = 0; 63 private static final int ALPHA_FULLY_OPAQUE = 255; 64 private static final int ALPHA_HALF_TRANSPARENT = 127; 65 66 private final GestureDetector mGestureDetector; 67 private final Paint mMaskPaint = new Paint(); 68 private final Rect mIconRect = new Rect(); 69 /** An empty drawable to fall back to when mIconDrawable set to null. */ 70 private final Drawable mDefaultDrawable = new ColorDrawable(); 71 72 private Drawable mIconDrawable; 73 private int mBackgroundColor; 74 private int mWidth = 0; 75 private int mHeight = 0; 76 private int mPeepHoleCenterX = 0; 77 private int mPeepHoleCenterY = 0; 78 private float mRadius = 0f; 79 private int mIconSize; 80 private AnimatorSet mPeepHoleAnimator; 81 private int mAnimationType = PEEP_HOLE_ANIMATION; 82 private float mScrollDistance = 0; 83 private final Path mShadePath = new Path(); 84 private final Paint mShadePaint = new Paint(); 85 private CameraAppUI.AnimationFinishedListener mAnimationFinishedListener; 86 private float mScrollTrend; 87 private Bitmap mBackgroundBitmap; 88 89 public ModeTransitionView(Context context, AttributeSet attrs) { 90 super(context, attrs); 91 mMaskPaint.setAlpha(0); 92 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 93 mBackgroundColor = getResources().getColor(R.color.video_mode_color); 94 mGestureDetector = new GestureDetector(getContext(), 95 new GestureDetector.SimpleOnGestureListener() { 96 @Override 97 public boolean onDown(MotionEvent ev) { 98 setScrollDistance(0f); 99 mScrollTrend = 0f; 100 return true; 101 } 102 103 @Override 104 public boolean onScroll(MotionEvent e1, MotionEvent e2, 105 float distanceX, float distanceY) { 106 setScrollDistance(getScrollDistance() 107 + SCROLL_DISTANCE_MULTIPLY_FACTOR * distanceY); 108 mScrollTrend = 0.3f * mScrollTrend + 0.7f * distanceY; 109 return false; 110 } 111 }); 112 mIconSize = getResources().getDimensionPixelSize(R.dimen.mode_transition_view_icon_size); 113 setIconDrawable(mDefaultDrawable); 114 } 115 116 /** 117 * Updates the size and shape of the shade 118 */ 119 private void updateShade() { 120 if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) { 121 mShadePath.reset(); 122 float shadeHeight; 123 if (mAnimationType == PULL_UP_SHADE) { 124 // Scroll distance > 0. 125 mShadePath.addRect(0, mHeight - getScrollDistance(), mWidth, mHeight, 126 Path.Direction.CW); 127 shadeHeight = getScrollDistance(); 128 } else { 129 // Scroll distance < 0. 130 mShadePath.addRect(0, 0, mWidth, - getScrollDistance(), Path.Direction.CW); 131 shadeHeight = getScrollDistance() * (-1); 132 } 133 134 if (mIconDrawable != null) { 135 if (shadeHeight < mHeight / 2 || mHeight == 0) { 136 mIconDrawable.setAlpha(ALPHA_FULLY_TRANSPARENT); 137 } else { 138 int alpha = ((int) shadeHeight - mHeight / 2) * ALPHA_FULLY_OPAQUE 139 / (mHeight / 2); 140 mIconDrawable.setAlpha(alpha); 141 } 142 } 143 invalidate(); 144 } 145 } 146 147 /** 148 * Sets the scroll distance. Note this function gets called in every 149 * frame during animation. It should be very light weight. 150 * 151 * @param scrollDistance the scaled distance that user has scrolled 152 */ 153 public void setScrollDistance(float scrollDistance) { 154 // First make sure scroll distance is clamped to the valid range. 155 if (mAnimationType == PULL_UP_SHADE) { 156 scrollDistance = Math.min(scrollDistance, mHeight); 157 scrollDistance = Math.max(scrollDistance, 0); 158 } else if (mAnimationType == PULL_DOWN_SHADE) { 159 scrollDistance = Math.min(scrollDistance, 0); 160 scrollDistance = Math.max(scrollDistance, -mHeight); 161 } 162 mScrollDistance = scrollDistance; 163 updateShade(); 164 } 165 166 public float getScrollDistance() { 167 return mScrollDistance; 168 } 169 170 @Override 171 public void onDraw(Canvas canvas) { 172 if (mAnimationType == PEEP_HOLE_ANIMATION) { 173 canvas.drawColor(mBackgroundColor); 174 if (mPeepHoleAnimator != null) { 175 // Draw a transparent circle using clear mode 176 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); 177 } 178 } else if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) { 179 canvas.drawPath(mShadePath, mShadePaint); 180 } else if (mAnimationType == IDLE || mAnimationType == FADE_OUT) { 181 canvas.drawColor(mBackgroundColor); 182 } else if (mAnimationType == SHOW_STATIC_IMAGE) { 183 // TODO: These different animation types need to be refactored into 184 // different animation effects. 185 canvas.drawBitmap(mBackgroundBitmap, 0, 0, null); 186 super.onDraw(canvas); 187 return; 188 } 189 super.onDraw(canvas); 190 mIconDrawable.draw(canvas); 191 } 192 193 @Override 194 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 195 mWidth = right - left; 196 mHeight = bottom - top; 197 // Center the icon in the view. 198 mIconRect.set(mWidth / 2 - mIconSize / 2, mHeight / 2 - mIconSize / 2, 199 mWidth / 2 + mIconSize / 2, mHeight / 2 + mIconSize / 2); 200 mIconDrawable.setBounds(mIconRect); 201 } 202 203 /** 204 * This is an overloaded function. When no position is provided for the animation, 205 * the peep hole will start at the default position (i.e. center of the view). 206 */ 207 public void startPeepHoleAnimation() { 208 float x = mWidth / 2; 209 float y = mHeight / 2; 210 startPeepHoleAnimation(x, y); 211 } 212 213 /** 214 * Starts the peep hole animation where the circle is centered at position (x, y). 215 */ 216 private void startPeepHoleAnimation(float x, float y) { 217 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 218 return; 219 } 220 mAnimationType = PEEP_HOLE_ANIMATION; 221 mPeepHoleCenterX = (int) x; 222 mPeepHoleCenterY = (int) y; 223 224 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); 225 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); 226 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge 227 + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); 228 229 final ValueAnimator radiusAnimator = ValueAnimator.ofFloat(0, endRadius); 230 radiusAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); 231 232 final ValueAnimator iconScaleAnimator = ValueAnimator.ofFloat(1f, 0.5f); 233 iconScaleAnimator.setDuration(ICON_FADE_OUT_DURATION_MS); 234 235 final ValueAnimator iconAlphaAnimator = ValueAnimator.ofInt(ALPHA_HALF_TRANSPARENT, 236 ALPHA_FULLY_TRANSPARENT); 237 iconAlphaAnimator.setDuration(ICON_FADE_OUT_DURATION_MS); 238 239 mPeepHoleAnimator = new AnimatorSet(); 240 mPeepHoleAnimator.playTogether(radiusAnimator, iconAlphaAnimator, iconScaleAnimator); 241 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); 242 243 iconAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 244 @Override 245 public void onAnimationUpdate(ValueAnimator animation) { 246 // Modify mask by enlarging the hole 247 mRadius = (Float) radiusAnimator.getAnimatedValue(); 248 249 mIconDrawable.setAlpha((Integer) iconAlphaAnimator.getAnimatedValue()); 250 float scale = (Float) iconScaleAnimator.getAnimatedValue(); 251 int size = (int) (scale * (float) mIconSize); 252 253 mIconDrawable.setBounds(mPeepHoleCenterX - size / 2, 254 mPeepHoleCenterY - size / 2, 255 mPeepHoleCenterX + size / 2, 256 mPeepHoleCenterY + size / 2); 257 258 invalidate(); 259 } 260 }); 261 262 mPeepHoleAnimator.addListener(new Animator.AnimatorListener() { 263 @Override 264 public void onAnimationStart(Animator animation) { 265 // Sets a HW layer on the view for the animation. 266 setLayerType(LAYER_TYPE_HARDWARE, null); 267 } 268 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 // Sets the layer type back to NONE as a workaround for b/12594617. 272 setLayerType(LAYER_TYPE_NONE, null); 273 mPeepHoleAnimator = null; 274 mRadius = 0; 275 mIconDrawable.setAlpha(ALPHA_FULLY_OPAQUE); 276 mIconDrawable.setBounds(mIconRect); 277 setVisibility(GONE); 278 mAnimationType = IDLE; 279 if (mAnimationFinishedListener != null) { 280 mAnimationFinishedListener.onAnimationFinished(true); 281 mAnimationFinishedListener = null; 282 } 283 } 284 285 @Override 286 public void onAnimationCancel(Animator animation) { 287 288 } 289 290 @Override 291 public void onAnimationRepeat(Animator animation) { 292 293 } 294 }); 295 mPeepHoleAnimator.start(); 296 297 } 298 299 @Override 300 public boolean onTouchEvent(MotionEvent ev) { 301 boolean touchHandled = mGestureDetector.onTouchEvent(ev); 302 if (ev.getActionMasked() == MotionEvent.ACTION_UP) { 303 // TODO: Take into account fling 304 snap(); 305 } 306 return touchHandled; 307 } 308 309 /** 310 * Snaps the shade to position at the end of a gesture. 311 */ 312 private void snap() { 313 if (mScrollTrend >= 0 && mAnimationType == PULL_UP_SHADE) { 314 // Snap to full screen. 315 snapShadeTo(mHeight, ALPHA_FULLY_OPAQUE); 316 } else if (mScrollTrend <= 0 && mAnimationType == PULL_DOWN_SHADE) { 317 // Snap to full screen. 318 snapShadeTo(-mHeight, ALPHA_FULLY_OPAQUE); 319 } else if (mScrollTrend < 0 && mAnimationType == PULL_UP_SHADE) { 320 // Snap back. 321 snapShadeTo(0, ALPHA_FULLY_TRANSPARENT, false); 322 } else if (mScrollTrend > 0 && mAnimationType == PULL_DOWN_SHADE) { 323 // Snap back. 324 snapShadeTo(0, ALPHA_FULLY_TRANSPARENT, false); 325 } 326 } 327 328 private void snapShadeTo(int scrollDistance, int alpha) { 329 snapShadeTo(scrollDistance, alpha, true); 330 } 331 332 /** 333 * Snaps the shade to a given scroll distance and sets the icon alpha. If the shade 334 * is to snap back out, then hide the view after the animation. 335 * 336 * @param scrollDistance scaled user scroll distance 337 * @param alpha ending alpha of the icon drawable 338 * @param snapToFullScreen whether this snap animation snaps the shade to full screen 339 */ 340 private void snapShadeTo(final int scrollDistance, final int alpha, 341 final boolean snapToFullScreen) { 342 if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) { 343 ObjectAnimator scrollAnimator = ObjectAnimator.ofFloat(this, "scrollDistance", 344 scrollDistance); 345 scrollAnimator.addListener(new Animator.AnimatorListener() { 346 @Override 347 public void onAnimationStart(Animator animation) { 348 349 } 350 351 @Override 352 public void onAnimationEnd(Animator animation) { 353 setScrollDistance(scrollDistance); 354 mIconDrawable.setAlpha(alpha); 355 mAnimationType = IDLE; 356 if (!snapToFullScreen) { 357 setVisibility(GONE); 358 } 359 if (mAnimationFinishedListener != null) { 360 mAnimationFinishedListener.onAnimationFinished(snapToFullScreen); 361 mAnimationFinishedListener = null; 362 } 363 } 364 365 @Override 366 public void onAnimationCancel(Animator animation) { 367 368 } 369 370 @Override 371 public void onAnimationRepeat(Animator animation) { 372 373 } 374 }); 375 scrollAnimator.setInterpolator(Gusterpolator.INSTANCE); 376 scrollAnimator.start(); 377 } 378 } 379 380 381 /** 382 * Set the states for the animation that pulls up a shade with given shade color. 383 * 384 * @param shadeColorId color id of the shade that will be pulled up 385 * @param iconId id of the icon that will appear on top the shade 386 * @param listener a listener that will get notified when the animation 387 * is finished. Could be <code>null</code>. 388 */ 389 public void prepareToPullUpShade(int shadeColorId, int iconId, 390 CameraAppUI.AnimationFinishedListener listener) { 391 prepareShadeAnimation(PULL_UP_SHADE, shadeColorId, iconId, listener); 392 } 393 394 /** 395 * Set the states for the animation that pulls down a shade with given shade color. 396 * 397 * @param shadeColorId color id of the shade that will be pulled down 398 * @param modeIconResourceId id of the icon that will appear on top the shade 399 * @param listener a listener that will get notified when the animation 400 * is finished. Could be <code>null</code>. 401 */ 402 public void prepareToPullDownShade(int shadeColorId, int modeIconResourceId, 403 CameraAppUI.AnimationFinishedListener listener) {; 404 prepareShadeAnimation(PULL_DOWN_SHADE, shadeColorId, modeIconResourceId, listener); 405 } 406 407 /** 408 * Set the states for the animation that involves a shade. 409 * 410 * @param animationType type of animation that will happen to the shade 411 * @param shadeColorId color id of the shade that will be animated 412 * @param iconResId id of the icon that will appear on top the shade 413 * @param listener a listener that will get notified when the animation 414 * is finished. Could be <code>null</code>. 415 */ 416 private void prepareShadeAnimation(int animationType, int shadeColorId, int iconResId, 417 CameraAppUI.AnimationFinishedListener listener) { 418 mAnimationFinishedListener = listener; 419 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 420 mPeepHoleAnimator.end(); 421 } 422 mAnimationType = animationType; 423 resetShade(shadeColorId, iconResId); 424 } 425 426 /** 427 * Reset the shade with the given shade color and icon drawable. 428 * 429 * @param shadeColorId id of the shade color 430 * @param modeIconResourceId resource id of the icon drawable 431 */ 432 private void resetShade(int shadeColorId, int modeIconResourceId) { 433 // Sets color for the shade. 434 int shadeColor = getResources().getColor(shadeColorId); 435 mBackgroundColor = shadeColor; 436 mShadePaint.setColor(shadeColor); 437 // Reset scroll distance. 438 setScrollDistance(0f); 439 // Sets new drawable. 440 updateIconDrawableByResourceId(modeIconResourceId); 441 mIconDrawable.setAlpha(0); 442 setVisibility(VISIBLE); 443 } 444 445 /** 446 * By default, all drawables instances loaded from the same resource share a 447 * common state; if you modify the state of one instance, all the other 448 * instances will receive the same modification. So here we need to make sure 449 * we mutate the drawable loaded from resource. 450 * 451 * @param modeIconResourceId resource id of the icon drawable 452 */ 453 private void updateIconDrawableByResourceId(int modeIconResourceId) { 454 Drawable iconDrawable = getResources().getDrawable(modeIconResourceId); 455 if (iconDrawable == null) { 456 // Resource id not found 457 Log.e(TAG, "Invalid resource id for icon drawable. Setting icon drawable to null."); 458 setIconDrawable(null); 459 return; 460 } 461 // Mutate the drawable loaded from resource so modifying its states does 462 // not affect other drawable instances loaded from the same resource. 463 setIconDrawable(iconDrawable.mutate()); 464 } 465 466 /** 467 * In order to make sure icon drawable is never set to null. Fall back to an 468 * empty drawable when icon needs to get reset. 469 * 470 * @param iconDrawable new drawable for icon. A value of <code>null</code> sets 471 * the icon drawable to the default drawable. 472 */ 473 private void setIconDrawable(Drawable iconDrawable) { 474 if (iconDrawable == null) { 475 mIconDrawable = mDefaultDrawable; 476 } else { 477 mIconDrawable = iconDrawable; 478 } 479 } 480 481 /** 482 * Initialize the mode cover with a mode theme color and a mode icon. 483 * 484 * @param colorId resource id of the mode theme color 485 * @param modeIconResourceId resource id of the icon drawable 486 */ 487 public void setupModeCover(int colorId, int modeIconResourceId) { 488 mBackgroundBitmap = null; 489 // Stop ongoing animation. 490 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 491 mPeepHoleAnimator.cancel(); 492 } 493 mAnimationType = IDLE; 494 mBackgroundColor = getResources().getColor(colorId); 495 // Sets new drawable. 496 updateIconDrawableByResourceId(modeIconResourceId); 497 mIconDrawable.setAlpha(ALPHA_FULLY_OPAQUE); 498 setVisibility(VISIBLE); 499 } 500 501 /** 502 * Hides the cover view and notifies the 503 * {@link com.android.camera.app.CameraAppUI.AnimationFinishedListener} of whether 504 * the hide animation is successfully finished. 505 * 506 * @param animationFinishedListener a listener that will get notified when the 507 * animation is finished. Could be <code>null</code>. 508 */ 509 public void hideModeCover( 510 final CameraAppUI.AnimationFinishedListener animationFinishedListener) { 511 if (mAnimationType != IDLE) { 512 // Nothing to hide. 513 if (animationFinishedListener != null) { 514 // Animation not successful. 515 animationFinishedListener.onAnimationFinished(false); 516 } 517 } else { 518 // Start fade out animation. 519 mAnimationType = FADE_OUT; 520 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f); 521 alphaAnimator.setDuration(FADE_OUT_DURATION_MS); 522 // Linear interpolation. 523 alphaAnimator.setInterpolator(null); 524 alphaAnimator.addListener(new Animator.AnimatorListener() { 525 @Override 526 public void onAnimationStart(Animator animation) { 527 528 } 529 530 @Override 531 public void onAnimationEnd(Animator animation) { 532 setVisibility(GONE); 533 setAlpha(1f); 534 if (animationFinishedListener != null) { 535 animationFinishedListener.onAnimationFinished(true); 536 mAnimationType = IDLE; 537 } 538 } 539 540 @Override 541 public void onAnimationCancel(Animator animation) { 542 543 } 544 545 @Override 546 public void onAnimationRepeat(Animator animation) { 547 548 } 549 }); 550 alphaAnimator.start(); 551 } 552 } 553 554 @Override 555 public void setAlpha(float alpha) { 556 super.setAlpha(alpha); 557 int alphaScaled = (int) (255f * getAlpha()); 558 mBackgroundColor = (mBackgroundColor & 0xFFFFFF) | (alphaScaled << 24); 559 mIconDrawable.setAlpha(alphaScaled); 560 } 561 562 /** 563 * Setup the mode cover with a screenshot. 564 */ 565 public void setupModeCover(Bitmap screenShot) { 566 mBackgroundBitmap = screenShot; 567 setVisibility(VISIBLE); 568 mAnimationType = SHOW_STATIC_IMAGE; 569 } 570 571 /** 572 * Hide the mode cover without animation. 573 */ 574 // TODO: Refactor this and define how cover should be hidden during cover setup 575 public void hideImageCover() { 576 mBackgroundBitmap = null; 577 setVisibility(GONE); 578 mAnimationType = IDLE; 579 } 580 } 581 582