• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.systemui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.PropertyValuesHolder;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.graphics.Canvas;
25 import android.graphics.Outline;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.util.AttributeSet;
29 import android.view.View;
30 import android.view.ViewOutlineProvider;
31 import android.view.animation.AnimationUtils;
32 import android.view.animation.Interpolator;
33 import android.view.animation.LinearInterpolator;
34 import android.widget.FrameLayout;
35 import android.widget.ImageView;
36 import com.android.systemui.statusbar.phone.PhoneStatusBar;
37 
38 import java.util.ArrayList;
39 
40 public 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 
SearchPanelCircleView(Context context)93     public SearchPanelCircleView(Context context) {
94         this(context, null);
95     }
96 
SearchPanelCircleView(Context context, AttributeSet attrs)97     public SearchPanelCircleView(Context context, AttributeSet attrs) {
98         this(context, attrs, 0);
99     }
100 
SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr)101     public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
102         this(context, attrs, defStyleAttr, 0);
103     }
104 
SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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
onDraw(Canvas canvas)141     protected void onDraw(Canvas canvas) {
142         super.onDraw(canvas);
143         drawBackground(canvas);
144         drawRipples(canvas);
145     }
146 
drawRipples(Canvas canvas)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 
drawBackground(Canvas canvas)154     private void drawBackground(Canvas canvas) {
155         canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2,
156                 mBackgroundPaint);
157     }
158 
159     @Override
onFinishInflate()160     protected void onFinishInflate() {
161         super.onFinishInflate();
162         mLogo = (ImageView) findViewById(R.id.search_logo);
163     }
164 
165     @Override
onLayout(boolean changed, int l, int t, int r, int b)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 
setCircleSize(float circleSize)173     public void setCircleSize(float circleSize) {
174         setCircleSize(circleSize, false, null, 0, null);
175     }
176 
setCircleSize(float circleSize, boolean animated, final Runnable endRunnable, int startDelay, Interpolator interpolator)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 
applyCircleSize(float circleSize)221     private void applyCircleSize(float circleSize) {
222         mCircleSize = circleSize;
223         updateLayout();
224     }
225 
updateElevation()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      */
setOffset(float offset)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      */
setOffset(float offset, boolean animate, int startDelay, Interpolator interpolator, final Runnable endRunnable)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 
updateLayout()287     private void updateLayout() {
288         updateCircleRect();
289         updateLogo();
290         invalidateOutline();
291         invalidate();
292         updateClipping();
293     }
294 
updateClipping()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 
reset()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      */
isAnimationRunning(boolean enterAnimation)503     public boolean isAnimationRunning(boolean enterAnimation) {
504         return mOffsetAnimator != null && (enterAnimation == mOffsetAnimatingIn);
505     }
506 
performOnAnimationFinished(final Runnable runnable)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 
setAnimatingOut(boolean animatingOut)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      */
isAnimatingOut()532     public boolean isAnimatingOut() {
533         return mAnimatingOut;
534     }
535 
536     @Override
hasOverlappingRendering()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 
Ripple(float x, float y, float endRadius)549         Ripple(float x, float y, float endRadius) {
550             this.x = x;
551             this.y = y;
552             this.endRadius = endRadius;
553         }
554 
start()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 
draw(Canvas canvas)586         public void draw(Canvas canvas) {
587             mRipplePaint.setAlpha((int) (alpha * 255));
588             canvas.drawCircle(x, y, radius, mRipplePaint);
589         }
590     }
591 
592 }
593