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.assist;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.graphics.Canvas;
24import android.graphics.Outline;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.util.AttributeSet;
28import android.view.View;
29import android.view.ViewOutlineProvider;
30import android.view.animation.Interpolator;
31import android.view.animation.OvershootInterpolator;
32import android.widget.FrameLayout;
33import android.widget.ImageView;
34
35import com.android.systemui.Interpolators;
36import com.android.systemui.R;
37
38public 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 = (ImageView) 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