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 | |
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')
14 files changed, 702 insertions, 215 deletions
diff --git a/packages/SystemUI/res/anim/search_launch_enter.xml b/packages/SystemUI/res/anim/search_launch_enter.xml index f3333b7..19c0a95 100644 --- a/packages/SystemUI/res/anim/search_launch_enter.xml +++ b/packages/SystemUI/res/anim/search_launch_enter.xml @@ -17,16 +17,8 @@ */ --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:shareInterpolator="false" android:zAdjustment="top"> - - <alpha android:fromAlpha="0" android:toAlpha="1.0" - android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" - android:interpolator="@android:interpolator/decelerate_cubic" - android:duration="300"/> - - <translate android:fromYDelta="100%" android:toYDelta="0" - android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" - android:interpolator="@android:interpolator/decelerate_cubic" - android:duration="300" /> -</set> +<alpha xmlns:android="http://schemas.android.com/apk/res/android" + android:fromAlpha="0" android:toAlpha="1.0" + android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" + android:interpolator="@android:interpolator/decelerate_cubic" + android:duration="300"/>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/search_panel_card_bg.xml b/packages/SystemUI/res/drawable/search_panel_card_bg.xml deleted file mode 100644 index c19f900..0000000 --- a/packages/SystemUI/res/drawable/search_panel_card_bg.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- - ~ 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 - --> -<shape xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="@color/search_panel_card_color" /> - <corners android:radius="@dimen/notification_material_rounded_rect_radius" /> -</shape> diff --git a/packages/SystemUI/res/layout/status_bar_search_panel.xml b/packages/SystemUI/res/layout/status_bar_search_panel.xml index f025abd..e0520ef 100644 --- a/packages/SystemUI/res/layout/status_bar_search_panel.xml +++ b/packages/SystemUI/res/layout/status_bar_search_panel.xml @@ -30,17 +30,14 @@ android:id="@+id/search_panel_scrim" android:background="@drawable/search_panel_scrim" /> - <FrameLayout - style="@style/SearchPanelCard" - android:id="@+id/search_panel_card" - android:background="@drawable/search_panel_card_bg" - android:elevation="12dp"> + <com.android.systemui.SearchPanelCircleView + style="@style/SearchPanelCircle" + android:id="@+id/search_panel_circle"> <ImageView - style="@style/SearchPanelLogo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/search_logo" /> - </FrameLayout> + </com.android.systemui.SearchPanelCircleView> </com.android.systemui.SearchPanelView> diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index 682998d..e58fbb1 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -19,18 +19,6 @@ <item name="android:layout_width">360dp</item> </style> - <style name="SearchPanelCard"> - <item name="android:layout_width">@dimen/search_panel_card_height</item> - <item name="android:layout_height">match_parent</item> - <item name="android:layout_marginTop">16dp</item> - <item name="android:layout_marginBottom">16dp</item> - <item name="android:layout_gravity">right</item> - </style> - - <style name="SearchPanelLogo"> - <item name="android:layout_gravity">top|left</item> - </style> - <style name="SearchPanelScrim"> <item name="android:layout_width">@dimen/search_panel_scrim_height</item> <item name="android:layout_height">match_parent</item> diff --git a/packages/SystemUI/res/values-sw600dp/styles.xml b/packages/SystemUI/res/values-sw600dp/styles.xml index 9e5b1d6..156fa65 100644 --- a/packages/SystemUI/res/values-sw600dp/styles.xml +++ b/packages/SystemUI/res/values-sw600dp/styles.xml @@ -19,16 +19,6 @@ <item name="android:layout_width">480dp</item> </style> - <style name="SearchPanelCard"> - <item name="android:layout_width">550dp</item> - <item name="android:layout_height">@dimen/search_panel_card_height</item> - <item name="android:layout_gravity">center_horizontal|bottom</item> - </style> - - <style name="SearchPanelLogo"> - <item name="android:layout_gravity">top|center_horizontal</item> - </style> - <style name="SearchPanelScrim"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">@dimen/search_panel_scrim_height</item> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 6da811f..d5b0f17 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -109,7 +109,8 @@ <color name="notification_guts_text_color">#b2FFFFFF</color> <color name="notification_guts_btn_color">#FFFFFFFF</color> - <color name="search_panel_card_color">#ffffff</color> + <color name="search_panel_circle_color">#ffffff</color> + <color name="search_panel_ripple_color">#ffbbbbbb</color> <color name="keyguard_user_switcher_background_gradient_color">#77000000</color> <color name="doze_small_icon_background_color">#ff434343</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 4495318..8fe0af8 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -434,15 +434,20 @@ from Keyguard. --> <dimen name="go_to_full_shade_appearing_translation">200dp</dimen> - <!-- The height of the search panel card. --> - <dimen name="search_panel_card_height">300dp</dimen> + <!-- The diameter of the search panel circle. --> + <dimen name="search_panel_circle_size">88dp</dimen> - <!-- The height of the scrim behind the search panel card. --> - <dimen name="search_panel_scrim_height">250dp</dimen> + <!-- The margin to the edge of the screen from where the circle starts to appear --> + <dimen name="search_panel_circle_base_margin">80dp</dimen> + + <!-- The amount the circle translates when appearing --> + <dimen name="search_panel_circle_travel_distance">80dp</dimen> - <!-- How much from the bottom of the screen the card should peek in when activating the search - panel --> - <dimen name="search_card_peek_height">100dp</dimen> + <!-- The elevation of the search panel circle --> + <dimen name="search_panel_circle_elevation">12dp</dimen> + + <!-- The height of the scrim behind the search panel circle. --> + <dimen name="search_panel_scrim_height">250dp</dimen> <!-- How far the user needs to drag up to invoke search. --> <dimen name="search_panel_threshold">100dp</dimen> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index cb66e7a..0456c82 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -239,16 +239,9 @@ <item name="android:textColor">#60000000</item> </style> - <style name="SearchPanelCard"> + <style name="SearchPanelCircle"> <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">@dimen/search_panel_card_height</item> - <item name="android:layout_marginStart">8dp</item> - <item name="android:layout_marginEnd">8dp</item> - <item name="android:layout_gravity">bottom</item> - </style> - - <style name="SearchPanelLogo"> - <item name="android:layout_gravity">top|center_horizontal</item> + <item name="android:layout_height">match_parent</item> </style> <style name="SearchPanelScrim"> 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); + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java index 7d0ca14..445b499 100644 --- a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java @@ -16,10 +16,6 @@ package com.android.systemui; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; import android.app.ActivityOptions; import android.app.SearchManager; import android.content.ActivityNotFoundException; @@ -38,8 +34,6 @@ import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; @@ -62,26 +56,19 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { private final Context mContext; private BaseStatusBar mBar; - private View mCard; + private SearchPanelCircleView mCircle; private ImageView mLogo; private View mScrim; - private int mPeekHeight; private int mThreshold; private boolean mHorizontal; - private final Interpolator mLinearOutSlowInInterpolator; - private final Interpolator mFastOutLinearInInterpolator; - private boolean mAnimatingIn; - private boolean mAnimatingOut; + private boolean mLaunching; private boolean mDragging; private boolean mDraggedFarEnough; private float mStartTouch; private float mStartDrag; - - private ObjectAnimator mEnterAnimator; - - private boolean mStartExitAfterAnimatingIn; + private boolean mLaunchPending; public SearchPanelView(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -90,12 +77,7 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { public SearchPanelView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; - mPeekHeight = context.getResources().getDimensionPixelSize(R.dimen.search_card_peek_height); mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold); - mLinearOutSlowInInterpolator = - AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); - mFastOutLinearInInterpolator = - AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); } private void startAssistActivity() { @@ -128,7 +110,7 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { protected void onFinishInflate() { super.onFinishInflate(); mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mCard = findViewById(R.id.search_panel_card); + mCircle = (SearchPanelCircleView) findViewById(R.id.search_panel_circle); mLogo = (ImageView) findViewById(R.id.search_logo); mScrim = findViewById(R.id.search_panel_scrim); } @@ -170,16 +152,9 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { v.setImageDrawable(null); } - private boolean pointInside(int x, int y, View v) { - final int l = v.getLeft(); - final int r = v.getRight(); - final int t = v.getTop(); - final int b = v.getBottom(); - return x >= l && x < r && y >= t && y < b; - } - + @Override public boolean isInContentArea(int x, int y) { - return pointInside(x, y, mCard); + return true; } private void vibrate() { @@ -199,16 +174,10 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { if (getVisibility() != View.VISIBLE) { setVisibility(View.VISIBLE); vibrate(); - mCard.setAlpha(1f); if (animate) { startEnterAnimation(); } else { mScrim.setAlpha(1f); - if (mHorizontal) { - mCard.setX(getWidth() - mPeekHeight); - } else { - mCard.setY(getHeight() - mPeekHeight); - } } } setFocusable(true); @@ -224,30 +193,7 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { } private void startEnterAnimation() { - if (mHorizontal) { - mCard.setX(getWidth()); - } else { - mCard.setY(getHeight()); - } - mAnimatingIn = true; - mCard.animate().cancel(); - mEnterAnimator = ObjectAnimator.ofFloat(mCard, mHorizontal ? View.X : View.Y, - mHorizontal ? mCard.getX() : mCard.getY(), - mHorizontal ? getWidth() - mPeekHeight : getHeight() - mPeekHeight); - mEnterAnimator.setDuration(300); - mEnterAnimator.setStartDelay(50); - mEnterAnimator.setInterpolator(mLinearOutSlowInInterpolator); - mEnterAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mEnterAnimator = null; - mAnimatingIn = false; - if (mStartExitAfterAnimatingIn) { - startExitAnimation(); - } - } - }); - mEnterAnimator.start(); + mCircle.startEnterAnimation(); mScrim.setAlpha(0f); mScrim.animate() .alpha(1f) @@ -259,26 +205,17 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { } private void startAbortAnimation() { - mCard.animate().cancel(); - mAnimatingOut = true; - if (mHorizontal) { - mCard.animate().x(getWidth()); - } else { - mCard.animate().y(getHeight()); - } - mCard.animate() - .setDuration(150) - .setInterpolator(mFastOutLinearInInterpolator) - .withEndAction(new Runnable() { + mCircle.startAbortAnimation(new Runnable() { @Override public void run() { - mAnimatingOut = false; + mCircle.setAnimatingOut(false); setVisibility(View.INVISIBLE); } }); + mCircle.setAnimatingOut(true); mScrim.animate() .alpha(0f) - .setDuration(150) + .setDuration(300) .setStartDelay(0) .setInterpolator(PhoneStatusBar.ALPHA_OUT); } @@ -314,7 +251,7 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { * when the animation is done. */ public boolean isShowing() { - return getVisibility() == View.VISIBLE && !mAnimatingOut; + return getVisibility() == View.VISIBLE && !mCircle.isAnimatingOut(); } public void setBar(BaseStatusBar bar) { @@ -326,60 +263,46 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; } - private float rubberband(float diff) { - return Math.signum(diff) * (float) Math.pow(Math.abs(diff), 0.8f); - } - @Override public boolean onTouchEvent(MotionEvent event) { + if (mLaunching || mLaunchPending) { + return false; + } int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: mStartTouch = mHorizontal ? event.getX() : event.getY(); mDragging = false; mDraggedFarEnough = false; - mStartExitAfterAnimatingIn = false; + mCircle.reset(); break; case MotionEvent.ACTION_MOVE: float currentTouch = mHorizontal ? event.getX() : event.getY(); if (getVisibility() == View.VISIBLE && !mDragging && - (!mAnimatingIn || Math.abs(mStartTouch - currentTouch) > mThreshold)) { + (!mCircle.isAnimationRunning(true /* enterAnimation */) + || Math.abs(mStartTouch - currentTouch) > mThreshold)) { mStartDrag = currentTouch; mDragging = true; } - if (!mDraggedFarEnough && Math.abs(mStartTouch - currentTouch) > mThreshold) { - mDraggedFarEnough = true; - } if (mDragging) { - if (!mAnimatingIn && !mAnimatingOut) { - if (Math.abs(currentTouch - mStartDrag) > mThreshold) { - startExitAnimation(); - } else { - if (mHorizontal) { - mCard.setX(getWidth() - mPeekHeight + rubberband( - currentTouch - mStartDrag)); - } else { - mCard.setY(getHeight() - mPeekHeight + rubberband( - currentTouch - mStartDrag)); - } - } - } else if (mAnimatingIn ) { - float diff = rubberband(currentTouch - mStartDrag); - PropertyValuesHolder[] values = mEnterAnimator.getValues(); - values[0].setFloatValues( - mHorizontal ? getWidth() + diff : getHeight() + diff, - mHorizontal - ? getWidth() - mPeekHeight + diff - : getHeight() - mPeekHeight + diff); - mEnterAnimator.setCurrentPlayTime(mEnterAnimator.getCurrentPlayTime()); - } + float offset = Math.max(mStartDrag - currentTouch, 0.0f); + mCircle.setDragDistance(offset); + mDraggedFarEnough = Math.abs(mStartTouch - currentTouch) > mThreshold; + mCircle.setDraggedFarEnough(mDraggedFarEnough); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mDraggedFarEnough) { - if (mAnimatingIn) { - mStartExitAfterAnimatingIn = true; + if (mCircle.isAnimationRunning(true /* enterAnimation */)) { + mLaunchPending = true; + mCircle.setAnimatingOut(true); + mCircle.performOnAnimationFinished(new Runnable() { + @Override + public void run() { + startExitAnimation(); + } + }); } else { startExitAnimation(); } @@ -392,35 +315,31 @@ public class SearchPanelView extends FrameLayout implements StatusBarPanel { } private void startExitAnimation() { - if (mAnimatingOut || getVisibility() != View.VISIBLE) { + mLaunchPending = false; + if (mLaunching || getVisibility() != View.VISIBLE) { return; } - if (mEnterAnimator != null) { - mEnterAnimator.cancel(); - } - mAnimatingOut = true; + mLaunching = true; startAssistActivity(); vibrate(); - mCard.animate() - .alpha(0f) - .withLayer() - .setDuration(250) - .setInterpolator(PhoneStatusBar.ALPHA_OUT) - .withEndAction(new Runnable() { + mCircle.setAnimatingOut(true); + mCircle.startExitAnimation(new Runnable() { @Override public void run() { - mAnimatingOut = false; + mLaunching = false; + mCircle.setAnimatingOut(false); setVisibility(View.INVISIBLE); } }); mScrim.animate() .alpha(0f) - .setDuration(250) + .setDuration(300) .setStartDelay(0) .setInterpolator(PhoneStatusBar.ALPHA_OUT); } public void setHorizontal(boolean horizontal) { mHorizontal = horizontal; + mCircle.setHorizontal(horizontal); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index ce3739c..aa75fd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -93,6 +93,7 @@ import com.android.systemui.SystemUI; import com.android.systemui.statusbar.NotificationData.Entry; import com.android.systemui.statusbar.phone.KeyguardTouchDelegate; import com.android.systemui.statusbar.phone.PhoneStatusBar; +import com.android.systemui.statusbar.phone.NavigationBarView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.HeadsUpNotificationView; import com.android.systemui.statusbar.policy.PreviewInflater; @@ -165,6 +166,9 @@ public abstract class BaseStatusBar extends SystemUI implements protected int mLayoutDirection = -1; // invalid protected AccessibilityManager mAccessibilityManager; + + // on-screen navigation buttons + protected NavigationBarView mNavigationBarView = null; private Locale mLocale; private float mFontScale; @@ -1006,6 +1010,8 @@ public abstract class BaseStatusBar extends SystemUI implements mSearchPanelView.setOnTouchListener( new TouchOutsideListener(MSG_CLOSE_SEARCH_PANEL, mSearchPanelView)); mSearchPanelView.setVisibility(View.GONE); + boolean vertical = mNavigationBarView != null && mNavigationBarView.isVertical(); + mSearchPanelView.setHorizontal(vertical); WindowManager.LayoutParams lp = getSearchLayoutParams(mSearchPanelView.getLayoutParams()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 0fb2192..3874b41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -88,6 +88,7 @@ public class NavigationBarView extends LinearLayout { private OnVerticalChangedListener mOnVerticalChangedListener; private boolean mIsLayoutRtl; + private boolean mDelegateIntercepted; private class NavTransitionListener implements TransitionListener { private boolean mBackTransitioning; @@ -198,27 +199,45 @@ public class NavigationBarView extends LinearLayout { public void setOnVerticalChangedListener(OnVerticalChangedListener onVerticalChangedListener) { mOnVerticalChangedListener = onVerticalChangedListener; + notifyVerticalChangedListener(mVertical); } @Override public boolean onTouchEvent(MotionEvent event) { - if (mTaskSwitchHelper.onTouchEvent(event)) { + initDownStates(event); + if (!mDelegateIntercepted && mTaskSwitchHelper.onTouchEvent(event)) { return true; } if (mDeadZone != null && event.getAction() == MotionEvent.ACTION_OUTSIDE) { mDeadZone.poke(event); } - if (mDelegateHelper != null) { + if (mDelegateHelper != null && mDelegateIntercepted) { boolean ret = mDelegateHelper.onInterceptTouchEvent(event); if (ret) return true; } return super.onTouchEvent(event); } + private void initDownStates(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mDelegateIntercepted = false; + } + } + @Override public boolean onInterceptTouchEvent(MotionEvent event) { - return mTaskSwitchHelper.onInterceptTouchEvent(event) || - mDelegateHelper.onInterceptTouchEvent(event); + initDownStates(event); + boolean intercept = mTaskSwitchHelper.onInterceptTouchEvent(event); + if (!intercept) { + mDelegateIntercepted = mDelegateHelper.onInterceptTouchEvent(event); + intercept = mDelegateIntercepted; + } else { + MotionEvent cancelEvent = MotionEvent.obtain(event); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL); + mDelegateHelper.onInterceptTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + return intercept; } private H mHandler = new H(); @@ -426,12 +445,16 @@ public class NavigationBarView extends LinearLayout { if (mDelegateHelper != null) { mDelegateHelper.setSwapXY(mVertical); } - boolean isRTL = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - mTaskSwitchHelper.setBarState(mVertical, isRTL); + updateTaskSwitchHelper(); setNavigationIconHints(mNavigationIconHints, true); } + private void updateTaskSwitchHelper() { + boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + mTaskSwitchHelper.setBarState(mVertical, isRtl); + } + @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); @@ -448,19 +471,24 @@ public class NavigationBarView extends LinearLayout { mVertical = newVertical; //Log.v(TAG, String.format("onSizeChanged: h=%d, w=%d, vert=%s", h, w, mVertical?"y":"n")); reorient(); - if (mOnVerticalChangedListener != null) { - mOnVerticalChangedListener.onVerticalChanged(newVertical); - } + notifyVerticalChangedListener(newVertical); } postCheckForInvalidLayout("sizeChanged"); super.onSizeChanged(w, h, oldw, oldh); } + private void notifyVerticalChangedListener(boolean newVertical) { + if (mOnVerticalChangedListener != null) { + mOnVerticalChangedListener.onVerticalChanged(newVertical); + } + } + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateRTLOrder(); + updateTaskSwitchHelper(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarViewTaskSwitchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarViewTaskSwitchHelper.java index 79bb1cd..c253e19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarViewTaskSwitchHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarViewTaskSwitchHelper.java @@ -55,7 +55,7 @@ public class NavigationBarViewTaskSwitchHelper extends GestureDetector.SimpleOnG // task switcher detector mTaskSwitcherDetector.onTouchEvent(event); int action = event.getAction(); - boolean interceptTouches = false; + boolean intercepted = false; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { mTouchDownX = (int) event.getX(); @@ -71,7 +71,6 @@ public class NavigationBarViewTaskSwitchHelper extends GestureDetector.SimpleOnG ? xDiff > mScrollTouchSlop && xDiff > yDiff : yDiff > mScrollTouchSlop && yDiff > xDiff; if (exceededTouchSlop) { - interceptTouches = true; return true; } break; @@ -80,7 +79,7 @@ public class NavigationBarViewTaskSwitchHelper extends GestureDetector.SimpleOnG case MotionEvent.ACTION_UP: break; } - return interceptTouches; + return intercepted; } public boolean onTouchEvent(MotionEvent event) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index e6db2c8..4272582 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -316,8 +316,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, int[] mPositionTmp = new int[2]; boolean mExpandedVisible; - // on-screen navigation buttons - private NavigationBarView mNavigationBarView = null; private int mNavigationBarWindowState = WINDOW_STATE_SHOWING; // the tracker view |