diff options
author | Selim Cinek <cinek@google.com> | 2014-09-11 15:11:00 +0200 |
---|---|---|
committer | Selim Cinek <cinek@google.com> | 2014-09-15 17:25:40 +0200 |
commit | 92d892c0cd5462237f818b1129d936d95640e297 (patch) | |
tree | ac4e427a35fc7bd41fdb701c66aaedb2126bdfe8 /packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java | |
parent | 2047df6f5f6af14cdb93f220c0329f1bff7a43ff (diff) | |
download | frameworks_base-92d892c0cd5462237f818b1129d936d95640e297.zip frameworks_base-92d892c0cd5462237f818b1129d936d95640e297.tar.gz frameworks_base-92d892c0cd5462237f818b1129d936d95640e297.tar.bz2 |
Changed the swipe up search affordance
The previous card animation is removed and replaced by a animating
circle with a shadow. Also fixes several cases where the card could
either get stuck and the affordance was not launched.
Bug: 17457300
Bug: 17444236
Change-Id: I005313a1dbe63d338490e6100dd3bd01e35687ba
Diffstat (limited to 'packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java')
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java new file mode 100644 index 0000000..d8fb6da --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java @@ -0,0 +1,592 @@ +/* + * 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); + } + } + +} |