summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest-common.xml2
-rw-r--r--res/drawable/bg_white_pill_bottom.xml22
-rw-r--r--res/drawable/bg_white_pill_top.xml22
-rw-r--r--res/layout/notification.xml61
-rw-r--r--res/layout/notification_footer.xml42
-rw-r--r--res/layout/notification_main.xml54
-rw-r--r--res/values/config.xml3
-rw-r--r--res/values/dimens.xml15
-rw-r--r--res/values/strings.xml7
-rw-r--r--res/values/styles.xml20
-rw-r--r--src/com/android/launcher3/BubbleTextView.java6
-rw-r--r--src/com/android/launcher3/FastBitmapDrawable.java13
-rw-r--r--src/com/android/launcher3/Launcher.java2
-rw-r--r--src/com/android/launcher3/LauncherAnimUtils.java16
-rw-r--r--src/com/android/launcher3/badge/BadgeInfo.java16
-rw-r--r--src/com/android/launcher3/graphics/IconPalette.java4
-rw-r--r--src/com/android/launcher3/notification/FlingAnimationUtils.java356
-rw-r--r--src/com/android/launcher3/notification/Interpolators.java37
-rw-r--r--src/com/android/launcher3/notification/NotificationFooterLayout.java242
-rw-r--r--src/com/android/launcher3/notification/NotificationInfo.java (renamed from src/com/android/launcher3/badge/NotificationInfo.java)28
-rw-r--r--src/com/android/launcher3/notification/NotificationItemView.java175
-rw-r--r--src/com/android/launcher3/notification/NotificationListener.java (renamed from src/com/android/launcher3/badge/NotificationListener.java)3
-rw-r--r--src/com/android/launcher3/notification/NotificationMainView.java156
-rw-r--r--src/com/android/launcher3/notification/SwipeHelper.java690
-rw-r--r--src/com/android/launcher3/popup/PopupContainerWithArrow.java120
-rw-r--r--src/com/android/launcher3/popup/PopupDataProvider.java22
-rw-r--r--src/com/android/launcher3/popup/PopupItemView.java8
-rw-r--r--src/com/android/launcher3/popup/PopupPopulator.java47
-rw-r--r--src/com/android/launcher3/util/PillRevealOutlineProvider.java9
29 files changed, 2145 insertions, 53 deletions
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index d6c95dbb0..f3b7827ba 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -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" />
diff --git a/res/drawable/bg_white_pill_bottom.xml b/res/drawable/bg_white_pill_bottom.xml
new file mode 100644
index 000000000..a1ea48cec
--- /dev/null
+++ b/res/drawable/bg_white_pill_bottom.xml
@@ -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> \ No newline at end of file
diff --git a/res/drawable/bg_white_pill_top.xml b/res/drawable/bg_white_pill_top.xml
new file mode 100644
index 000000000..9988b2913
--- /dev/null
+++ b/res/drawable/bg_white_pill_top.xml
@@ -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> \ No newline at end of file
diff --git a/res/layout/notification.xml b/res/layout/notification.xml
new file mode 100644
index 000000000..d828c4a36
--- /dev/null
+++ b/res/layout/notification.xml
@@ -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>
diff --git a/res/layout/notification_footer.xml b/res/layout/notification_footer.xml
new file mode 100644
index 000000000..ceea24a66
--- /dev/null
+++ b/res/layout/notification_footer.xml
@@ -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>
+
diff --git a/res/layout/notification_main.xml b/res/layout/notification_main.xml
new file mode 100644
index 000000000..efb74b0f7
--- /dev/null
+++ b/res/layout/notification_main.xml
@@ -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>
+
diff --git a/res/values/config.xml b/res/values/config.xml
index d270def78..cb813d59b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -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" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3a2eea61b..2cf17ead7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -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>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f1de62378..a6f44f637 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -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 -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4e70f43d0..8b4a1dba4 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -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>
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..77355c75e 100644
--- a/src/com/android/launcher3/badge/BadgeInfo.java
+++ b/src/com/android/launcher3/badge/BadgeInfo.java
@@ -16,10 +16,11 @@
package com.android.launcher3.badge;
+import com.android.launcher3.notification.NotificationInfo;
import com.android.launcher3.util.PackageUserKey;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.List;
/**
* Contains data to be used in an icon badge.
@@ -32,17 +33,20 @@ public class BadgeInfo {
* The keys of the notifications that this badge represents. These keys can later be
* used to retrieve {@link NotificationInfo}'s.
*/
- private Set<String> mNotificationKeys;
+ private List<String> mNotificationKeys;
public BadgeInfo(PackageUserKey packageUserKey) {
mPackageUserKey = packageUserKey;
- mNotificationKeys = new HashSet<>();
+ mNotificationKeys = new ArrayList<>();
}
/**
* Returns whether the notification was added (false if it already existed).
*/
- public boolean addNotificationKey(String notificationKey) {
+ public boolean addNotificationKeyIfNotExists(String notificationKey) {
+ if (mNotificationKeys.contains(notificationKey)) {
+ return false;
+ }
return mNotificationKeys.add(notificationKey);
}
@@ -53,7 +57,7 @@ public class BadgeInfo {
return mNotificationKeys.remove(notificationKey);
}
- public Set<String> getNotificationKeys() {
+ public List<String> getNotificationKeys() {
return mNotificationKeys;
}
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..a1f7e49c0
--- /dev/null
+++ b/src/com/android/launcher3/notification/FlingAnimationUtils.java
@@ -0,0 +1,356 @@
+/*
+ * 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.
+ *
+ * This class was copied from com.android.systemui.statusbar.
+ */
+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..5c3b22ab1
--- /dev/null
+++ b/src/com/android/launcher3/notification/Interpolators.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ *
+ * This class was copied from com.android.systemui.
+ */
+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..cd610bd3b
--- /dev/null
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -0,0 +1,242 @@
+/*
+ * 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;
+
+/**
+ * A {@link LinearLayout} that contains only icons of notifications.
+ * 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(List<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..bf57b2aff 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,30 @@ 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);
+ // Load the icon. Since it is backed by ashmem, we won't copy the entire bitmap
+ // into our process as long as we don't touch it and it exists in systemui.
+ 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..b74cd4e1e
--- /dev/null
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -0,0 +1,175 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+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 List<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);
+ // Remove the animated notification from the footer by calling trim
+ // TODO: Remove the notification in NotificationFooterLayout directly
+ // instead of relying on this hack.
+ List<String> footerNotificationKeys = new ArrayList<>(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..5f03252cd
--- /dev/null
+++ b/src/com/android/launcher3/notification/SwipeHelper.java
@@ -0,0 +1,690 @@
+/*
+ * 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;
+
+/**
+ * This class was copied from com.android.systemui.
+ */
+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..c773079fe 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;
@@ -33,7 +33,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* Provides data for the popup menu that appears after long-clicking on apps.
@@ -59,10 +58,10 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(postedPackageUserKey);
if (oldBadgeInfo == null) {
BadgeInfo newBadgeInfo = new BadgeInfo(postedPackageUserKey);
- newBadgeInfo.addNotificationKey(notificationKey);
+ newBadgeInfo.addNotificationKeyIfNotExists(notificationKey);
mPackageUserToBadgeInfos.put(postedPackageUserKey, newBadgeInfo);
mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
- } else if (oldBadgeInfo.addNotificationKey(notificationKey)) {
+ } else if (oldBadgeInfo.addNotificationKeyIfNotExists(notificationKey)) {
mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
}
}
@@ -75,6 +74,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);
+ }
}
}
@@ -91,7 +95,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
badgeInfo = new BadgeInfo(packageUserKey);
mPackageUserToBadgeInfos.put(packageUserKey, badgeInfo);
}
- badgeInfo.addNotificationKey(notification.getKey());
+ badgeInfo.addNotificationKeyIfNotExists(notification.getKey());
}
// Add and remove from updatedBadges so it contains the PackageUserKeys of updated badges.
@@ -110,6 +114,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,7 +149,8 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
public String[] getNotificationKeysForItem(ItemInfo info) {
BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
- Set<String> notificationKeys = badgeInfo.getNotificationKeys();
+ if (badgeInfo == null) { return new String[0]; }
+ List<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);
}
}