FileDocCategorySizeDatePackage
SearchPanelCircleView.javaAPI DocAndroid 5.1 API21943Thu Mar 12 22:22:42 GMT 2015com.android.systemui

SearchPanelCircleView.java

/*
 * 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);
        }
    }

}