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.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.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
30import android.graphics.Rect;
31import android.graphics.drawable.ColorDrawable;
32import android.graphics.drawable.Drawable;
33import android.util.AttributeSet;
34import android.view.GestureDetector;
35import android.view.MotionEvent;
36import android.view.View;
37
38import com.android.camera.app.CameraAppUI;
39import com.android.camera.debug.Log;
40import com.android.camera.util.Gusterpolator;
41import 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 */
47public 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