/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.assist; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; import android.view.animation.Interpolator; import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.systemui.Interpolators; import com.android.systemui.R; public class AssistOrbView extends FrameLayout { private final int mCircleMinSize; private final int mBaseMargin; private final int mStaticOffset; private final Paint mBackgroundPaint = new Paint(); private final Rect mCircleRect = new Rect(); private final Rect mStaticRect = new Rect(); private final Interpolator mOvershootInterpolator = new OvershootInterpolator(); private boolean mClipToOutline; private final int mMaxElevation; private float mOutlineAlpha; private float mOffset; private float mCircleSize; private ImageView mLogo; private float mCircleAnimationEndValue; private ValueAnimator mOffsetAnimator; private ValueAnimator mCircleAnimator; private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { applyCircleSize((float) animation.getAnimatedValue()); updateElevation(); } }; private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCircleAnimator = null; } }; private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mOffset = (float) animation.getAnimatedValue(); updateLayout(); } }; public AssistOrbView(Context context) { this(context, null); } public AssistOrbView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { if (mCircleSize > 0.0f) { outline.setOval(mCircleRect); } else { outline.setEmpty(); } outline.setAlpha(mOutlineAlpha); } }); setWillNotDraw(false); mCircleMinSize = context.getResources().getDimensionPixelSize( R.dimen.assist_orb_size); mBaseMargin = context.getResources().getDimensionPixelSize( R.dimen.assist_orb_base_margin); mStaticOffset = context.getResources().getDimensionPixelSize( R.dimen.assist_orb_travel_distance); mMaxElevation = context.getResources().getDimensionPixelSize( R.dimen.assist_orb_elevation); mBackgroundPaint.setAntiAlias(true); mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color)); } public ImageView getLogo() { return mLogo; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBackground(canvas); } private void drawBackground(Canvas canvas) { canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, mBackgroundPaint); } @Override protected void onFinishInflate() { super.onFinishInflate(); mLogo = findViewById(R.id.search_logo); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); if (changed) { updateCircleRect(mStaticRect, mStaticOffset, true); } } public void animateCircleSize(float circleSize, long duration, long startDelay, Interpolator interpolator) { if (circleSize == mCircleAnimationEndValue) { return; } if (mCircleAnimator != null) { mCircleAnimator.cancel(); } mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); mCircleAnimator.addUpdateListener(mCircleUpdateListener); mCircleAnimator.addListener(mClearAnimatorListener); mCircleAnimator.setInterpolator(interpolator); mCircleAnimator.setDuration(duration); mCircleAnimator.setStartDelay(startDelay); mCircleAnimator.start(); mCircleAnimationEndValue = circleSize; } private void applyCircleSize(float circleSize) { mCircleSize = circleSize; updateLayout(); } private void updateElevation() { float t = (mStaticOffset - mOffset) / (float) mStaticOffset; t = 1.0f - Math.max(t, 0.0f); float offset = t * mMaxElevation; setElevation(offset); } /** * Animates the offset to the edge of the screen. * * @param offset The offset to apply. * @param startDelay The desired start delay if animated. * * @param interpolator The desired interpolator if animated. If null, * a default interpolator will be taken designed for appearing or * disappearing. */ private void animateOffset(float offset, long duration, long startDelay, Interpolator interpolator) { if (mOffsetAnimator != null) { mOffsetAnimator.removeAllListeners(); mOffsetAnimator.cancel(); } mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); mOffsetAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mOffsetAnimator = null; } }); mOffsetAnimator.setInterpolator(interpolator); mOffsetAnimator.setStartDelay(startDelay); mOffsetAnimator.setDuration(duration); mOffsetAnimator.start(); } private void updateLayout() { updateCircleRect(); updateLogo(); invalidateOutline(); invalidate(); updateClipping(); } private void updateClipping() { boolean clip = mCircleSize < mCircleMinSize; if (clip != mClipToOutline) { setClipToOutline(clip); mClipToOutline = clip; } } private void updateLogo() { float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f; float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f; float t = (mStaticOffset - mOffset) / (float) mStaticOffset; translationY += t * mStaticOffset * 0.1f; float alpha = 1.0f-t; alpha = Math.max((alpha - 0.5f) * 2.0f, 0); mLogo.setImageAlpha((int) (alpha * 255)); mLogo.setTranslationX(translationX); mLogo.setTranslationY(translationY); } private void updateCircleRect() { updateCircleRect(mCircleRect, mOffset, false); } private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { int left, top; float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; left = (int) (getWidth() - circleSize) / 2; top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); } public void startExitAnimation(long delay) { animateCircleSize(0, 200, delay, Interpolators.FAST_OUT_LINEAR_IN); animateOffset(0, 200, delay, Interpolators.FAST_OUT_LINEAR_IN); } public void startEnterAnimation() { applyCircleSize(0); post(new Runnable() { @Override public void run() { animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator); animateOffset(mStaticOffset, 400, 0 /* delay */, Interpolators.LINEAR_OUT_SLOW_IN); } }); } public void reset() { mClipToOutline = false; mBackgroundPaint.setAlpha(255); mOutlineAlpha = 1.0f; } @Override public boolean hasOverlappingRendering() { // not really true but it's ok during an animation, as it's never permanent return false; } }