| /* |
| * 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; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.PropertyValuesHolder; |
| 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.AnimationUtils; |
| import android.view.animation.Interpolator; |
| import android.view.animation.LinearInterpolator; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| import com.android.systemui.statusbar.phone.PhoneStatusBar; |
| |
| import java.util.ArrayList; |
| |
| public class SearchPanelCircleView extends FrameLayout { |
| |
| private final int mCircleMinSize; |
| private final int mBaseMargin; |
| private final int mStaticOffset; |
| private final Paint mBackgroundPaint = new Paint(); |
| private final Paint mRipplePaint = new Paint(); |
| private final Rect mCircleRect = new Rect(); |
| private final Rect mStaticRect = new Rect(); |
| private final Interpolator mFastOutSlowInInterpolator; |
| private final Interpolator mAppearInterpolator; |
| private final Interpolator mDisappearInterpolator; |
| |
| private boolean mClipToOutline; |
| private final int mMaxElevation; |
| private boolean mAnimatingOut; |
| private float mOutlineAlpha; |
| private float mOffset; |
| private float mCircleSize; |
| private boolean mHorizontal; |
| private boolean mCircleHidden; |
| private ImageView mLogo; |
| private boolean mDraggedFarEnough; |
| private boolean mOffsetAnimatingIn; |
| private float mCircleAnimationEndValue; |
| private ArrayList<Ripple> mRipples = new ArrayList<Ripple>(); |
| |
| private ValueAnimator mOffsetAnimator; |
| private ValueAnimator mCircleAnimator; |
| private ValueAnimator mFadeOutAnimator; |
| 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) { |
| setOffset((float) animation.getAnimatedValue()); |
| } |
| }; |
| |
| |
| public SearchPanelCircleView(Context context) { |
| this(context, null); |
| } |
| |
| public SearchPanelCircleView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public SearchPanelCircleView(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.search_panel_circle_size); |
| mBaseMargin = context.getResources().getDimensionPixelSize( |
| R.dimen.search_panel_circle_base_margin); |
| mStaticOffset = context.getResources().getDimensionPixelSize( |
| R.dimen.search_panel_circle_travel_distance); |
| mMaxElevation = context.getResources().getDimensionPixelSize( |
| R.dimen.search_panel_circle_elevation); |
| mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, |
| android.R.interpolator.linear_out_slow_in); |
| mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, |
| android.R.interpolator.fast_out_slow_in); |
| mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, |
| android.R.interpolator.fast_out_linear_in); |
| mBackgroundPaint.setAntiAlias(true); |
| mBackgroundPaint.setColor(getResources().getColor(R.color.search_panel_circle_color)); |
| mRipplePaint.setColor(getResources().getColor(R.color.search_panel_ripple_color)); |
| mRipplePaint.setAntiAlias(true); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| drawBackground(canvas); |
| drawRipples(canvas); |
| } |
| |
| private void drawRipples(Canvas canvas) { |
| for (int i = 0; i < mRipples.size(); i++) { |
| Ripple ripple = mRipples.get(i); |
| ripple.draw(canvas); |
| } |
| } |
| |
| private void drawBackground(Canvas canvas) { |
| canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, |
| mBackgroundPaint); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mLogo = (ImageView) 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 setCircleSize(float circleSize) { |
| setCircleSize(circleSize, false, null, 0, null); |
| } |
| |
| public void setCircleSize(float circleSize, boolean animated, final Runnable endRunnable, |
| int startDelay, Interpolator interpolator) { |
| boolean isAnimating = mCircleAnimator != null; |
| boolean animationPending = isAnimating && !mCircleAnimator.isRunning(); |
| boolean animatingOut = isAnimating && mCircleAnimationEndValue == 0; |
| if (animated || animationPending || animatingOut) { |
| if (isAnimating) { |
| if (circleSize == mCircleAnimationEndValue) { |
| return; |
| } |
| mCircleAnimator.cancel(); |
| } |
| mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); |
| mCircleAnimator.addUpdateListener(mCircleUpdateListener); |
| mCircleAnimator.addListener(mClearAnimatorListener); |
| mCircleAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (endRunnable != null) { |
| endRunnable.run(); |
| } |
| } |
| }); |
| Interpolator desiredInterpolator = interpolator != null ? interpolator |
| : circleSize == 0 ? mDisappearInterpolator : mAppearInterpolator; |
| mCircleAnimator.setInterpolator(desiredInterpolator); |
| mCircleAnimator.setDuration(300); |
| mCircleAnimator.setStartDelay(startDelay); |
| mCircleAnimator.start(); |
| mCircleAnimationEndValue = circleSize; |
| } else { |
| if (isAnimating) { |
| float diff = circleSize - mCircleAnimationEndValue; |
| PropertyValuesHolder[] values = mCircleAnimator.getValues(); |
| values[0].setFloatValues(diff, circleSize); |
| mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime()); |
| mCircleAnimationEndValue = circleSize; |
| } else { |
| applyCircleSize(circleSize); |
| updateElevation(); |
| } |
| } |
| } |
| |
| 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); |
| } |
| |
| /** |
| * Sets the offset to the edge of the screen. By default this not not animated. |
| * |
| * @param offset The offset to apply. |
| */ |
| public void setOffset(float offset) { |
| setOffset(offset, false, 0, null, null); |
| } |
| |
| /** |
| * Sets the offset to the edge of the screen. |
| * |
| * @param offset The offset to apply. |
| * @param animate Whether an animation should be performed. |
| * @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. |
| * @param endRunnable The end runnable which should be executed when the animation is finished. |
| */ |
| private void setOffset(float offset, boolean animate, int startDelay, |
| Interpolator interpolator, final Runnable endRunnable) { |
| if (!animate) { |
| mOffset = offset; |
| updateLayout(); |
| if (endRunnable != null) { |
| endRunnable.run(); |
| } |
| } else { |
| 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; |
| if (endRunnable != null) { |
| endRunnable.run(); |
| } |
| } |
| }); |
| Interpolator desiredInterpolator = interpolator != null ? |
| interpolator : offset == 0 ? mDisappearInterpolator : mAppearInterpolator; |
| mOffsetAnimator.setInterpolator(desiredInterpolator); |
| mOffsetAnimator.setStartDelay(startDelay); |
| mOffsetAnimator.setDuration(300); |
| mOffsetAnimator.start(); |
| mOffsetAnimatingIn = offset != 0; |
| } |
| } |
| |
| private void updateLayout() { |
| updateCircleRect(); |
| updateLogo(); |
| invalidateOutline(); |
| invalidate(); |
| updateClipping(); |
| } |
| |
| private void updateClipping() { |
| boolean clip = mCircleSize < mCircleMinSize || !mRipples.isEmpty(); |
| if (clip != mClipToOutline) { |
| setClipToOutline(clip); |
| mClipToOutline = clip; |
| } |
| } |
| |
| private void updateLogo() { |
| boolean exitAnimationRunning = mFadeOutAnimator != null; |
| Rect rect = exitAnimationRunning ? mCircleRect : mStaticRect; |
| float translationX = (rect.left + rect.right) / 2.0f - mLogo.getWidth() / 2.0f; |
| float translationY = (rect.top + rect.bottom) / 2.0f - mLogo.getHeight() / 2.0f; |
| float t = (mStaticOffset - mOffset) / (float) mStaticOffset; |
| if (!exitAnimationRunning) { |
| if (mHorizontal) { |
| translationX += t * mStaticOffset * 0.3f; |
| } else { |
| translationY += t * mStaticOffset * 0.3f; |
| } |
| float alpha = 1.0f-t; |
| alpha = Math.max((alpha - 0.5f) * 2.0f, 0); |
| mLogo.setAlpha(alpha); |
| } else { |
| translationY += (mOffset - mStaticOffset) / 2; |
| } |
| 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; |
| if (mHorizontal) { |
| left = (int) (getWidth() - circleSize / 2 - mBaseMargin - offset); |
| top = (int) ((getHeight() - circleSize) / 2); |
| } else { |
| left = (int) (getWidth() - circleSize) / 2; |
| top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); |
| } |
| rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); |
| } |
| |
| public void setHorizontal(boolean horizontal) { |
| mHorizontal = horizontal; |
| updateCircleRect(mStaticRect, mStaticOffset, true); |
| updateLayout(); |
| } |
| |
| public void setDragDistance(float distance) { |
| if (!mAnimatingOut && (!mCircleHidden || mDraggedFarEnough)) { |
| float circleSize = mCircleMinSize + rubberband(distance); |
| setCircleSize(circleSize); |
| } |
| |
| } |
| |
| private float rubberband(float diff) { |
| return (float) Math.pow(Math.abs(diff), 0.6f); |
| } |
| |
| public void startAbortAnimation(Runnable endRunnable) { |
| if (mAnimatingOut) { |
| if (endRunnable != null) { |
| endRunnable.run(); |
| } |
| return; |
| } |
| setCircleSize(0, true, null, 0, null); |
| setOffset(0, true, 0, null, endRunnable); |
| mCircleHidden = true; |
| } |
| |
| public void startEnterAnimation() { |
| if (mAnimatingOut) { |
| return; |
| } |
| applyCircleSize(0); |
| setOffset(0); |
| setCircleSize(mCircleMinSize, true, null, 50, null); |
| setOffset(mStaticOffset, true, 50, null, null); |
| mCircleHidden = false; |
| } |
| |
| |
| public void startExitAnimation(final Runnable endRunnable) { |
| if (!mHorizontal) { |
| float offset = getHeight() / 2.0f; |
| setOffset(offset - mBaseMargin, true, 50, mFastOutSlowInInterpolator, null); |
| float xMax = getWidth() / 2; |
| float yMax = getHeight() / 2; |
| float maxRadius = (float) Math.ceil(Math.hypot(xMax, yMax) * 2); |
| setCircleSize(maxRadius, true, null, 50, mFastOutSlowInInterpolator); |
| performExitFadeOutAnimation(50, 300, endRunnable); |
| } else { |
| |
| // when in landscape, we don't wan't the animation as it interferes with the general |
| // rotation animation to the homescreen. |
| endRunnable.run(); |
| } |
| } |
| |
| private void performExitFadeOutAnimation(int startDelay, int duration, |
| final Runnable endRunnable) { |
| mFadeOutAnimator = ValueAnimator.ofFloat(mBackgroundPaint.getAlpha() / 255.0f, 0.0f); |
| |
| // Linear since we are animating multiple values |
| mFadeOutAnimator.setInterpolator(new LinearInterpolator()); |
| mFadeOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| float animatedFraction = animation.getAnimatedFraction(); |
| float logoValue = animatedFraction > 0.5f ? 1.0f : animatedFraction / 0.5f; |
| logoValue = PhoneStatusBar.ALPHA_OUT.getInterpolation(1.0f - logoValue); |
| float backgroundValue = animatedFraction < 0.2f ? 0.0f : |
| PhoneStatusBar.ALPHA_OUT.getInterpolation((animatedFraction - 0.2f) / 0.8f); |
| backgroundValue = 1.0f - backgroundValue; |
| mBackgroundPaint.setAlpha((int) (backgroundValue * 255)); |
| mOutlineAlpha = backgroundValue; |
| mLogo.setAlpha(logoValue); |
| invalidateOutline(); |
| invalidate(); |
| } |
| }); |
| mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (endRunnable != null) { |
| endRunnable.run(); |
| } |
| mLogo.setAlpha(1.0f); |
| mBackgroundPaint.setAlpha(255); |
| mOutlineAlpha = 1.0f; |
| mFadeOutAnimator = null; |
| } |
| }); |
| mFadeOutAnimator.setStartDelay(startDelay); |
| mFadeOutAnimator.setDuration(duration); |
| mFadeOutAnimator.start(); |
| } |
| |
| public void setDraggedFarEnough(boolean farEnough) { |
| if (farEnough != mDraggedFarEnough) { |
| if (farEnough) { |
| if (mCircleHidden) { |
| startEnterAnimation(); |
| } |
| if (mOffsetAnimator == null) { |
| addRipple(); |
| } else { |
| postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| addRipple(); |
| } |
| }, 100); |
| } |
| } else { |
| startAbortAnimation(null); |
| } |
| mDraggedFarEnough = farEnough; |
| } |
| |
| } |
| |
| private void addRipple() { |
| if (mRipples.size() > 1) { |
| // we only want 2 ripples at the time |
| return; |
| } |
| float xInterpolation, yInterpolation; |
| if (mHorizontal) { |
| xInterpolation = 0.75f; |
| yInterpolation = 0.5f; |
| } else { |
| xInterpolation = 0.5f; |
| yInterpolation = 0.75f; |
| } |
| float circleCenterX = mStaticRect.left * (1.0f - xInterpolation) |
| + mStaticRect.right * xInterpolation; |
| float circleCenterY = mStaticRect.top * (1.0f - yInterpolation) |
| + mStaticRect.bottom * yInterpolation; |
| float radius = Math.max(mCircleSize, mCircleMinSize * 1.25f) * 0.75f; |
| Ripple ripple = new Ripple(circleCenterX, circleCenterY, radius); |
| ripple.start(); |
| } |
| |
| public void reset() { |
| mDraggedFarEnough = false; |
| mAnimatingOut = false; |
| mCircleHidden = true; |
| mClipToOutline = false; |
| if (mFadeOutAnimator != null) { |
| mFadeOutAnimator.cancel(); |
| } |
| mBackgroundPaint.setAlpha(255); |
| mOutlineAlpha = 1.0f; |
| } |
| |
| /** |
| * Check if an animation is currently running |
| * |
| * @param enterAnimation Is the animating queried the enter animation. |
| */ |
| public boolean isAnimationRunning(boolean enterAnimation) { |
| return mOffsetAnimator != null && (enterAnimation == mOffsetAnimatingIn); |
| } |
| |
| public void performOnAnimationFinished(final Runnable runnable) { |
| if (mOffsetAnimator != null) { |
| mOffsetAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (runnable != null) { |
| runnable.run(); |
| } |
| } |
| }); |
| } else { |
| if (runnable != null) { |
| runnable.run(); |
| } |
| } |
| } |
| |
| public void setAnimatingOut(boolean animatingOut) { |
| mAnimatingOut = animatingOut; |
| } |
| |
| /** |
| * @return Whether the circle is currently launching to the search activity or aborting the |
| * interaction |
| */ |
| public boolean isAnimatingOut() { |
| return mAnimatingOut; |
| } |
| |
| @Override |
| public boolean hasOverlappingRendering() { |
| // not really true but it's ok during an animation, as it's never permanent |
| return false; |
| } |
| |
| private class Ripple { |
| float x; |
| float y; |
| float radius; |
| float endRadius; |
| float alpha; |
| |
| Ripple(float x, float y, float endRadius) { |
| this.x = x; |
| this.y = y; |
| this.endRadius = endRadius; |
| } |
| |
| void start() { |
| ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); |
| |
| // Linear since we are animating multiple values |
| animator.setInterpolator(new LinearInterpolator()); |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| alpha = 1.0f - animation.getAnimatedFraction(); |
| alpha = mDisappearInterpolator.getInterpolation(alpha); |
| radius = mAppearInterpolator.getInterpolation(animation.getAnimatedFraction()); |
| radius *= endRadius; |
| invalidate(); |
| } |
| }); |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mRipples.remove(Ripple.this); |
| updateClipping(); |
| } |
| |
| public void onAnimationStart(Animator animation) { |
| mRipples.add(Ripple.this); |
| updateClipping(); |
| } |
| }); |
| animator.setDuration(400); |
| animator.start(); |
| } |
| |
| public void draw(Canvas canvas) { |
| mRipplePaint.setAlpha((int) (alpha * 255)); |
| canvas.drawCircle(x, y, radius, mRipplePaint); |
| } |
| } |
| |
| } |