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 
ModeTransitionView(Context context, AttributeSet attrs)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      */
updateShade()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      */
setScrollDistance(float scrollDistance)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 
getScrollDistance()166     public float getScrollDistance() {
167         return mScrollDistance;
168     }
169 
170     @Override
onDraw(Canvas canvas)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
onLayout(boolean changed, int left, int top, int right, int bottom)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      */
startPeepHoleAnimation()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      */
startPeepHoleAnimation(float x, float y)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
onTouchEvent(MotionEvent ev)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      */
snap()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 
snapShadeTo(int scrollDistance, int alpha)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      */
snapShadeTo(final int scrollDistance, final int alpha, final boolean snapToFullScreen)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      */
prepareToPullUpShade(int shadeColorId, int iconId, CameraAppUI.AnimationFinishedListener listener)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      */
prepareToPullDownShade(int shadeColorId, int modeIconResourceId, CameraAppUI.AnimationFinishedListener listener)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      */
prepareShadeAnimation(int animationType, int shadeColorId, int iconResId, CameraAppUI.AnimationFinishedListener listener)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      */
resetShade(int shadeColorId, int modeIconResourceId)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      */
updateIconDrawableByResourceId(int modeIconResourceId)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      */
setIconDrawable(Drawable iconDrawable)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      */
setupModeCover(int colorId, int modeIconResourceId)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      */
hideModeCover( final CameraAppUI.AnimationFinishedListener animationFinishedListener)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
setAlpha(float alpha)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      */
setupModeCover(Bitmap screenShot)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
hideImageCover()575     public void hideImageCover() {
576         mBackgroundBitmap = null;
577         setVisibility(GONE);
578         mAnimationType = IDLE;
579     }
580 }
581 
582