1/*
2 * Copyright (C) 2014 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.systemui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.PropertyValuesHolder;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.graphics.Canvas;
25import android.graphics.Outline;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.util.AttributeSet;
29import android.view.View;
30import android.view.ViewOutlineProvider;
31import android.view.animation.AnimationUtils;
32import android.view.animation.Interpolator;
33import android.view.animation.LinearInterpolator;
34import android.widget.FrameLayout;
35import android.widget.ImageView;
36import com.android.systemui.statusbar.phone.PhoneStatusBar;
37
38import java.util.ArrayList;
39
40public class SearchPanelCircleView extends FrameLayout {
41
42    private final int mCircleMinSize;
43    private final int mBaseMargin;
44    private final int mStaticOffset;
45    private final Paint mBackgroundPaint = new Paint();
46    private final Paint mRipplePaint = new Paint();
47    private final Rect mCircleRect = new Rect();
48    private final Rect mStaticRect = new Rect();
49    private final Interpolator mFastOutSlowInInterpolator;
50    private final Interpolator mAppearInterpolator;
51    private final Interpolator mDisappearInterpolator;
52
53    private boolean mClipToOutline;
54    private final int mMaxElevation;
55    private boolean mAnimatingOut;
56    private float mOutlineAlpha;
57    private float mOffset;
58    private float mCircleSize;
59    private boolean mHorizontal;
60    private boolean mCircleHidden;
61    private ImageView mLogo;
62    private boolean mDraggedFarEnough;
63    private boolean mOffsetAnimatingIn;
64    private float mCircleAnimationEndValue;
65    private ArrayList<Ripple> mRipples = new ArrayList<Ripple>();
66
67    private ValueAnimator mOffsetAnimator;
68    private ValueAnimator mCircleAnimator;
69    private ValueAnimator mFadeOutAnimator;
70    private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener
71            = new ValueAnimator.AnimatorUpdateListener() {
72        @Override
73        public void onAnimationUpdate(ValueAnimator animation) {
74            applyCircleSize((float) animation.getAnimatedValue());
75            updateElevation();
76        }
77    };
78    private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
79        @Override
80        public void onAnimationEnd(Animator animation) {
81            mCircleAnimator = null;
82        }
83    };
84    private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener
85            = new ValueAnimator.AnimatorUpdateListener() {
86        @Override
87        public void onAnimationUpdate(ValueAnimator animation) {
88            setOffset((float) animation.getAnimatedValue());
89        }
90    };
91
92
93    public SearchPanelCircleView(Context context) {
94        this(context, null);
95    }
96
97    public SearchPanelCircleView(Context context, AttributeSet attrs) {
98        this(context, attrs, 0);
99    }
100
101    public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
102        this(context, attrs, defStyleAttr, 0);
103    }
104
105    public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr,
106            int defStyleRes) {
107        super(context, attrs, defStyleAttr, defStyleRes);
108        setOutlineProvider(new ViewOutlineProvider() {
109            @Override
110            public void getOutline(View view, Outline outline) {
111                if (mCircleSize > 0.0f) {
112                    outline.setOval(mCircleRect);
113                } else {
114                    outline.setEmpty();
115                }
116                outline.setAlpha(mOutlineAlpha);
117            }
118        });
119        setWillNotDraw(false);
120        mCircleMinSize = context.getResources().getDimensionPixelSize(
121                R.dimen.search_panel_circle_size);
122        mBaseMargin = context.getResources().getDimensionPixelSize(
123                R.dimen.search_panel_circle_base_margin);
124        mStaticOffset = context.getResources().getDimensionPixelSize(
125                R.dimen.search_panel_circle_travel_distance);
126        mMaxElevation = context.getResources().getDimensionPixelSize(
127                R.dimen.search_panel_circle_elevation);
128        mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
129                android.R.interpolator.linear_out_slow_in);
130        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext,
131                android.R.interpolator.fast_out_slow_in);
132        mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
133                android.R.interpolator.fast_out_linear_in);
134        mBackgroundPaint.setAntiAlias(true);
135        mBackgroundPaint.setColor(getResources().getColor(R.color.search_panel_circle_color));
136        mRipplePaint.setColor(getResources().getColor(R.color.search_panel_ripple_color));
137        mRipplePaint.setAntiAlias(true);
138    }
139
140    @Override
141    protected void onDraw(Canvas canvas) {
142        super.onDraw(canvas);
143        drawBackground(canvas);
144        drawRipples(canvas);
145    }
146
147    private void drawRipples(Canvas canvas) {
148        for (int i = 0; i < mRipples.size(); i++) {
149            Ripple ripple = mRipples.get(i);
150            ripple.draw(canvas);
151        }
152    }
153
154    private void drawBackground(Canvas canvas) {
155        canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2,
156                mBackgroundPaint);
157    }
158
159    @Override
160    protected void onFinishInflate() {
161        super.onFinishInflate();
162        mLogo = (ImageView) findViewById(R.id.search_logo);
163    }
164
165    @Override
166    protected void onLayout(boolean changed, int l, int t, int r, int b) {
167        mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight());
168        if (changed) {
169            updateCircleRect(mStaticRect, mStaticOffset, true);
170        }
171    }
172
173    public void setCircleSize(float circleSize) {
174        setCircleSize(circleSize, false, null, 0, null);
175    }
176
177    public void setCircleSize(float circleSize, boolean animated, final Runnable endRunnable,
178            int startDelay, Interpolator interpolator) {
179        boolean isAnimating = mCircleAnimator != null;
180        boolean animationPending = isAnimating && !mCircleAnimator.isRunning();
181        boolean animatingOut = isAnimating && mCircleAnimationEndValue == 0;
182        if (animated || animationPending || animatingOut) {
183            if (isAnimating) {
184                if (circleSize == mCircleAnimationEndValue) {
185                    return;
186                }
187                mCircleAnimator.cancel();
188            }
189            mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize);
190            mCircleAnimator.addUpdateListener(mCircleUpdateListener);
191            mCircleAnimator.addListener(mClearAnimatorListener);
192            mCircleAnimator.addListener(new AnimatorListenerAdapter() {
193                @Override
194                public void onAnimationEnd(Animator animation) {
195                    if (endRunnable != null) {
196                        endRunnable.run();
197                    }
198                }
199            });
200            Interpolator desiredInterpolator = interpolator != null ? interpolator
201                    : circleSize == 0 ? mDisappearInterpolator : mAppearInterpolator;
202            mCircleAnimator.setInterpolator(desiredInterpolator);
203            mCircleAnimator.setDuration(300);
204            mCircleAnimator.setStartDelay(startDelay);
205            mCircleAnimator.start();
206            mCircleAnimationEndValue = circleSize;
207        } else {
208            if (isAnimating) {
209                float diff = circleSize - mCircleAnimationEndValue;
210                PropertyValuesHolder[] values = mCircleAnimator.getValues();
211                values[0].setFloatValues(diff, circleSize);
212                mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
213                mCircleAnimationEndValue = circleSize;
214            } else {
215                applyCircleSize(circleSize);
216                updateElevation();
217            }
218        }
219    }
220
221    private void applyCircleSize(float circleSize) {
222        mCircleSize = circleSize;
223        updateLayout();
224    }
225
226    private void updateElevation() {
227        float t = (mStaticOffset - mOffset) / (float) mStaticOffset;
228        t = 1.0f - Math.max(t, 0.0f);
229        float offset = t * mMaxElevation;
230        setElevation(offset);
231    }
232
233    /**
234     * Sets the offset to the edge of the screen. By default this not not animated.
235     *
236     * @param offset The offset to apply.
237     */
238    public void setOffset(float offset) {
239        setOffset(offset, false, 0, null, null);
240    }
241
242    /**
243     * Sets the offset to the edge of the screen.
244     *
245     * @param offset The offset to apply.
246     * @param animate Whether an animation should be performed.
247     * @param startDelay The desired start delay if animated.
248     * @param interpolator The desired interpolator if animated. If null,
249     *                     a default interpolator will be taken designed for appearing or
250     *                     disappearing.
251     * @param endRunnable The end runnable which should be executed when the animation is finished.
252     */
253    private void setOffset(float offset, boolean animate, int startDelay,
254            Interpolator interpolator, final Runnable endRunnable) {
255        if (!animate) {
256            mOffset = offset;
257            updateLayout();
258            if (endRunnable != null) {
259                endRunnable.run();
260            }
261        } else {
262            if (mOffsetAnimator != null) {
263                mOffsetAnimator.removeAllListeners();
264                mOffsetAnimator.cancel();
265            }
266            mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset);
267            mOffsetAnimator.addUpdateListener(mOffsetUpdateListener);
268            mOffsetAnimator.addListener(new AnimatorListenerAdapter() {
269                @Override
270                public void onAnimationEnd(Animator animation) {
271                    mOffsetAnimator = null;
272                    if (endRunnable != null) {
273                        endRunnable.run();
274                    }
275                }
276            });
277            Interpolator desiredInterpolator = interpolator != null ?
278                    interpolator : offset == 0 ? mDisappearInterpolator : mAppearInterpolator;
279            mOffsetAnimator.setInterpolator(desiredInterpolator);
280            mOffsetAnimator.setStartDelay(startDelay);
281            mOffsetAnimator.setDuration(300);
282            mOffsetAnimator.start();
283            mOffsetAnimatingIn = offset != 0;
284        }
285    }
286
287    private void updateLayout() {
288        updateCircleRect();
289        updateLogo();
290        invalidateOutline();
291        invalidate();
292        updateClipping();
293    }
294
295    private void updateClipping() {
296        boolean clip = mCircleSize < mCircleMinSize || !mRipples.isEmpty();
297        if (clip != mClipToOutline) {
298            setClipToOutline(clip);
299            mClipToOutline = clip;
300        }
301    }
302
303    private void updateLogo() {
304        boolean exitAnimationRunning = mFadeOutAnimator != null;
305        Rect rect = exitAnimationRunning ? mCircleRect : mStaticRect;
306        float translationX = (rect.left + rect.right) / 2.0f - mLogo.getWidth() / 2.0f;
307        float translationY = (rect.top + rect.bottom) / 2.0f - mLogo.getHeight() / 2.0f;
308        float t = (mStaticOffset - mOffset) / (float) mStaticOffset;
309        if (!exitAnimationRunning) {
310            if (mHorizontal) {
311                translationX += t * mStaticOffset * 0.3f;
312            } else {
313                translationY += t * mStaticOffset * 0.3f;
314            }
315            float alpha = 1.0f-t;
316            alpha = Math.max((alpha - 0.5f) * 2.0f, 0);
317            mLogo.setAlpha(alpha);
318        } else {
319            translationY += (mOffset - mStaticOffset) / 2;
320        }
321        mLogo.setTranslationX(translationX);
322        mLogo.setTranslationY(translationY);
323    }
324
325    private void updateCircleRect() {
326        updateCircleRect(mCircleRect, mOffset, false);
327    }
328
329    private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) {
330        int left, top;
331        float circleSize = useStaticSize ? mCircleMinSize : mCircleSize;
332        if (mHorizontal) {
333            left = (int) (getWidth() - circleSize / 2 - mBaseMargin - offset);
334            top = (int) ((getHeight() - circleSize) / 2);
335        } else {
336            left = (int) (getWidth() - circleSize) / 2;
337            top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset);
338        }
339        rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize));
340    }
341
342    public void setHorizontal(boolean horizontal) {
343        mHorizontal = horizontal;
344        updateCircleRect(mStaticRect, mStaticOffset, true);
345        updateLayout();
346    }
347
348    public void setDragDistance(float distance) {
349        if (!mAnimatingOut && (!mCircleHidden || mDraggedFarEnough)) {
350            float circleSize = mCircleMinSize + rubberband(distance);
351            setCircleSize(circleSize);
352        }
353
354    }
355
356    private float rubberband(float diff) {
357        return (float) Math.pow(Math.abs(diff), 0.6f);
358    }
359
360    public void startAbortAnimation(Runnable endRunnable) {
361        if (mAnimatingOut) {
362            if (endRunnable != null) {
363                endRunnable.run();
364            }
365            return;
366        }
367        setCircleSize(0, true, null, 0, null);
368        setOffset(0, true, 0, null, endRunnable);
369        mCircleHidden = true;
370    }
371
372    public void startEnterAnimation() {
373        if (mAnimatingOut) {
374            return;
375        }
376        applyCircleSize(0);
377        setOffset(0);
378        setCircleSize(mCircleMinSize, true, null, 50, null);
379        setOffset(mStaticOffset, true, 50, null, null);
380        mCircleHidden = false;
381    }
382
383
384    public void startExitAnimation(final Runnable endRunnable) {
385        if (!mHorizontal) {
386            float offset = getHeight() / 2.0f;
387            setOffset(offset - mBaseMargin, true, 50, mFastOutSlowInInterpolator, null);
388            float xMax = getWidth() / 2;
389            float yMax = getHeight() / 2;
390            float maxRadius = (float) Math.ceil(Math.hypot(xMax, yMax) * 2);
391            setCircleSize(maxRadius, true, null, 50, mFastOutSlowInInterpolator);
392            performExitFadeOutAnimation(50, 300, endRunnable);
393        } else {
394
395            // when in landscape, we don't wan't the animation as it interferes with the general
396            // rotation animation to the homescreen.
397            endRunnable.run();
398        }
399    }
400
401    private void performExitFadeOutAnimation(int startDelay, int duration,
402            final Runnable endRunnable) {
403        mFadeOutAnimator = ValueAnimator.ofFloat(mBackgroundPaint.getAlpha() / 255.0f, 0.0f);
404
405        // Linear since we are animating multiple values
406        mFadeOutAnimator.setInterpolator(new LinearInterpolator());
407        mFadeOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
408            @Override
409            public void onAnimationUpdate(ValueAnimator animation) {
410                float animatedFraction = animation.getAnimatedFraction();
411                float logoValue = animatedFraction > 0.5f ? 1.0f : animatedFraction / 0.5f;
412                logoValue = PhoneStatusBar.ALPHA_OUT.getInterpolation(1.0f - logoValue);
413                float backgroundValue = animatedFraction < 0.2f ? 0.0f :
414                        PhoneStatusBar.ALPHA_OUT.getInterpolation((animatedFraction - 0.2f) / 0.8f);
415                backgroundValue = 1.0f - backgroundValue;
416                mBackgroundPaint.setAlpha((int) (backgroundValue * 255));
417                mOutlineAlpha = backgroundValue;
418                mLogo.setAlpha(logoValue);
419                invalidateOutline();
420                invalidate();
421            }
422        });
423        mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
424            @Override
425            public void onAnimationEnd(Animator animation) {
426                if (endRunnable != null) {
427                    endRunnable.run();
428                }
429                mLogo.setAlpha(1.0f);
430                mBackgroundPaint.setAlpha(255);
431                mOutlineAlpha = 1.0f;
432                mFadeOutAnimator = null;
433            }
434        });
435        mFadeOutAnimator.setStartDelay(startDelay);
436        mFadeOutAnimator.setDuration(duration);
437        mFadeOutAnimator.start();
438    }
439
440    public void setDraggedFarEnough(boolean farEnough) {
441        if (farEnough != mDraggedFarEnough) {
442            if (farEnough) {
443                if (mCircleHidden) {
444                    startEnterAnimation();
445                }
446                if (mOffsetAnimator == null) {
447                    addRipple();
448                } else {
449                    postDelayed(new Runnable() {
450                        @Override
451                        public void run() {
452                            addRipple();
453                        }
454                    }, 100);
455                }
456            } else {
457                startAbortAnimation(null);
458            }
459            mDraggedFarEnough = farEnough;
460        }
461
462    }
463
464    private void addRipple() {
465        if (mRipples.size() > 1) {
466            // we only want 2 ripples at the time
467            return;
468        }
469        float xInterpolation, yInterpolation;
470        if (mHorizontal) {
471            xInterpolation = 0.75f;
472            yInterpolation = 0.5f;
473        } else {
474            xInterpolation = 0.5f;
475            yInterpolation = 0.75f;
476        }
477        float circleCenterX = mStaticRect.left * (1.0f - xInterpolation)
478                + mStaticRect.right * xInterpolation;
479        float circleCenterY = mStaticRect.top * (1.0f - yInterpolation)
480                + mStaticRect.bottom * yInterpolation;
481        float radius = Math.max(mCircleSize, mCircleMinSize * 1.25f) * 0.75f;
482        Ripple ripple = new Ripple(circleCenterX, circleCenterY, radius);
483        ripple.start();
484    }
485
486    public void reset() {
487        mDraggedFarEnough = false;
488        mAnimatingOut = false;
489        mCircleHidden = true;
490        mClipToOutline = false;
491        if (mFadeOutAnimator != null) {
492            mFadeOutAnimator.cancel();
493        }
494        mBackgroundPaint.setAlpha(255);
495        mOutlineAlpha = 1.0f;
496    }
497
498    /**
499     * Check if an animation is currently running
500     *
501     * @param enterAnimation Is the animating queried the enter animation.
502     */
503    public boolean isAnimationRunning(boolean enterAnimation) {
504        return mOffsetAnimator != null && (enterAnimation == mOffsetAnimatingIn);
505    }
506
507    public void performOnAnimationFinished(final Runnable runnable) {
508        if (mOffsetAnimator != null) {
509            mOffsetAnimator.addListener(new AnimatorListenerAdapter() {
510                @Override
511                public void onAnimationEnd(Animator animation) {
512                    if (runnable != null) {
513                        runnable.run();
514                    }
515                }
516            });
517        } else {
518            if (runnable != null) {
519                runnable.run();
520            }
521        }
522    }
523
524    public void setAnimatingOut(boolean animatingOut) {
525        mAnimatingOut = animatingOut;
526    }
527
528    /**
529     * @return Whether the circle is currently launching to the search activity or aborting the
530     * interaction
531     */
532    public boolean isAnimatingOut() {
533        return mAnimatingOut;
534    }
535
536    @Override
537    public boolean hasOverlappingRendering() {
538        // not really true but it's ok during an animation, as it's never permanent
539        return false;
540    }
541
542    private class Ripple {
543        float x;
544        float y;
545        float radius;
546        float endRadius;
547        float alpha;
548
549        Ripple(float x, float y, float endRadius) {
550            this.x = x;
551            this.y = y;
552            this.endRadius = endRadius;
553        }
554
555        void start() {
556            ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f);
557
558            // Linear since we are animating multiple values
559            animator.setInterpolator(new LinearInterpolator());
560            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
561                @Override
562                public void onAnimationUpdate(ValueAnimator animation) {
563                    alpha = 1.0f - animation.getAnimatedFraction();
564                    alpha = mDisappearInterpolator.getInterpolation(alpha);
565                    radius = mAppearInterpolator.getInterpolation(animation.getAnimatedFraction());
566                    radius *= endRadius;
567                    invalidate();
568                }
569            });
570            animator.addListener(new AnimatorListenerAdapter() {
571                @Override
572                public void onAnimationEnd(Animator animation) {
573                    mRipples.remove(Ripple.this);
574                    updateClipping();
575                }
576
577                public void onAnimationStart(Animator animation) {
578                    mRipples.add(Ripple.this);
579                    updateClipping();
580                }
581            });
582            animator.setDuration(400);
583            animator.start();
584        }
585
586        public void draw(Canvas canvas) {
587            mRipplePaint.setAlpha((int) (alpha * 255));
588            canvas.drawCircle(x, y, radius, mRipplePaint);
589        }
590    }
591
592}
593