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