diff options
author | Tony Wickham <twickham@google.com> | 2017-01-20 09:38:25 -0800 |
---|---|---|
committer | Tony Wickham <twickham@google.com> | 2017-01-25 17:36:31 -0800 |
commit | 9438ed414fdabadb4cd09da184867b1c44b91095 (patch) | |
tree | 9d84de236a89cde247b9de8f11408e815e6d6702 /src/com/android/launcher3 | |
parent | f3d02e4716f89d14d9017851db9ad6141ad26875 (diff) | |
download | android_packages_apps_Trebuchet-9438ed414fdabadb4cd09da184867b1c44b91095.tar.gz android_packages_apps_Trebuchet-9438ed414fdabadb4cd09da184867b1c44b91095.tar.bz2 android_packages_apps_Trebuchet-9438ed414fdabadb4cd09da184867b1c44b91095.zip |
Add swipe-to-dismiss notifications in popup menu.
- Next secondary icon animates up to replace dismissed main notification
- Add padding around main notification so it always aligns with the
straight edges of the view (not the rounded corners); looks more
dismissable
- Notification view collapses as notifications are dismissed
- To mimic system notification behavior, we copy SwipeHelper,
FlingAnimationUtils, and Interpolators. We also apply elevation
to notifications and reveal a darker color beneath when dismissing.
Bug: 32410600
Change-Id: I9fbf10e73bb4996f17ef061c856efb013967d972
Diffstat (limited to 'src/com/android/launcher3')
19 files changed, 1878 insertions, 39 deletions
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index ed8b53132..2efe31fa0 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -44,6 +44,7 @@ import com.android.launcher3.badge.BadgeRenderer; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.graphics.DrawableFactory; import com.android.launcher3.graphics.HolographicOutlineHelper; +import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.model.PackageItemInfo; import java.text.NumberFormat; @@ -514,6 +515,11 @@ public class BubbleTextView extends TextView } } + public IconPalette getIconPalette() { + return mIcon instanceof FastBitmapDrawable ? ((FastBitmapDrawable) mIcon).getIconPalette() + : null; + } + private Theme getPreloaderTheme() { Object tag = getTag(); int style = ((tag != null) && (tag instanceof ShortcutInfo) && diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index 587d44552..df195471b 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -129,10 +129,7 @@ public class FastBitmapDrawable extends Drawable { mBadgeInfo = badgeInfo; mBadgeRenderer = badgeRenderer; if (wasBadged || isBadged) { - if (mBadgeInfo != null && mIconPalette == null) { - mIconPalette = IconPalette.fromDominantColor(Utilities - .findDominantColorByHue(mBitmap, 20)); - } + mIconPalette = getIconPalette(); invalidateSelf(); } } @@ -161,6 +158,14 @@ public class FastBitmapDrawable extends Drawable { } } + public IconPalette getIconPalette() { + if (mIconPalette == null) { + mIconPalette = IconPalette.fromDominantColor(Utilities + .findDominantColorByHue(mBitmap, 20)); + } + return mIconPalette; + } + private boolean hasBadge() { return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0; } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index e2108a710..69b305f9a 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -85,7 +85,7 @@ import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.allapps.DefaultAppSearchController; import com.android.launcher3.anim.AnimationLayerSet; -import com.android.launcher3.badge.NotificationListener; +import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherAppsCompat; diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index 01e73d4a1..9ea277c13 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -23,7 +23,9 @@ import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.util.Property; import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.widget.ViewAnimator; import java.util.HashSet; import java.util.WeakHashMap; @@ -127,4 +129,18 @@ public class LauncherAnimUtils { new FirstFrameAnimatorHelper(anim, view); return anim; } + + public static ValueAnimator animateViewHeight(final View v, int fromHeight, int toHeight) { + ValueAnimator anim = ValueAnimator.ofInt(fromHeight, toHeight); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); + layoutParams.height = val; + v.setLayoutParams(layoutParams); + } + }); + return anim; + } } diff --git a/src/com/android/launcher3/badge/BadgeInfo.java b/src/com/android/launcher3/badge/BadgeInfo.java index 4255c5132..673c297dc 100644 --- a/src/com/android/launcher3/badge/BadgeInfo.java +++ b/src/com/android/launcher3/badge/BadgeInfo.java @@ -16,6 +16,7 @@ package com.android.launcher3.badge; +import com.android.launcher3.notification.NotificationInfo; import com.android.launcher3.util.PackageUserKey; import java.util.HashSet; diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java index dcc5fcb14..58ad449d6 100644 --- a/src/com/android/launcher3/graphics/IconPalette.java +++ b/src/com/android/launcher3/graphics/IconPalette.java @@ -26,16 +26,18 @@ public class IconPalette { public int backgroundColor; public int textColor; + public int secondaryColor; public static IconPalette fromDominantColor(int dominantColor) { IconPalette palette = new IconPalette(); palette.backgroundColor = getMutedColor(dominantColor); palette.textColor = getTextColorForBackground(palette.backgroundColor); + palette.secondaryColor = getLowContrastColor(palette.backgroundColor); return palette; } private static int getMutedColor(int color) { - int alpha = (int) (255 * 0.2f); + int alpha = (int) (255 * 0.15f); return ColorUtils.compositeColors(ColorUtils.setAlphaComponent(color, alpha), Color.WHITE); } diff --git a/src/com/android/launcher3/notification/FlingAnimationUtils.java b/src/com/android/launcher3/notification/FlingAnimationUtils.java new file mode 100644 index 000000000..3e954d0bb --- /dev/null +++ b/src/com/android/launcher3/notification/FlingAnimationUtils.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2017 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.launcher3.notification; + +import android.animation.Animator; +import android.content.Context; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * Utility class to calculate general fling animation when the finger is released. + */ +public class FlingAnimationUtils { + + private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f; + private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f; + private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f; + private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f; + private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f; + private static final float MIN_VELOCITY_DP_PER_SECOND = 250; + private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000; + + private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f; + private final float mSpeedUpFactor; + private final float mY2; + + private float mMinVelocityPxPerSecond; + private float mMaxLengthSeconds; + private float mHighVelocityPxPerSecond; + private float mLinearOutSlowInX2; + + private AnimatorProperties mAnimatorProperties = new AnimatorProperties(); + private PathInterpolator mInterpolator; + private float mCachedStartGradient = -1; + private float mCachedVelocityFactor = -1; + + public FlingAnimationUtils(Context ctx, float maxLengthSeconds) { + this(ctx, maxLengthSeconds, 0.0f); + } + + /** + * @param maxLengthSeconds the longest duration an animation can become in seconds + * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards + * the end of the animation. 0 means it's at the beginning and no + * acceleration will take place. + */ + public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) { + this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f); + } + + /** + * @param maxLengthSeconds the longest duration an animation can become in seconds + * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards + * the end of the animation. 0 means it's at the beginning and no + * acceleration will take place. + * @param x2 the x value to take for the second point of the bezier spline. If a value below 0 + * is provided, the value is automatically calculated. + * @param y2 the y value to take for the second point of the bezier spline + */ + public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2, + float y2) { + mMaxLengthSeconds = maxLengthSeconds; + mSpeedUpFactor = speedUpFactor; + if (x2 < 0) { + mLinearOutSlowInX2 = interpolate(LINEAR_OUT_SLOW_IN_X2, + LINEAR_OUT_SLOW_IN_X2_MAX, + mSpeedUpFactor); + } else { + mLinearOutSlowInX2 = x2; + } + mY2 = y2; + + mMinVelocityPxPerSecond + = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density; + mHighVelocityPxPerSecond + = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density; + } + + private static float interpolate(float start, float end, float amount) { + return start * (1.0f - amount) + end * amount; + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + */ + public void apply(Animator animator, float currValue, float endValue, float velocity) { + apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + */ + public void apply(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity) { + apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void apply(Animator animator, float currValue, float endValue, float velocity, + float maxDistance) { + AnimatorProperties properties = getProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void apply(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + private AnimatorProperties getProperties(float currValue, + float endValue, float velocity, float maxDistance) { + float maxLengthSeconds = (float) (mMaxLengthSeconds + * Math.sqrt(Math.abs(endValue - currValue) / maxDistance)); + float diff = Math.abs(endValue - currValue); + float velAbs = Math.abs(velocity); + float velocityFactor = mSpeedUpFactor == 0.0f + ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f); + float startGradient = interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT, + mY2 / mLinearOutSlowInX2, velocityFactor); + float durationSeconds = startGradient * diff / velAbs; + Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor); + if (durationSeconds <= maxLengthSeconds) { + mAnimatorProperties.interpolator = slowInInterpolator; + } else if (velAbs >= mMinVelocityPxPerSecond) { + + // Cross fade between fast-out-slow-in and linear interpolator with current velocity. + durationSeconds = maxLengthSeconds; + VelocityInterpolator velocityInterpolator + = new VelocityInterpolator(durationSeconds, velAbs, diff); + InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( + velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN); + mAnimatorProperties.interpolator = superInterpolator; + } else { + + // Just use a normal interpolator which doesn't take the velocity into account. + durationSeconds = maxLengthSeconds; + mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN; + } + mAnimatorProperties.duration = (long) (durationSeconds * 1000); + return mAnimatorProperties; + } + + private Interpolator getInterpolator(float startGradient, float velocityFactor) { + if (startGradient != mCachedStartGradient + || velocityFactor != mCachedVelocityFactor) { + float speedup = mSpeedUpFactor * (1.0f - velocityFactor); + mInterpolator = new PathInterpolator(speedup, + speedup * startGradient, + mLinearOutSlowInX2, mY2); + mCachedStartGradient = startGradient; + mCachedVelocityFactor = velocityFactor; + } + return mInterpolator; + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion for the case when the animation is making something + * disappear. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void applyDismissing(Animator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion for the case when the animation is making something + * disappear. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + private AnimatorProperties getDismissingProperties(float currValue, float endValue, + float velocity, float maxDistance) { + float maxLengthSeconds = (float) (mMaxLengthSeconds + * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f)); + float diff = Math.abs(endValue - currValue); + float velAbs = Math.abs(velocity); + float y2 = calculateLinearOutFasterInY2(velAbs); + + float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2; + Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2); + float durationSeconds = startGradient * diff / velAbs; + if (durationSeconds <= maxLengthSeconds) { + mAnimatorProperties.interpolator = mLinearOutFasterIn; + } else if (velAbs >= mMinVelocityPxPerSecond) { + + // Cross fade between linear-out-faster-in and linear interpolator with current + // velocity. + durationSeconds = maxLengthSeconds; + VelocityInterpolator velocityInterpolator + = new VelocityInterpolator(durationSeconds, velAbs, diff); + InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( + velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN); + mAnimatorProperties.interpolator = superInterpolator; + } else { + + // Just use a normal interpolator which doesn't take the velocity into account. + durationSeconds = maxLengthSeconds; + mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN; + } + mAnimatorProperties.duration = (long) (durationSeconds * 1000); + return mAnimatorProperties; + } + + /** + * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the + * velocity. The faster the velocity, the more "linear" the interpolator gets. + * + * @param velocity the velocity of the gesture. + * @return the y2 control point for a cubic bezier path interpolator + */ + private float calculateLinearOutFasterInY2(float velocity) { + float t = (velocity - mMinVelocityPxPerSecond) + / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond); + t = Math.max(0, Math.min(1, t)); + return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX; + } + + /** + * @return the minimum velocity a gesture needs to have to be considered a fling + */ + public float getMinVelocityPxPerSecond() { + return mMinVelocityPxPerSecond; + } + + /** + * An interpolator which interpolates two interpolators with an interpolator. + */ + private static final class InterpolatorInterpolator implements Interpolator { + + private Interpolator mInterpolator1; + private Interpolator mInterpolator2; + private Interpolator mCrossfader; + + InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, + Interpolator crossfader) { + mInterpolator1 = interpolator1; + mInterpolator2 = interpolator2; + mCrossfader = crossfader; + } + + @Override + public float getInterpolation(float input) { + float t = mCrossfader.getInterpolation(input); + return (1 - t) * mInterpolator1.getInterpolation(input) + + t * mInterpolator2.getInterpolation(input); + } + } + + /** + * An interpolator which interpolates with a fixed velocity. + */ + private static final class VelocityInterpolator implements Interpolator { + + private float mDurationSeconds; + private float mVelocity; + private float mDiff; + + private VelocityInterpolator(float durationSeconds, float velocity, float diff) { + mDurationSeconds = durationSeconds; + mVelocity = velocity; + mDiff = diff; + } + + @Override + public float getInterpolation(float input) { + float time = input * mDurationSeconds; + return time * mVelocity / mDiff; + } + } + + private static class AnimatorProperties { + Interpolator interpolator; + long duration; + } + +} diff --git a/src/com/android/launcher3/notification/Interpolators.java b/src/com/android/launcher3/notification/Interpolators.java new file mode 100644 index 000000000..664b313ed --- /dev/null +++ b/src/com/android/launcher3/notification/Interpolators.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.launcher3.notification; + +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * Utility class to receive interpolators from + */ +public class Interpolators { + public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); + + /** + * Interpolator to be used when animating a move based on a click. Pair with enough duration. + */ + public static final Interpolator TOUCH_RESPONSE = + new PathInterpolator(0.3f, 0f, 0.1f, 1f); +} diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java new file mode 100644 index 000000000..2965e4a63 --- /dev/null +++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2017 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.launcher3.notification; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.LauncherViewPropertyAnimator; +import com.android.launcher3.R; +import com.android.launcher3.graphics.IconPalette; +import com.android.launcher3.popup.PopupContainerWithArrow; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A {@link LinearLayout} that contains icons of notifications. If there is only one icon, + * we also supply the notification text/secondary text like we do for the main notification. + * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "+x" overflow. + */ +public class NotificationFooterLayout extends LinearLayout { + + public interface IconAnimationEndListener { + void onIconAnimationEnd(NotificationInfo animatedNotification); + } + + private static final int MAX_FOOTER_NOTIFICATIONS = 5; + + private static final Rect sTempRect = new Rect(); + + private final List<NotificationInfo> mNotifications = new ArrayList<>(); + private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>(); + private final Map<View, NotificationInfo> mViewsToInfos = new HashMap<>(); + + LinearLayout.LayoutParams mIconLayoutParams; + private LinearLayout mIconRow; + private int mTextColor; + + public NotificationFooterLayout(Context context) { + this(context, null, 0); + } + + public NotificationFooterLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + int size = getResources().getDimensionPixelSize( + R.dimen.notification_footer_icon_size); + int padding = getResources().getDimensionPixelSize( + R.dimen.deep_shortcut_drawable_padding); + mIconLayoutParams = new LayoutParams(size, size); + mIconLayoutParams.setMarginStart(padding); + mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mIconRow = (LinearLayout) findViewById(R.id.icon_row); + } + + public void applyColors(IconPalette iconPalette) { + setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor)); + findViewById(R.id.divider).setBackgroundColor(iconPalette.secondaryColor); + mTextColor = iconPalette.textColor; + } + + /** + * Keep track of the NotificationInfo, and then update the UI when + * {@link #commitNotificationInfos()} is called. + */ + public void addNotificationInfo(final NotificationInfo notificationInfo) { + if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) { + mNotifications.add(notificationInfo); + } else { + mOverflowNotifications.add(notificationInfo); + } + } + + /** + * Adds icons and potentially overflow text for all of the NotificationInfo's + * added using {@link #addNotificationInfo(NotificationInfo)}. + */ + public void commitNotificationInfos() { + mIconRow.removeAllViews(); + mViewsToInfos.clear(); + + for (int i = 0; i < mNotifications.size(); i++) { + NotificationInfo info = mNotifications.get(i); + addNotificationIconForInfo(info, false /* fromOverflow */); + } + + if (!mOverflowNotifications.isEmpty()) { + TextView overflowText = new TextView(getContext()); + overflowText.setTextColor(mTextColor); + updateOverflowText(overflowText); + mIconRow.addView(overflowText, mIconLayoutParams); + } + } + + private void addNotificationIconForInfo(NotificationInfo info, boolean fromOverflow) { + View icon = new View(getContext()); + icon.setBackground(info.iconDrawable); + icon.setOnClickListener(info); + int addIndex = mIconRow.getChildCount(); + if (fromOverflow) { + // Add the notification before the overflow view. + addIndex--; + icon.setAlpha(0); + icon.animate().alpha(1); + } + mIconRow.addView(icon, addIndex, mIconLayoutParams); + mViewsToInfos.put(icon, info); + } + + private void updateOverflowText(TextView overflowTextView) { + overflowTextView.setText(getResources().getString(R.string.deep_notifications_overflow, + mOverflowNotifications.size())); + } + + public void animateFirstNotificationTo(Rect toBounds, + final IconAnimationEndListener callback) { + AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); + final View firstNotification = mIconRow.getChildAt(0); + + Rect fromBounds = sTempRect; + firstNotification.getGlobalVisibleRect(fromBounds); + float scale = (float) toBounds.height() / fromBounds.height(); + Animator moveAndScaleIcon = new LauncherViewPropertyAnimator(firstNotification) + .translationY(toBounds.top - fromBounds.top + + (fromBounds.height() * scale - fromBounds.height()) / 2) + .scaleX(scale).scaleY(scale); + moveAndScaleIcon.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callback.onIconAnimationEnd(mViewsToInfos.get(firstNotification)); + } + }); + animation.play(moveAndScaleIcon); + + // Shift all notifications (not the overflow) over to fill the gap. + int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart(); + int numIcons = mIconRow.getChildCount() + - (mOverflowNotifications.isEmpty() ? 0 : 1); + for (int i = 1; i < numIcons; i++) { + final View child = mIconRow.getChildAt(i); + Animator shiftChild = new LauncherViewPropertyAnimator(child).translationX(-gapWidth); + shiftChild.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // We have to set the translation X to 0 when the new main notification + // is removed from the footer. + // TODO: remove it here instead of expecting trimNotifications to do so. + child.setTranslationX(0); + } + }); + animation.play(shiftChild); + } + animation.start(); + } + + public void trimNotifications(Set<String> notifications) { + if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) { + return; + } + Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator(); + while (overflowIterator.hasNext()) { + if (!notifications.contains(overflowIterator.next().notificationKey)) { + overflowIterator.remove(); + } + } + TextView overflowView = null; + for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) { + View child = mIconRow.getChildAt(i); + if (child instanceof TextView) { + overflowView = (TextView) child; + } else { + NotificationInfo childInfo = mViewsToInfos.get(child); + if (!notifications.contains(childInfo.notificationKey)) { + mIconRow.removeView(child); + mNotifications.remove(childInfo); + mViewsToInfos.remove(child); + if (!mOverflowNotifications.isEmpty()) { + NotificationInfo notification = mOverflowNotifications.remove(0); + mNotifications.add(notification); + addNotificationIconForInfo(notification, true /* fromOverflow */); + } + } + } + } + if (overflowView != null) { + if (mOverflowNotifications.isEmpty()) { + mIconRow.removeView(overflowView); + } else { + updateOverflowText(overflowView); + } + } + if (mIconRow.getChildCount() == 0) { + // There are no more icons in the secondary view, so hide it. + PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen( + Launcher.getLauncher(getContext())); + int newHeight = getResources().getDimensionPixelSize( + R.dimen.notification_footer_collapsed_height); + AnimatorSet collapseSecondary = LauncherAnimUtils.createAnimatorSet(); + collapseSecondary.play(popup.animateTranslationYBy(getHeight() - newHeight, + getResources().getInteger(R.integer.config_removeNotificationViewDuration))); + collapseSecondary.play(LauncherAnimUtils.animateViewHeight( + this, getHeight(), newHeight)); + collapseSecondary.start(); + } + } +} diff --git a/src/com/android/launcher3/badge/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java index 51f6a4f3a..33ab96679 100644 --- a/src/com/android/launcher3/badge/NotificationInfo.java +++ b/src/com/android/launcher3/notification/NotificationInfo.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.badge; +package com.android.launcher3.notification; import android.app.Notification; import android.app.PendingIntent; @@ -44,26 +44,28 @@ public class NotificationInfo implements View.OnClickListener { public final Drawable iconDrawable; public final PendingIntent intent; public final boolean autoCancel; + public final boolean dismissable; /** * Extracts the data that we need from the StatusBarNotification. */ - public NotificationInfo(Context context, StatusBarNotification notification) { - packageUserKey = PackageUserKey.fromNotification(notification); - notificationKey = notification.getKey(); - title = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TITLE); - text = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TEXT); - Icon icon = notification.getNotification().getLargeIcon(); + public NotificationInfo(Context context, StatusBarNotification statusBarNotification) { + packageUserKey = PackageUserKey.fromNotification(statusBarNotification); + notificationKey = statusBarNotification.getKey(); + Notification notification = statusBarNotification.getNotification(); + title = notification.extras.getCharSequence(Notification.EXTRA_TITLE); + text = notification.extras.getCharSequence(Notification.EXTRA_TEXT); + Icon icon = notification.getLargeIcon(); if (icon == null) { - icon = notification.getNotification().getSmallIcon(); + icon = notification.getSmallIcon(); iconDrawable = icon.loadDrawable(context); - iconDrawable.setTint(notification.getNotification().color); + iconDrawable.setTint(statusBarNotification.getNotification().color); } else { iconDrawable = icon.loadDrawable(context); } - intent = notification.getNotification().contentIntent; - autoCancel = (notification.getNotification().flags - & Notification.FLAG_AUTO_CANCEL) != 0; + intent = notification.contentIntent; + autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0; + dismissable = (notification.flags & Notification.FLAG_ONGOING_EVENT) == 0; } @Override diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java new file mode 100644 index 000000000..f38838261 --- /dev/null +++ b/src/com/android/launcher3/notification/NotificationItemView.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 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.launcher3.notification; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.graphics.IconPalette; +import com.android.launcher3.popup.PopupItemView; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.android.launcher3.LauncherAnimUtils.animateViewHeight; + +/** + * A {@link FrameLayout} that contains a header, main view and a footer. + * The main view contains the icon and text (title + subtext) of the first notification. + * The footer contains: A list of just the icons of all the notifications past the first one. + * @see NotificationFooterLayout + */ +public class NotificationItemView extends PopupItemView { + + private static final Rect sTempRect = new Rect(); + + private TextView mHeader; + private View mDivider; + private NotificationMainView mMainView; + private NotificationFooterLayout mFooter; + private SwipeHelper mSwipeHelper; + private boolean mAnimatingNextIcon; + private IconPalette mIconPalette; + + public NotificationItemView(Context context) { + this(context, null, 0); + } + + public NotificationItemView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NotificationItemView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mHeader = (TextView) findViewById(R.id.header); + mDivider = findViewById(R.id.divider); + mMainView = (NotificationMainView) findViewById(R.id.main_view); + mFooter = (NotificationFooterLayout) findViewById(R.id.footer); + mSwipeHelper = new SwipeHelper(SwipeHelper.X, mMainView, getContext()); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + getParent().requestDisallowInterceptTouchEvent(true); + return mSwipeHelper.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); + } + + @Override + protected ColorStateList getAttachedArrowColor() { + // This NotificationView itself has a different color that is only + // revealed when dismissing notifications. + return mFooter.getBackgroundTintList(); + } + + public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) { + if (notificationInfos.isEmpty()) { + return; + } + + NotificationInfo mainNotification = notificationInfos.get(0); + mMainView.applyNotificationInfo(mainNotification, mIconView); + + for (int i = 1; i < notificationInfos.size(); i++) { + mFooter.addNotificationInfo(notificationInfos.get(i)); + } + mFooter.commitNotificationInfos(); + } + + public void applyColors(IconPalette iconPalette) { + mIconPalette = iconPalette; + setBackgroundTintList(ColorStateList.valueOf(iconPalette.secondaryColor)); + mHeader.setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor)); + mHeader.setTextColor(ColorStateList.valueOf(iconPalette.textColor)); + mDivider.setBackgroundColor(iconPalette.secondaryColor); + mMainView.setBackgroundColor(iconPalette.backgroundColor); + mFooter.applyColors(iconPalette); + } + + public void trimNotifications(final Set<String> notificationKeys) { + boolean dismissedMainNotification = !notificationKeys.contains( + mMainView.getNotificationInfo().notificationKey); + if (dismissedMainNotification && !mAnimatingNextIcon) { + // Animate the next icon into place as the new main notification. + mAnimatingNextIcon = true; + mMainView.setVisibility(INVISIBLE); + mMainView.setTranslationX(0); + mIconView.getGlobalVisibleRect(sTempRect); + mFooter.animateFirstNotificationTo(sTempRect, + new NotificationFooterLayout.IconAnimationEndListener() { + @Override + public void onIconAnimationEnd(NotificationInfo newMainNotification) { + if (newMainNotification != null) { + mMainView.applyNotificationInfo(newMainNotification, mIconView, mIconPalette); + Set<String> footerNotificationKeys = new HashSet<>(notificationKeys); + footerNotificationKeys.remove(newMainNotification.notificationKey); + mFooter.trimNotifications(footerNotificationKeys); + mMainView.setVisibility(VISIBLE); + } + mAnimatingNextIcon = false; + } + }); + } else { + mFooter.trimNotifications(notificationKeys); + } + } + + public Animator createRemovalAnimation(int fullDuration) { + AnimatorSet animation = new AnimatorSet(); + int mainHeight = mMainView.getMeasuredHeight(); + Animator removeMainView = animateViewHeight(mMainView, mainHeight, 0); + removeMainView.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Remove the remaining views but take on their color instead of the darker one. + setBackgroundTintList(mHeader.getBackgroundTintList()); + removeAllViews(); + } + }); + Animator removeRest = LauncherAnimUtils.animateViewHeight(this, getHeight() - mainHeight, 0); + removeMainView.setDuration(fullDuration / 2); + removeRest.setDuration(fullDuration / 2); + removeMainView.setInterpolator(new LinearInterpolator()); + removeRest.setInterpolator(new LinearInterpolator()); + animation.playSequentially(removeMainView, removeRest); + return animation; + } +} diff --git a/src/com/android/launcher3/badge/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java index 1668a6267..3f9a58413 100644 --- a/src/com/android/launcher3/badge/NotificationListener.java +++ b/src/com/android/launcher3/notification/NotificationListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.badge; +package com.android.launcher3.notification; import android.app.Notification; import android.os.Handler; @@ -24,6 +24,7 @@ import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.support.annotation.Nullable; import android.support.v4.util.Pair; +import android.util.Log; import com.android.launcher3.LauncherModel; import com.android.launcher3.config.FeatureFlags; diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java new file mode 100644 index 000000000..2997d4010 --- /dev/null +++ b/src/com/android/launcher3/notification/NotificationMainView.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 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.launcher3.notification; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.LauncherViewPropertyAnimator; +import com.android.launcher3.R; +import com.android.launcher3.graphics.IconPalette; + +/** + * A {@link LinearLayout} that contains a single notification, e.g. icon + title + text. + */ +public class NotificationMainView extends LinearLayout implements SwipeHelper.Callback { + + private NotificationInfo mNotificationInfo; + private TextView mTitleView; + private TextView mTextView; + + public NotificationMainView(Context context) { + this(context, null, 0); + } + + public NotificationMainView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NotificationMainView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mTitleView = (TextView) findViewById(R.id.title); + mTextView = (TextView) findViewById(R.id.text); + } + + public void applyNotificationInfo(NotificationInfo mainNotification, View iconView) { + applyNotificationInfo(mainNotification, iconView, null); + } + + /** + * @param iconPalette if not null, indicates that the new info should be animated in, + * and that part of this animation includes animating the background + * from iconPalette.secondaryColor to iconPalette.backgroundColor. + */ + public void applyNotificationInfo(NotificationInfo mainNotification, View iconView, + @Nullable IconPalette iconPalette) { + boolean animate = iconPalette != null; + if (animate) { + mTitleView.setAlpha(0); + mTextView.setAlpha(0); + setBackgroundColor(iconPalette.secondaryColor); + } + mNotificationInfo = mainNotification; + mTitleView.setText(mNotificationInfo.title); + mTextView.setText(mNotificationInfo.text); + iconView.setBackground(mNotificationInfo.iconDrawable); + setOnClickListener(mNotificationInfo); + setTranslationX(0); + if (animate) { + AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); + Animator textFade = new LauncherViewPropertyAnimator(mTextView).alpha(1); + Animator titleFade = new LauncherViewPropertyAnimator(mTitleView).alpha(1); + ValueAnimator colorChange = ValueAnimator.ofArgb(iconPalette.secondaryColor, + iconPalette.backgroundColor); + colorChange.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + setBackgroundColor((Integer) valueAnimator.getAnimatedValue()); + } + }); + animation.playTogether(textFade, titleFade, colorChange); + animation.setDuration(150); + animation.start(); + } + } + + public NotificationInfo getNotificationInfo() { + return mNotificationInfo; + } + + + // SwipeHelper.Callback's + + @Override + public View getChildAtPosition(MotionEvent ev) { + return this; + } + + @Override + public boolean canChildBeDismissed(View v) { + return mNotificationInfo.dismissable; + } + + @Override + public boolean isAntiFalsingNeeded() { + return false; + } + + @Override + public void onBeginDrag(View v) { + } + + @Override + public void onChildDismissed(View v) { + Launcher.getLauncher(getContext()).getPopupDataProvider().cancelNotification( + mNotificationInfo.notificationKey); + } + + @Override + public void onDragCancelled(View v) { + } + + @Override + public void onChildSnappedBack(View animView, float targetLeft) { + } + + @Override + public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { + // Don't fade out. + return true; + } + + @Override + public float getFalsingThresholdFactor() { + return 1; + } +} diff --git a/src/com/android/launcher3/notification/SwipeHelper.java b/src/com/android/launcher3/notification/SwipeHelper.java new file mode 100644 index 000000000..3678174e6 --- /dev/null +++ b/src/com/android/launcher3/notification/SwipeHelper.java @@ -0,0 +1,687 @@ +/* + * Copyright (C) 2017 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.launcher3.notification; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.graphics.RectF; +import android.os.Handler; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; + +import com.android.launcher3.R; + +import java.util.HashMap; + +public class SwipeHelper { + static final String TAG = "SwipeHelper"; + private static final boolean DEBUG = false; + private static final boolean DEBUG_INVALIDATE = false; + private static final boolean SLOW_ANIMATIONS = false; // DEBUG; + private static final boolean CONSTRAIN_SWIPE = true; + private static final boolean FADE_OUT_DURING_SWIPE = true; + private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; + + public static final int X = 0; + public static final int Y = 1; + + private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec + private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms + private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms + private int MAX_DISMISS_VELOCITY = 4000; // dp/sec + private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms + + static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width + // beyond which swipe progress->0 + private float mMinSwipeProgress = 0f; + private float mMaxSwipeProgress = 1f; + + private FlingAnimationUtils mFlingAnimationUtils; + private float mPagingTouchSlop; + private Callback mCallback; + private Handler mHandler; + private int mSwipeDirection; + private VelocityTracker mVelocityTracker; + + private float mInitialTouchPos; + private float mPerpendicularInitialTouchPos; + private boolean mDragging; + private boolean mSnappingChild; + private View mCurrView; + private boolean mCanCurrViewBeDimissed; + private float mDensityScale; + private float mTranslation = 0; + + private boolean mLongPressSent; + private LongPressListener mLongPressListener; + private Runnable mWatchLongPress; + private long mLongPressTimeout; + + final private int[] mTmpPos = new int[2]; + private int mFalsingThreshold; + private boolean mTouchAboveFalsingThreshold; + private boolean mDisableHwLayers; + + private HashMap<View, Animator> mDismissPendingMap = new HashMap<>(); + + public SwipeHelper(int swipeDirection, Callback callback, Context context) { + mCallback = callback; + mHandler = new Handler(); + mSwipeDirection = swipeDirection; + mVelocityTracker = VelocityTracker.obtain(); + mDensityScale = context.getResources().getDisplayMetrics().density; + mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); + + mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press! + mFalsingThreshold = context.getResources().getDimensionPixelSize( + R.dimen.swipe_helper_falsing_threshold); + mFlingAnimationUtils = new FlingAnimationUtils(context, getMaxEscapeAnimDuration() / 1000f); + } + + public void setLongPressListener(LongPressListener listener) { + mLongPressListener = listener; + } + + public void setDensityScale(float densityScale) { + mDensityScale = densityScale; + } + + public void setPagingTouchSlop(float pagingTouchSlop) { + mPagingTouchSlop = pagingTouchSlop; + } + + public void setDisableHardwareLayers(boolean disableHwLayers) { + mDisableHwLayers = disableHwLayers; + } + + private float getPos(MotionEvent ev) { + return mSwipeDirection == X ? ev.getX() : ev.getY(); + } + + private float getPerpendicularPos(MotionEvent ev) { + return mSwipeDirection == X ? ev.getY() : ev.getX(); + } + + protected float getTranslation(View v) { + return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); + } + + private float getVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getXVelocity() : + vt.getYVelocity(); + } + + protected ObjectAnimator createTranslationAnimation(View v, float newPos) { + ObjectAnimator anim = ObjectAnimator.ofFloat(v, + mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); + return anim; + } + + private float getPerpendicularVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getYVelocity() : + vt.getXVelocity(); + } + + protected Animator getViewTranslationAnimator(View v, float target, + AnimatorUpdateListener listener) { + ObjectAnimator anim = createTranslationAnimation(v, target); + if (listener != null) { + anim.addUpdateListener(listener); + } + return anim; + } + + protected void setTranslation(View v, float translate) { + if (v == null) { + return; + } + if (mSwipeDirection == X) { + v.setTranslationX(translate); + } else { + v.setTranslationY(translate); + } + } + + protected float getSize(View v) { + return mSwipeDirection == X ? v.getMeasuredWidth() : + v.getMeasuredHeight(); + } + + public void setMinSwipeProgress(float minSwipeProgress) { + mMinSwipeProgress = minSwipeProgress; + } + + public void setMaxSwipeProgress(float maxSwipeProgress) { + mMaxSwipeProgress = maxSwipeProgress; + } + + private float getSwipeProgressForOffset(View view, float translation) { + float viewSize = getSize(view); + float result = Math.abs(translation / viewSize); + return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress); + } + + private float getSwipeAlpha(float progress) { + return Math.min(0, Math.max(1, progress / SWIPE_PROGRESS_FADE_END)); + } + + private void updateSwipeProgressFromOffset(View animView, boolean dismissable) { + updateSwipeProgressFromOffset(animView, dismissable, getTranslation(animView)); + } + + private void updateSwipeProgressFromOffset(View animView, boolean dismissable, + float translation) { + float swipeProgress = getSwipeProgressForOffset(animView, translation); + if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) { + if (FADE_OUT_DURING_SWIPE && dismissable) { + float alpha = swipeProgress; + if (!mDisableHwLayers) { + if (alpha != 0f && alpha != 1f) { + animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } else { + animView.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + animView.setAlpha(getSwipeAlpha(swipeProgress)); + } + } + invalidateGlobalRegion(animView); + } + + // invalidate the view's own bounds all the way up the view hierarchy + public static void invalidateGlobalRegion(View view) { + invalidateGlobalRegion( + view, + new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); + } + + // invalidate a rectangle relative to the view's coordinate system all the way up the view + // hierarchy + public static void invalidateGlobalRegion(View view, RectF childBounds) { + //childBounds.offset(view.getTranslationX(), view.getTranslationY()); + if (DEBUG_INVALIDATE) + Log.v(TAG, "-------------"); + while (view.getParent() != null && view.getParent() instanceof View) { + view = (View) view.getParent(); + view.getMatrix().mapRect(childBounds); + view.invalidate((int) Math.floor(childBounds.left), + (int) Math.floor(childBounds.top), + (int) Math.ceil(childBounds.right), + (int) Math.ceil(childBounds.bottom)); + if (DEBUG_INVALIDATE) { + Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) + + "," + (int) Math.floor(childBounds.top) + + "," + (int) Math.ceil(childBounds.right) + + "," + (int) Math.ceil(childBounds.bottom)); + } + } + } + + public void removeLongPressCallback() { + if (mWatchLongPress != null) { + mHandler.removeCallbacks(mWatchLongPress); + mWatchLongPress = null; + } + } + + public boolean onInterceptTouchEvent(final MotionEvent ev) { + final int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mTouchAboveFalsingThreshold = false; + mDragging = false; + mSnappingChild = false; + mLongPressSent = false; + mVelocityTracker.clear(); + mCurrView = mCallback.getChildAtPosition(ev); + + if (mCurrView != null) { + onDownUpdate(mCurrView); + mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); + mVelocityTracker.addMovement(ev); + mInitialTouchPos = getPos(ev); + mPerpendicularInitialTouchPos = getPerpendicularPos(ev); + mTranslation = getTranslation(mCurrView); + if (mLongPressListener != null) { + if (mWatchLongPress == null) { + mWatchLongPress = new Runnable() { + @Override + public void run() { + if (mCurrView != null && !mLongPressSent) { + mLongPressSent = true; + mCurrView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + mCurrView.getLocationOnScreen(mTmpPos); + final int x = (int) ev.getRawX() - mTmpPos[0]; + final int y = (int) ev.getRawY() - mTmpPos[1]; + mLongPressListener.onLongPress(mCurrView, x, y); + } + } + }; + } + mHandler.postDelayed(mWatchLongPress, mLongPressTimeout); + } + } + break; + + case MotionEvent.ACTION_MOVE: + if (mCurrView != null && !mLongPressSent) { + mVelocityTracker.addMovement(ev); + float pos = getPos(ev); + float perpendicularPos = getPerpendicularPos(ev); + float delta = pos - mInitialTouchPos; + float deltaPerpendicular = perpendicularPos - mPerpendicularInitialTouchPos; + if (Math.abs(delta) > mPagingTouchSlop + && Math.abs(delta) > Math.abs(deltaPerpendicular)) { + mCallback.onBeginDrag(mCurrView); + mDragging = true; + mInitialTouchPos = getPos(ev); + mTranslation = getTranslation(mCurrView); + removeLongPressCallback(); + } + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + final boolean captured = (mDragging || mLongPressSent); + mDragging = false; + mCurrView = null; + mLongPressSent = false; + removeLongPressCallback(); + if (captured) return true; + break; + } + return mDragging || mLongPressSent; + } + + /** + * @param view The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should move + * @param useAccelerateInterpolator Should an accelerating Interpolator be used + */ + public void dismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { + dismissChild(view, velocity, null /* endAction */, 0 /* delay */, + useAccelerateInterpolator, 0 /* fixedDuration */, false /* isDismissAll */); + } + + /** + * @param animView The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should move + * @param endAction The action to perform at the end + * @param delay The delay after which we should start + * @param useAccelerateInterpolator Should an accelerating Interpolator be used + * @param fixedDuration If not 0, this exact duration will be taken + */ + public void dismissChild(final View animView, float velocity, final Runnable endAction, + long delay, boolean useAccelerateInterpolator, long fixedDuration, + boolean isDismissAll) { + final boolean canBeDismissed = mCallback.canChildBeDismissed(animView); + float newPos; + boolean isLayoutRtl = animView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + + // if we use the Menu to dismiss an item in landscape, animate up + boolean animateUpForMenu = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll) + && mSwipeDirection == Y; + // if the language is rtl we prefer swiping to the left + boolean animateLeftForRtl = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll) + && isLayoutRtl; + boolean animateLeft = velocity < 0 + || (velocity == 0 && getTranslation(animView) < 0 && !isDismissAll); + + if (animateLeft || animateLeftForRtl || animateUpForMenu) { + newPos = -getSize(animView); + } else { + newPos = getSize(animView); + } + long duration; + if (fixedDuration == 0) { + duration = MAX_ESCAPE_ANIMATION_DURATION; + if (velocity != 0) { + duration = Math.min(duration, + (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math + .abs(velocity)) + ); + } else { + duration = DEFAULT_ESCAPE_ANIMATION_DURATION; + } + } else { + duration = fixedDuration; + } + + if (!mDisableHwLayers) { + animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed); + } + }; + + Animator anim = getViewTranslationAnimator(animView, newPos, updateListener); + if (anim == null) { + return; + } + if (useAccelerateInterpolator) { + anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + anim.setDuration(duration); + } else { + mFlingAnimationUtils.applyDismissing(anim, getTranslation(animView), + newPos, velocity, getSize(animView)); + } + if (delay > 0) { + anim.setStartDelay(delay); + } + anim.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + public void onAnimationEnd(Animator animation) { + updateSwipeProgressFromOffset(animView, canBeDismissed); + mDismissPendingMap.remove(animView); + if (!mCancelled) { + mCallback.onChildDismissed(animView); + } + if (endAction != null) { + endAction.run(); + } + if (!mDisableHwLayers) { + animView.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + }); + + prepareDismissAnimation(animView, anim); + mDismissPendingMap.put(animView, anim); + anim.start(); + } + + /** + * Called to update the dismiss animation. + */ + protected void prepareDismissAnimation(View view, Animator anim) { + // Do nothing + } + + public void snapChild(final View animView, final float targetLeft, float velocity) { + final boolean canBeDismissed = mCallback.canChildBeDismissed(animView); + AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed); + } + }; + + Animator anim = getViewTranslationAnimator(animView, targetLeft, updateListener); + if (anim == null) { + return; + } + int duration = SNAP_ANIM_LEN; + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + mSnappingChild = false; + updateSwipeProgressFromOffset(animView, canBeDismissed); + mCallback.onChildSnappedBack(animView, targetLeft); + } + }); + prepareSnapBackAnimation(animView, anim); + mSnappingChild = true; + anim.start(); + } + + /** + * Called to update the snap back animation. + */ + protected void prepareSnapBackAnimation(View view, Animator anim) { + // Do nothing + } + + /** + * Called when there's a down event. + */ + public void onDownUpdate(View currView) { + // Do nothing + } + + /** + * Called on a move event. + */ + protected void onMoveUpdate(View view, float totalTranslation, float delta) { + // Do nothing + } + + /** + * Called in {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} when the current + * view is being animated to dismiss or snap. + */ + public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) { + updateSwipeProgressFromOffset(animView, canBeDismissed, value); + } + + private void snapChildInstantly(final View view) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + setTranslation(view, 0); + updateSwipeProgressFromOffset(view, canAnimViewBeDismissed); + } + + /** + * Called when a view is updated to be non-dismissable, if the view was being dismissed before + * the update this will handle snapping it back into place. + * + * @param view the view to snap if necessary. + * @param animate whether to animate the snap or not. + * @param targetLeft the target to snap to. + */ + public void snapChildIfNeeded(final View view, boolean animate, float targetLeft) { + if ((mDragging && mCurrView == view) || mSnappingChild) { + return; + } + boolean needToSnap = false; + Animator dismissPendingAnim = mDismissPendingMap.get(view); + if (dismissPendingAnim != null) { + needToSnap = true; + dismissPendingAnim.cancel(); + } else if (getTranslation(view) != 0) { + needToSnap = true; + } + if (needToSnap) { + if (animate) { + snapChild(view, targetLeft, 0.0f /* velocity */); + } else { + snapChildInstantly(view); + } + } + } + + public boolean onTouchEvent(MotionEvent ev) { + if (mLongPressSent) { + return true; + } + + if (!mDragging) { + if (mCallback.getChildAtPosition(ev) != null) { + + // We are dragging directly over a card, make sure that we also catch the gesture + // even if nobody else wants the touch event. + onInterceptTouchEvent(ev); + return true; + } else { + + // We are not doing anything, make sure the long press callback + // is not still ticking like a bomb waiting to go off. + removeLongPressCallback(); + return false; + } + } + + mVelocityTracker.addMovement(ev); + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_OUTSIDE: + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + float delta = getPos(ev) - mInitialTouchPos; + float absDelta = Math.abs(delta); + if (absDelta >= getFalsingThreshold()) { + mTouchAboveFalsingThreshold = true; + } + // don't let items that can't be dismissed be dragged more than + // maxScrollDistance + if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { + float size = getSize(mCurrView); + float maxScrollDistance = 0.25f * size; + if (absDelta >= size) { + delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2)); + } + } + + setTranslation(mCurrView, mTranslation + delta); + updateSwipeProgressFromOffset(mCurrView, mCanCurrViewBeDimissed); + onMoveUpdate(mCurrView, mTranslation + delta, delta); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mCurrView == null) { + break; + } + mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity()); + float velocity = getVelocity(mVelocityTracker); + + if (!handleUpEvent(ev, mCurrView, velocity, getTranslation(mCurrView))) { + if (isDismissGesture(ev)) { + // flingadingy + dismissChild(mCurrView, velocity, + !swipedFastEnough() /* useAccelerateInterpolator */); + } else { + // snappity + mCallback.onDragCancelled(mCurrView); + snapChild(mCurrView, 0 /* leftTarget */, velocity); + } + mCurrView = null; + } + mDragging = false; + break; + } + return true; + } + + private int getFalsingThreshold() { + float factor = mCallback.getFalsingThresholdFactor(); + return (int) (mFalsingThreshold * factor); + } + + private float getMaxVelocity() { + return MAX_DISMISS_VELOCITY * mDensityScale; + } + + protected float getEscapeVelocity() { + return getUnscaledEscapeVelocity() * mDensityScale; + } + + protected float getUnscaledEscapeVelocity() { + return SWIPE_ESCAPE_VELOCITY; + } + + protected long getMaxEscapeAnimDuration() { + return MAX_ESCAPE_ANIMATION_DURATION; + } + + protected boolean swipedFarEnough() { + float translation = getTranslation(mCurrView); + return DISMISS_IF_SWIPED_FAR_ENOUGH && Math.abs(translation) > 0.4 * getSize(mCurrView); + } + + protected boolean isDismissGesture(MotionEvent ev) { + boolean falsingDetected = mCallback.isAntiFalsingNeeded() && !mTouchAboveFalsingThreshold; + return !falsingDetected && (swipedFastEnough() || swipedFarEnough()) + && ev.getActionMasked() == MotionEvent.ACTION_UP + && mCallback.canChildBeDismissed(mCurrView); + } + + protected boolean swipedFastEnough() { + float velocity = getVelocity(mVelocityTracker); + float translation = getTranslation(mCurrView); + boolean ret = (Math.abs(velocity) > getEscapeVelocity()) + && (velocity > 0) == (translation > 0); + return ret; + } + + protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity, + float translation) { + return false; + } + + public interface Callback { + View getChildAtPosition(MotionEvent ev); + + boolean canChildBeDismissed(View v); + + boolean isAntiFalsingNeeded(); + + void onBeginDrag(View v); + + void onChildDismissed(View v); + + void onDragCancelled(View v); + + /** + * Called when the child is snapped to a position. + * + * @param animView the view that was snapped. + * @param targetLeft the left position the view was snapped to. + */ + void onChildSnappedBack(View animView, float targetLeft); + + /** + * Updates the swipe progress on a child. + * + * @return if true, prevents the default alpha fading. + */ + boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress); + + /** + * @return The factor the falsing threshold should be multiplied with + */ + float getFalsingThresholdFactor(); + } + + /** + * Equivalent to View.OnLongClickListener with coordinates + */ + public interface LongPressListener { + /** + * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates + * @return whether the longpress was handled + */ + boolean onLongPress(View v, int x, int y); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 95d51dc9c..c69cf6d7f 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -20,10 +20,10 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; -import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; @@ -33,6 +33,7 @@ import android.graphics.drawable.ShapeDrawable; import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; @@ -65,15 +66,18 @@ import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.graphics.TriangleShape; +import com.android.launcher3.notification.NotificationItemView; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; import com.android.launcher3.util.PackageUserKey; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import java.util.Map; -import static com.android.launcher3.userevent.nano.LauncherLogProto.*; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; +import static com.android.launcher3.userevent.nano.LauncherLogProto.Target; /** * A container for shortcuts to deep links within apps. @@ -137,19 +141,22 @@ public class PopupContainerWithArrow extends AbstractFloatingView } ItemInfo itemInfo = (ItemInfo) icon.getTag(); List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo); - if (shortcutIds.size() > 0) { + String[] notificationKeys = launcher.getPopupDataProvider() + .getNotificationKeysForItem(itemInfo); + if (shortcutIds.size() > 0 || notificationKeys.length > 0) { final PopupContainerWithArrow container = (PopupContainerWithArrow) launcher.getLayoutInflater().inflate( R.layout.popup_container, launcher.getDragLayer(), false); container.setVisibility(View.INVISIBLE); launcher.getDragLayer().addView(container); - container.populateAndShow(icon, shortcutIds); + container.populateAndShow(icon, shortcutIds, notificationKeys); return container; } return null; } - public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds) { + public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, + final String[] notificationKeys) { final Resources resources = getResources(); final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width); final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height); @@ -159,8 +166,9 @@ public class PopupContainerWithArrow extends AbstractFloatingView R.dimen.deep_shortcuts_arrow_vertical_offset); // Add dummy views first, and populate with real info when ready. - PopupPopulator.Item[] itemsToPopulate = PopupPopulator.getItemsToPopulate(shortcutIds); - addDummyViews(originalIcon, itemsToPopulate); + PopupPopulator.Item[] itemsToPopulate = PopupPopulator + .getItemsToPopulate(shortcutIds, notificationKeys); + addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1); measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset); @@ -169,13 +177,14 @@ public class PopupContainerWithArrow extends AbstractFloatingView if (reverseOrder) { removeAllViews(); itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate); - addDummyViews(originalIcon, itemsToPopulate); + addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1); measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset); } List<DeepShortcutView> shortcutViews = new ArrayList<>(); + NotificationItemView notificationView = null; for (int i = 0; i < getChildCount(); i++) { View item = getChildAt(i); switch (itemsToPopulate[i]) { @@ -186,6 +195,11 @@ public class PopupContainerWithArrow extends AbstractFloatingView shortcutViews.add((DeepShortcutView) item); } break; + case NOTIFICATION: + notificationView = (NotificationItemView) item; + IconPalette iconPalette = originalIcon.getIconPalette(); + notificationView.applyColors(iconPalette); + break; } } @@ -193,6 +207,8 @@ public class PopupContainerWithArrow extends AbstractFloatingView mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight); mArrow.setPivotX(arrowWidth / 2); mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight); + PopupItemView firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0); + mArrow.setBackgroundTintList(firstItem.getAttachedArrowColor()); animateOpen(); @@ -204,16 +220,24 @@ public class PopupContainerWithArrow extends AbstractFloatingView final Looper workerLooper = LauncherModel.getWorkerLooper(); new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable( mLauncher, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()), - this, shortcutIds, shortcutViews)); + this, shortcutIds, shortcutViews, notificationKeys, notificationView)); } - private void addDummyViews(BubbleTextView originalIcon, PopupPopulator.Item[] itemsToPopulate) { - final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing); + private void addDummyViews(BubbleTextView originalIcon, + PopupPopulator.Item[] itemsToPopulate, boolean secondaryNotificationViewHasIcons) { + final Resources res = getResources(); + final int spacing = res.getDimensionPixelSize(R.dimen.deep_shortcuts_spacing); final LayoutInflater inflater = mLauncher.getLayoutInflater(); int numItems = itemsToPopulate.length; for (int i = 0; i < numItems; i++) { final PopupItemView item = (PopupItemView) inflater.inflate( itemsToPopulate[i].layoutId, this, false); + if (itemsToPopulate[i] == PopupPopulator.Item.NOTIFICATION) { + int secondaryHeight = secondaryNotificationViewHasIcons ? + res.getDimensionPixelSize(R.dimen.notification_footer_height) : + res.getDimensionPixelSize(R.dimen.notification_footer_collapsed_height); + item.findViewById(R.id.footer).getLayoutParams().height = secondaryHeight; + } if (i < numItems - 1) { ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing; } @@ -550,6 +574,78 @@ public class PopupContainerWithArrow extends AbstractFloatingView return false; } + public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) { + final NotificationItemView notificationView = (NotificationItemView) findViewById(R.id.notification_view); + if (notificationView == null) { + return; + } + ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); + BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo)); + if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) { + AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet(); + final int duration = getResources().getInteger( + R.integer.config_removeNotificationViewDuration); + final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing); + removeNotification.play(animateTranslationYBy(notificationView.getHeight() + spacing, + duration)); + Animator reduceHeight = notificationView.createRemovalAnimation(duration); + final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2) + : notificationView; + if (removeMarginView != null) { + ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration); + removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin + = (int) (spacing * (float) valueAnimator.getAnimatedValue()); + } + }); + removeNotification.play(removeMargin); + } + removeNotification.play(reduceHeight); + Animator fade = new LauncherViewPropertyAnimator(notificationView).alpha(0) + .setDuration(duration); + fade.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + removeView(notificationView); + if (getItemCount() == 0) { + close(false); + return; + } + View firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0); + mArrow.setBackgroundTintList(firstItem.getBackgroundTintList()); + } + }); + removeNotification.play(fade); + final long arrowScaleDuration = getResources().getInteger( + R.integer.config_deepShortcutArrowOpenDuration); + Animator hideArrow = new LauncherViewPropertyAnimator(mArrow) + .scaleX(0).scaleY(0).setDuration(arrowScaleDuration); + hideArrow.setStartDelay(0); + Animator showArrow = new LauncherViewPropertyAnimator(mArrow) + .scaleX(1).scaleY(1).setDuration(arrowScaleDuration); + showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5)); + removeNotification.playSequentially(hideArrow, showArrow); + removeNotification.start(); + return; + } + notificationView.trimNotifications(badgeInfo.getNotificationKeys()); + } + + /** + * Animates the translationY of this container if it is open above the icon. + * If it is below the icon, the container already shifts up when the height + * of a child (e.g. NotificationView) changes, so the translation isn't necessary. + */ + public @Nullable Animator animateTranslationYBy(int translationY, int duration) { + if (mIsAboveIcon) { + return new LauncherViewPropertyAnimator(this) + .translationY(getTranslationY() + translationY).setDuration(duration); + } + return null; + } + @Override public boolean supportsAppInfoDropTarget() { return true; diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index b671c364f..f6fdb767f 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -23,7 +23,7 @@ import android.util.Log; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.badge.BadgeInfo; -import com.android.launcher3.badge.NotificationListener; +import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.MultiHashMap; @@ -75,6 +75,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan mPackageUserToBadgeInfos.remove(removedPackageUserKey); } mLauncher.updateIconBadges(Collections.singleton(removedPackageUserKey)); + + PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher); + if (openContainer != null) { + openContainer.trimNotifications(mPackageUserToBadgeInfos); + } } } @@ -110,6 +115,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan if (!updatedBadges.isEmpty()) { mLauncher.updateIconBadges(updatedBadges.keySet()); } + + PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher); + if (openContainer != null) { + openContainer.trimNotifications(updatedBadges); + } } public void setDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) { @@ -140,6 +150,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan public String[] getNotificationKeysForItem(ItemInfo info) { BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info)); + if (badgeInfo == null) { return new String[0]; } Set<String> notificationKeys = badgeInfo.getNotificationKeys(); return notificationKeys.toArray(new String[notificationKeys.size()]); } diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java index 25d496a4b..6af6e7d2c 100644 --- a/src/com/android/launcher3/popup/PopupItemView.java +++ b/src/com/android/launcher3/popup/PopupItemView.java @@ -20,6 +20,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.Point; import android.graphics.Rect; import android.util.AttributeSet; @@ -72,6 +73,10 @@ public abstract class PopupItemView extends FrameLayout mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); } + protected ColorStateList getAttachedArrowColor() { + return getBackgroundTintList(); + } + public boolean willDrawIcon() { return true; } @@ -158,7 +163,8 @@ public abstract class PopupItemView extends FrameLayout public ZoomRevealOutlineProvider(int x, int y, Rect pillRect, View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) { - super(x, y, pillRect); + super(x, y, pillRect, zoomView.getResources().getDimensionPixelSize( + R.dimen.bg_pill_radius)); mTranslateView = translateView; mZoomView = zoomView; mFullHeight = pillRect.height(); diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java index b5a59b02b..f990fa2d8 100644 --- a/src/com/android/launcher3/popup/PopupPopulator.java +++ b/src/com/android/launcher3/popup/PopupPopulator.java @@ -19,12 +19,15 @@ package com.android.launcher3.popup; import android.content.ComponentName; import android.os.Handler; import android.os.UserHandle; +import android.service.notification.StatusBarNotification; import android.support.annotation.VisibleForTesting; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.notification.NotificationInfo; +import com.android.launcher3.notification.NotificationItemView; import com.android.launcher3.graphics.LauncherIcons; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.DeepShortcutView; @@ -45,7 +48,8 @@ public class PopupPopulator { @VisibleForTesting static final int NUM_DYNAMIC = 2; public enum Item { - SHORTCUT(R.layout.deep_shortcut); + SHORTCUT(R.layout.deep_shortcut), + NOTIFICATION(R.layout.notification); public final int layoutId; @@ -54,12 +58,18 @@ public class PopupPopulator { } } - public static Item[] getItemsToPopulate(List<String> shortcutIds) { - int numItems = Math.min(MAX_ITEMS, shortcutIds.size()); + public static Item[] getItemsToPopulate(List<String> shortcutIds, String[] notificationKeys) { + boolean hasNotifications = notificationKeys.length > 0; + int numNotificationItems = hasNotifications ? 1 : 0; + int numItems = Math.min(MAX_ITEMS, shortcutIds.size() + numNotificationItems); Item[] items = new Item[numItems]; for (int i = 0; i < numItems; i++) { items[i] = Item.SHORTCUT; } + if (hasNotifications) { + // The notification layout is always first. + items[0] = Item.NOTIFICATION; + } return items; } @@ -134,12 +144,24 @@ public class PopupPopulator { public static Runnable createUpdateRunnable(final Launcher launcher, ItemInfo originalInfo, final Handler uiHandler, final PopupContainerWithArrow container, - final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews) { + final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews, + final String[] notificationKeys, final NotificationItemView notificationView) { final ComponentName activity = originalInfo.getTargetComponent(); final UserHandle user = originalInfo.user; return new Runnable() { @Override public void run() { + if (notificationView != null) { + List<StatusBarNotification> notifications = launcher.getPopupDataProvider() + .getStatusBarNotificationsForKeys(notificationKeys); + List<NotificationInfo> infos = new ArrayList<>(notifications.size()); + for (int i = 0; i < notifications.size(); i++) { + StatusBarNotification notification = notifications.get(i); + infos.add(new NotificationInfo(launcher, notification)); + } + uiHandler.post(new UpdateNotificationChild(notificationView, infos)); + } + final List<ShortcutInfoCompat> shortcuts = PopupPopulator.sortAndFilterShortcuts( DeepShortcutManager.getInstance(launcher).queryForShortcutsContainer( activity, shortcutIds, user)); @@ -176,4 +198,21 @@ public class PopupPopulator { mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer); } } + + /** Updates the child of this container at the given index based on the given shortcut info. */ + private static class UpdateNotificationChild implements Runnable { + private NotificationItemView mNotificationView; + private List<NotificationInfo> mNotificationInfos; + + public UpdateNotificationChild(NotificationItemView notificationView, + List<NotificationInfo> notificationInfos) { + mNotificationView = notificationView; + mNotificationInfos = notificationInfos; + } + + @Override + public void run() { + mNotificationView.applyNotificationInfos(mNotificationInfos); + } + } } diff --git a/src/com/android/launcher3/util/PillRevealOutlineProvider.java b/src/com/android/launcher3/util/PillRevealOutlineProvider.java index 1a3b48665..a57d69fab 100644 --- a/src/com/android/launcher3/util/PillRevealOutlineProvider.java +++ b/src/com/android/launcher3/util/PillRevealOutlineProvider.java @@ -28,6 +28,7 @@ public class PillRevealOutlineProvider extends RevealOutlineAnimation { private int mCenterX; private int mCenterY; + private float mFinalRadius; protected Rect mPillRect; /** @@ -36,10 +37,14 @@ public class PillRevealOutlineProvider extends RevealOutlineAnimation { * @param pillRect round rect that represents the final pill shape */ public PillRevealOutlineProvider(int x, int y, Rect pillRect) { + this(x, y, pillRect, pillRect.height() / 2f); + } + + public PillRevealOutlineProvider(int x, int y, Rect pillRect, float radius) { mCenterX = x; mCenterY = y; mPillRect = pillRect; - mOutlineRadius = pillRect.height() / 2f; + mOutlineRadius = mFinalRadius = radius; } @Override @@ -58,6 +63,6 @@ public class PillRevealOutlineProvider extends RevealOutlineAnimation { mOutline.top = Math.max(mPillRect.top, mCenterY - currentSize); mOutline.right = Math.min(mPillRect.right, mCenterX + currentSize); mOutline.bottom = Math.min(mPillRect.bottom, mCenterY + currentSize); - mOutlineRadius = mOutline.height() / 2; + mOutlineRadius = Math.min(mFinalRadius, mOutline.height() / 2); } } |