Home | History | Annotate | Download | only in systemui
      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 
     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