summaryrefslogtreecommitdiffstats
path: root/packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java
diff options
context:
space:
mode:
authorSelim Cinek <cinek@google.com>2014-09-11 15:11:00 +0200
committerSelim Cinek <cinek@google.com>2014-09-15 17:25:40 +0200
commit92d892c0cd5462237f818b1129d936d95640e297 (patch)
treeac4e427a35fc7bd41fdb701c66aaedb2126bdfe8 /packages/SystemUI/src/com/android/systemui/SearchPanelCircleView.java
parent2047df6f5f6af14cdb93f220c0329f1bff7a43ff (diff)
downloadframeworks_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.java592
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);
+ }
+ }
+
+}