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
This commit is contained in:
Tony Wickham 2017-01-20 09:38:25 -08:00
parent f3d02e4716
commit 9438ed414f
29 changed files with 2123 additions and 42 deletions

View File

@ -76,7 +76,7 @@
android:process=":wallpaper_chooser">
</service>
<service android:name="com.android.launcher3.badging.NotificationListener"
<service android:name="com.android.launcher3.notification.NotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:bottomLeftRadius="@dimen/bg_pill_radius"
android:bottomRightRadius="@dimen/bg_pill_radius" />
</shape>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:topLeftRadius="@dimen/bg_pill_radius"
android:topRightRadius="@dimen/bg_pill_radius" />
</shape>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.notification.NotificationItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/notification_view"
android:layout_width="@dimen/bg_pill_width"
android:layout_height="wrap_content"
android:elevation="@dimen/deep_shortcuts_elevation"
android:background="@drawable/bg_white_pill">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:clipChildren="false">
<TextView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_footer_collapsed_height"
android:gravity="center_vertical"
android:textAlignment="center"
android:text="@string/notifications_header"
android:elevation="@dimen/notification_elevation"
android:background="@drawable/bg_white_pill_top" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_divider_height"
android:layout_below="@id/header" />
<include layout="@layout/notification_main"
android:id="@+id/main_view"
android:layout_width="match_parent"
android:layout_height="@dimen/bg_pill_height"
android:layout_below="@id/divider" />
<include layout="@layout/notification_footer"
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_footer_height"
android:layout_below="@id/main_view" />
</RelativeLayout>
</com.android.launcher3.notification.NotificationItemView>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.notification.NotificationFooterLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@drawable/bg_white_pill_bottom"
android:elevation="@dimen/notification_elevation"
android:clipChildren="false" >
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_divider_height"/>
<LinearLayout
android:id="@+id/icon_row"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:padding="@dimen/notification_footer_icon_row_padding"
android:clipToPadding="false"
android:clipChildren="false"/>
</com.android.launcher3.notification.NotificationFooterLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.notification.NotificationMainView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:focusable="true"
android:background="@drawable/bg_pill_focused"
android:elevation="@dimen/notification_elevation" >
<View
android:id="@+id/popup_item_icon"
android:layout_width="@dimen/notification_icon_size"
android:layout_height="@dimen/notification_icon_size"
android:layout_marginStart="@dimen/notification_icon_margin_start"
android:layout_gravity="center_vertical" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginStart="@dimen/notification_text_margin_start"
android:gravity="center_vertical">
<TextView
android:id="@+id/title"
style="@style/Icon.DeepNotification"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text"
style="@style/Icon.DeepNotification.SubText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.android.launcher3.notification.NotificationMainView>

View File

@ -97,12 +97,13 @@
<!-- View ID used by PreviewImageView to cache its instance -->
<item type="id" name="preview_image_id" />
<!-- Deep shortcuts -->
<!-- Popup items -->
<integer name="config_deepShortcutOpenDuration">220</integer>
<integer name="config_deepShortcutArrowOpenDuration">80</integer>
<integer name="config_deepShortcutOpenStagger">40</integer>
<integer name="config_deepShortcutCloseDuration">150</integer>
<integer name="config_deepShortcutCloseStagger">20</integer>
<integer name="config_removeNotificationViewDuration">300</integer>
<!-- Accessibility actions -->
<item type="id" name="action_remove" />

View File

@ -172,9 +172,22 @@
<!-- Icon badges (with notification counts) -->
<dimen name="badge_size">24dp</dimen>
<dimen name="badge_text_size">12dp</dimen>
<dimen name="notification_icon_size">28dp</dimen>
<dimen name="notification_footer_icon_size">24dp</dimen>
<!-- (icon_size - secondary_icon_size) / 2 -->
<!-- Notifications -->
<dimen name="notification_footer_icon_row_padding">2dp</dimen>
<dimen name="notification_icon_margin_start">8dp</dimen>
<dimen name="notification_text_margin_start">8dp</dimen>
<dimen name="notification_footer_height">36dp</dimen>
<!-- The height to use when there are no icons in the footer -->
<dimen name="notification_footer_collapsed_height">@dimen/bg_pill_radius</dimen>
<dimen name="notification_elevation">2dp</dimen>
<dimen name="notification_divider_height">0.5dp</dimen>
<dimen name="swipe_helper_falsing_threshold">70dp</dimen>
<!-- Other -->
<!-- Approximates the system status bar height. Not guaranteed to be always be correct. -->
<dimen name="status_bar_height">24dp</dimen>
</resources>

View File

@ -67,6 +67,13 @@
<!-- Label for the button which allows the user to get app search results. [CHAR_LIMIT=50] -->
<string name="all_apps_search_market_message">Search for more apps</string>
<!-- Deep items -->
<!-- Text to indicate more items that couldn't be displayed due to space constraints.
The text must fit in the size of a small icon [CHAR_LIMIT=3] -->
<string name="deep_notifications_overflow" translatable="false">+%1$d</string>
<!-- Text to display as the header above notifications. [CHAR_LIMIT=30] -->
<string name="notifications_header" translatable="false">Notifications</string>
<!-- Drag and drop -->
<skip />
<!-- Error message when user has filled a home screen -->

View File

@ -112,6 +112,26 @@
<item name="iconSizeOverride">@dimen/deep_shortcut_icon_size</item>
</style>
<style name="Icon.DeepNotification">
<item name="android:gravity">start</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:elevation">@dimen/deep_shortcuts_elevation</item>
<item name="android:textColor">#FF212121</item>
<item name="android:textSize">14sp</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:shadowRadius">0</item>
<item name="customShadows">false</item>
<item name="layoutHorizontal">true</item>
<item name="iconDisplay">shortcut_popup</item>
<item name="iconSizeOverride">@dimen/deep_shortcut_icon_size</item>
</style>
<style name="Icon.DeepNotification.SubText">
<item name="android:textColor">#FF757575</item>
<item name="android:textSize">12sp</item>
<item name="android:paddingEnd">4dp</item>
</style>
<!-- Drop targets -->
<style name="DropTargetButtonBase">
<item name="android:drawablePadding">7.5dp</item>

View File

@ -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) &&

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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()]);
}

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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);
}
}