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