Home | History | Annotate | Download | only in assist
      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.assist;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.graphics.Canvas;
     24 import android.graphics.Outline;
     25 import android.graphics.Paint;
     26 import android.graphics.Rect;
     27 import android.util.AttributeSet;
     28 import android.view.View;
     29 import android.view.ViewOutlineProvider;
     30 import android.view.animation.AnimationUtils;
     31 import android.view.animation.Interpolator;
     32 import android.view.animation.OvershootInterpolator;
     33 import android.widget.FrameLayout;
     34 import android.widget.ImageView;
     35 
     36 import com.android.systemui.R;
     37 
     38 public class AssistOrbView extends FrameLayout {
     39 
     40     private final int mCircleMinSize;
     41     private final int mBaseMargin;
     42     private final int mStaticOffset;
     43     private final Paint mBackgroundPaint = new Paint();
     44     private final Rect mCircleRect = new Rect();
     45     private final Rect mStaticRect = new Rect();
     46     private final Interpolator mAppearInterpolator;
     47     private final Interpolator mDisappearInterpolator;
     48     private final Interpolator mOvershootInterpolator = new OvershootInterpolator();
     49 
     50     private boolean mClipToOutline;
     51     private final int mMaxElevation;
     52     private float mOutlineAlpha;
     53     private float mOffset;
     54     private float mCircleSize;
     55     private ImageView mLogo;
     56     private float mCircleAnimationEndValue;
     57 
     58     private ValueAnimator mOffsetAnimator;
     59     private ValueAnimator mCircleAnimator;
     60 
     61     private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener
     62             = new ValueAnimator.AnimatorUpdateListener() {
     63         @Override
     64         public void onAnimationUpdate(ValueAnimator animation) {
     65             applyCircleSize((float) animation.getAnimatedValue());
     66             updateElevation();
     67         }
     68     };
     69     private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
     70         @Override
     71         public void onAnimationEnd(Animator animation) {
     72             mCircleAnimator = null;
     73         }
     74     };
     75     private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener
     76             = new ValueAnimator.AnimatorUpdateListener() {
     77         @Override
     78         public void onAnimationUpdate(ValueAnimator animation) {
     79             mOffset = (float) animation.getAnimatedValue();
     80             updateLayout();
     81         }
     82     };
     83 
     84 
     85     public AssistOrbView(Context context) {
     86         this(context, null);
     87     }
     88 
     89     public AssistOrbView(Context context, AttributeSet attrs) {
     90         this(context, attrs, 0);
     91     }
     92 
     93     public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) {
     94         this(context, attrs, defStyleAttr, 0);
     95     }
     96 
     97     public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr,
     98             int defStyleRes) {
     99         super(context, attrs, defStyleAttr, defStyleRes);
    100         setOutlineProvider(new ViewOutlineProvider() {
    101             @Override
    102             public void getOutline(View view, Outline outline) {
    103                 if (mCircleSize > 0.0f) {
    104                     outline.setOval(mCircleRect);
    105                 } else {
    106                     outline.setEmpty();
    107                 }
    108                 outline.setAlpha(mOutlineAlpha);
    109             }
    110         });
    111         setWillNotDraw(false);
    112         mCircleMinSize = context.getResources().getDimensionPixelSize(
    113                 R.dimen.assist_orb_size);
    114         mBaseMargin = context.getResources().getDimensionPixelSize(
    115                 R.dimen.assist_orb_base_margin);
    116         mStaticOffset = context.getResources().getDimensionPixelSize(
    117                 R.dimen.assist_orb_travel_distance);
    118         mMaxElevation = context.getResources().getDimensionPixelSize(
    119                 R.dimen.assist_orb_elevation);
    120         mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
    121                 android.R.interpolator.linear_out_slow_in);
    122         mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
    123                 android.R.interpolator.fast_out_linear_in);
    124         mBackgroundPaint.setAntiAlias(true);
    125         mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color));
    126     }
    127 
    128     public ImageView getLogo() {
    129         return mLogo;
    130     }
    131 
    132     @Override
    133     protected void onDraw(Canvas canvas) {
    134         super.onDraw(canvas);
    135         drawBackground(canvas);
    136     }
    137 
    138     private void drawBackground(Canvas canvas) {
    139         canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2,
    140                 mBackgroundPaint);
    141     }
    142 
    143     @Override
    144     protected void onFinishInflate() {
    145         super.onFinishInflate();
    146         mLogo = (ImageView) findViewById(R.id.search_logo);
    147     }
    148 
    149     @Override
    150     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    151         mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight());
    152         if (changed) {
    153             updateCircleRect(mStaticRect, mStaticOffset, true);
    154         }
    155     }
    156 
    157     public void animateCircleSize(float circleSize, long duration,
    158             long startDelay, Interpolator interpolator) {
    159         if (circleSize == mCircleAnimationEndValue) {
    160             return;
    161         }
    162         if (mCircleAnimator != null) {
    163             mCircleAnimator.cancel();
    164         }
    165         mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize);
    166         mCircleAnimator.addUpdateListener(mCircleUpdateListener);
    167         mCircleAnimator.addListener(mClearAnimatorListener);
    168         mCircleAnimator.setInterpolator(interpolator);
    169         mCircleAnimator.setDuration(duration);
    170         mCircleAnimator.setStartDelay(startDelay);
    171         mCircleAnimator.start();
    172         mCircleAnimationEndValue = circleSize;
    173     }
    174 
    175     private void applyCircleSize(float circleSize) {
    176         mCircleSize = circleSize;
    177         updateLayout();
    178     }
    179 
    180     private void updateElevation() {
    181         float t = (mStaticOffset - mOffset) / (float) mStaticOffset;
    182         t = 1.0f - Math.max(t, 0.0f);
    183         float offset = t * mMaxElevation;
    184         setElevation(offset);
    185     }
    186 
    187     /**
    188      * Animates the offset to the edge of the screen.
    189      *
    190      * @param offset The offset to apply.
    191      * @param startDelay The desired start delay if animated.
    192      *
    193      * @param interpolator The desired interpolator if animated. If null,
    194      *                     a default interpolator will be taken designed for appearing or
    195      *                     disappearing.
    196      */
    197     private void animateOffset(float offset, long duration, long startDelay,
    198             Interpolator interpolator) {
    199         if (mOffsetAnimator != null) {
    200             mOffsetAnimator.removeAllListeners();
    201             mOffsetAnimator.cancel();
    202         }
    203         mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset);
    204         mOffsetAnimator.addUpdateListener(mOffsetUpdateListener);
    205         mOffsetAnimator.addListener(new AnimatorListenerAdapter() {
    206             @Override
    207             public void onAnimationEnd(Animator animation) {
    208                 mOffsetAnimator = null;
    209             }
    210         });
    211         mOffsetAnimator.setInterpolator(interpolator);
    212         mOffsetAnimator.setStartDelay(startDelay);
    213         mOffsetAnimator.setDuration(duration);
    214         mOffsetAnimator.start();
    215     }
    216 
    217     private void updateLayout() {
    218         updateCircleRect();
    219         updateLogo();
    220         invalidateOutline();
    221         invalidate();
    222         updateClipping();
    223     }
    224 
    225     private void updateClipping() {
    226         boolean clip = mCircleSize < mCircleMinSize;
    227         if (clip != mClipToOutline) {
    228             setClipToOutline(clip);
    229             mClipToOutline = clip;
    230         }
    231     }
    232 
    233     private void updateLogo() {
    234         float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f;
    235         float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f
    236                 - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f;
    237         float t = (mStaticOffset - mOffset) / (float) mStaticOffset;
    238         translationY += t * mStaticOffset * 0.1f;
    239         float alpha = 1.0f-t;
    240         alpha = Math.max((alpha - 0.5f) * 2.0f, 0);
    241         mLogo.setImageAlpha((int) (alpha * 255));
    242         mLogo.setTranslationX(translationX);
    243         mLogo.setTranslationY(translationY);
    244     }
    245 
    246     private void updateCircleRect() {
    247         updateCircleRect(mCircleRect, mOffset, false);
    248     }
    249 
    250     private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) {
    251         int left, top;
    252         float circleSize = useStaticSize ? mCircleMinSize : mCircleSize;
    253         left = (int) (getWidth() - circleSize) / 2;
    254         top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset);
    255         rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize));
    256     }
    257 
    258     public void startExitAnimation(long delay) {
    259         animateCircleSize(0, 200, delay, mDisappearInterpolator);
    260         animateOffset(0, 200, delay, mDisappearInterpolator);
    261     }
    262 
    263     public void startEnterAnimation() {
    264         applyCircleSize(0);
    265         post(new Runnable() {
    266             @Override
    267             public void run() {
    268                 animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator);
    269                 animateOffset(mStaticOffset, 400, 0 /* delay */, mAppearInterpolator);
    270             }
    271         });
    272     }
    273 
    274     public void reset() {
    275         mClipToOutline = false;
    276         mBackgroundPaint.setAlpha(255);
    277         mOutlineAlpha = 1.0f;
    278     }
    279 
    280     @Override
    281     public boolean hasOverlappingRendering() {
    282         // not really true but it's ok during an animation, as it's never permanent
    283         return false;
    284     }
    285 }
    286