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