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">
|
||||
</service>
|
||||
|
||||
<service android:name="com.android.launcher3.badging.NotificationListener"
|
||||
<service android:name="com.android.launcher3.notification.NotificationListener"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
|
|
|
@ -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 -->
|
||||
<item type="id" name="preview_image_id" />
|
||||
|
||||
<!-- Deep shortcuts -->
|
||||
<!-- Popup items -->
|
||||
<integer name="config_deepShortcutOpenDuration">220</integer>
|
||||
<integer name="config_deepShortcutArrowOpenDuration">80</integer>
|
||||
<integer name="config_deepShortcutOpenStagger">40</integer>
|
||||
<integer name="config_deepShortcutCloseDuration">150</integer>
|
||||
<integer name="config_deepShortcutCloseStagger">20</integer>
|
||||
<integer name="config_removeNotificationViewDuration">300</integer>
|
||||
|
||||
<!-- Accessibility actions -->
|
||||
<item type="id" name="action_remove" />
|
||||
|
|
|
@ -172,9 +172,22 @@
|
|||
<!-- Icon badges (with notification counts) -->
|
||||
<dimen name="badge_size">24dp</dimen>
|
||||
<dimen name="badge_text_size">12dp</dimen>
|
||||
<dimen name="notification_icon_size">28dp</dimen>
|
||||
<dimen name="notification_footer_icon_size">24dp</dimen>
|
||||
<!-- (icon_size - secondary_icon_size) / 2 -->
|
||||
|
||||
<!-- Notifications -->
|
||||
<dimen name="notification_footer_icon_row_padding">2dp</dimen>
|
||||
<dimen name="notification_icon_margin_start">8dp</dimen>
|
||||
<dimen name="notification_text_margin_start">8dp</dimen>
|
||||
<dimen name="notification_footer_height">36dp</dimen>
|
||||
<!-- The height to use when there are no icons in the footer -->
|
||||
<dimen name="notification_footer_collapsed_height">@dimen/bg_pill_radius</dimen>
|
||||
<dimen name="notification_elevation">2dp</dimen>
|
||||
<dimen name="notification_divider_height">0.5dp</dimen>
|
||||
<dimen name="swipe_helper_falsing_threshold">70dp</dimen>
|
||||
|
||||
<!-- Other -->
|
||||
<!-- Approximates the system status bar height. Not guaranteed to be always be correct. -->
|
||||
<dimen name="status_bar_height">24dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -67,6 +67,13 @@
|
|||
<!-- Label for the button which allows the user to get app search results. [CHAR_LIMIT=50] -->
|
||||
<string name="all_apps_search_market_message">Search for more apps</string>
|
||||
|
||||
<!-- Deep items -->
|
||||
<!-- Text to indicate more items that couldn't be displayed due to space constraints.
|
||||
The text must fit in the size of a small icon [CHAR_LIMIT=3] -->
|
||||
<string name="deep_notifications_overflow" translatable="false">+%1$d</string>
|
||||
<!-- Text to display as the header above notifications. [CHAR_LIMIT=30] -->
|
||||
<string name="notifications_header" translatable="false">Notifications</string>
|
||||
|
||||
<!-- Drag and drop -->
|
||||
<skip />
|
||||
<!-- Error message when user has filled a home screen -->
|
||||
|
|
|
@ -112,6 +112,26 @@
|
|||
<item name="iconSizeOverride">@dimen/deep_shortcut_icon_size</item>
|
||||
</style>
|
||||
|
||||
<style name="Icon.DeepNotification">
|
||||
<item name="android:gravity">start</item>
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
<item name="android:elevation">@dimen/deep_shortcuts_elevation</item>
|
||||
<item name="android:textColor">#FF212121</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:shadowRadius">0</item>
|
||||
<item name="customShadows">false</item>
|
||||
<item name="layoutHorizontal">true</item>
|
||||
<item name="iconDisplay">shortcut_popup</item>
|
||||
<item name="iconSizeOverride">@dimen/deep_shortcut_icon_size</item>
|
||||
</style>
|
||||
|
||||
<style name="Icon.DeepNotification.SubText">
|
||||
<item name="android:textColor">#FF757575</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:paddingEnd">4dp</item>
|
||||
</style>
|
||||
|
||||
<!-- Drop targets -->
|
||||
<style name="DropTargetButtonBase">
|
||||
<item name="android:drawablePadding">7.5dp</item>
|
||||
|
|
|
@ -44,6 +44,7 @@ import com.android.launcher3.badge.BadgeRenderer;
|
|||
import com.android.launcher3.folder.FolderIcon;
|
||||
import com.android.launcher3.graphics.DrawableFactory;
|
||||
import com.android.launcher3.graphics.HolographicOutlineHelper;
|
||||
import com.android.launcher3.graphics.IconPalette;
|
||||
import com.android.launcher3.model.PackageItemInfo;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
@ -514,6 +515,11 @@ public class BubbleTextView extends TextView
|
|||
}
|
||||
}
|
||||
|
||||
public IconPalette getIconPalette() {
|
||||
return mIcon instanceof FastBitmapDrawable ? ((FastBitmapDrawable) mIcon).getIconPalette()
|
||||
: null;
|
||||
}
|
||||
|
||||
private Theme getPreloaderTheme() {
|
||||
Object tag = getTag();
|
||||
int style = ((tag != null) && (tag instanceof ShortcutInfo) &&
|
||||
|
|
|
@ -129,10 +129,7 @@ public class FastBitmapDrawable extends Drawable {
|
|||
mBadgeInfo = badgeInfo;
|
||||
mBadgeRenderer = badgeRenderer;
|
||||
if (wasBadged || isBadged) {
|
||||
if (mBadgeInfo != null && mIconPalette == null) {
|
||||
mIconPalette = IconPalette.fromDominantColor(Utilities
|
||||
.findDominantColorByHue(mBitmap, 20));
|
||||
}
|
||||
mIconPalette = getIconPalette();
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
@ -161,6 +158,14 @@ public class FastBitmapDrawable extends Drawable {
|
|||
}
|
||||
}
|
||||
|
||||
public IconPalette getIconPalette() {
|
||||
if (mIconPalette == null) {
|
||||
mIconPalette = IconPalette.fromDominantColor(Utilities
|
||||
.findDominantColorByHue(mBitmap, 20));
|
||||
}
|
||||
return mIconPalette;
|
||||
}
|
||||
|
||||
private boolean hasBadge() {
|
||||
return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ import com.android.launcher3.allapps.AllAppsContainerView;
|
|||
import com.android.launcher3.allapps.AllAppsTransitionController;
|
||||
import com.android.launcher3.allapps.DefaultAppSearchController;
|
||||
import com.android.launcher3.anim.AnimationLayerSet;
|
||||
import com.android.launcher3.badge.NotificationListener;
|
||||
import com.android.launcher3.notification.NotificationListener;
|
||||
import com.android.launcher3.popup.PopupDataProvider;
|
||||
import com.android.launcher3.compat.AppWidgetManagerCompat;
|
||||
import com.android.launcher3.compat.LauncherAppsCompat;
|
||||
|
|
|
@ -23,7 +23,9 @@ import android.animation.PropertyValuesHolder;
|
|||
import android.animation.ValueAnimator;
|
||||
import android.util.Property;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.WeakHashMap;
|
||||
|
@ -127,4 +129,18 @@ public class LauncherAnimUtils {
|
|||
new FirstFrameAnimatorHelper(anim, view);
|
||||
return anim;
|
||||
}
|
||||
|
||||
public static ValueAnimator animateViewHeight(final View v, int fromHeight, int toHeight) {
|
||||
ValueAnimator anim = ValueAnimator.ofInt(fromHeight, toHeight);
|
||||
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
int val = (Integer) valueAnimator.getAnimatedValue();
|
||||
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
|
||||
layoutParams.height = val;
|
||||
v.setLayoutParams(layoutParams);
|
||||
}
|
||||
});
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.android.launcher3.badge;
|
||||
|
||||
import com.android.launcher3.notification.NotificationInfo;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
|
|
@ -26,16 +26,18 @@ public class IconPalette {
|
|||
|
||||
public int backgroundColor;
|
||||
public int textColor;
|
||||
public int secondaryColor;
|
||||
|
||||
public static IconPalette fromDominantColor(int dominantColor) {
|
||||
IconPalette palette = new IconPalette();
|
||||
palette.backgroundColor = getMutedColor(dominantColor);
|
||||
palette.textColor = getTextColorForBackground(palette.backgroundColor);
|
||||
palette.secondaryColor = getLowContrastColor(palette.backgroundColor);
|
||||
return palette;
|
||||
}
|
||||
|
||||
private static int getMutedColor(int color) {
|
||||
int alpha = (int) (255 * 0.2f);
|
||||
int alpha = (int) (255 * 0.15f);
|
||||
return ColorUtils.compositeColors(ColorUtils.setAlphaComponent(color, alpha), Color.WHITE);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.badge;
|
||||
package com.android.launcher3.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
|
@ -44,26 +44,28 @@ public class NotificationInfo implements View.OnClickListener {
|
|||
public final Drawable iconDrawable;
|
||||
public final PendingIntent intent;
|
||||
public final boolean autoCancel;
|
||||
public final boolean dismissable;
|
||||
|
||||
/**
|
||||
* Extracts the data that we need from the StatusBarNotification.
|
||||
*/
|
||||
public NotificationInfo(Context context, StatusBarNotification notification) {
|
||||
packageUserKey = PackageUserKey.fromNotification(notification);
|
||||
notificationKey = notification.getKey();
|
||||
title = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TITLE);
|
||||
text = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TEXT);
|
||||
Icon icon = notification.getNotification().getLargeIcon();
|
||||
public NotificationInfo(Context context, StatusBarNotification statusBarNotification) {
|
||||
packageUserKey = PackageUserKey.fromNotification(statusBarNotification);
|
||||
notificationKey = statusBarNotification.getKey();
|
||||
Notification notification = statusBarNotification.getNotification();
|
||||
title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
|
||||
text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
|
||||
Icon icon = notification.getLargeIcon();
|
||||
if (icon == null) {
|
||||
icon = notification.getNotification().getSmallIcon();
|
||||
icon = notification.getSmallIcon();
|
||||
iconDrawable = icon.loadDrawable(context);
|
||||
iconDrawable.setTint(notification.getNotification().color);
|
||||
iconDrawable.setTint(statusBarNotification.getNotification().color);
|
||||
} else {
|
||||
iconDrawable = icon.loadDrawable(context);
|
||||
}
|
||||
intent = notification.getNotification().contentIntent;
|
||||
autoCancel = (notification.getNotification().flags
|
||||
& Notification.FLAG_AUTO_CANCEL) != 0;
|
||||
intent = notification.contentIntent;
|
||||
autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0;
|
||||
dismissable = (notification.flags & Notification.FLAG_ONGOING_EVENT) == 0;
|
||||
}
|
||||
|
||||
@Override
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.badge;
|
||||
package com.android.launcher3.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.os.Handler;
|
||||
|
@ -24,6 +24,7 @@ import android.service.notification.NotificationListenerService;
|
|||
import android.service.notification.StatusBarNotification;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.launcher3.LauncherModel;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
|
@ -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.AnimatorSet;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Point;
|
||||
|
@ -33,6 +33,7 @@ import android.graphics.drawable.ShapeDrawable;
|
|||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -65,15 +66,18 @@ import com.android.launcher3.dragndrop.DragOptions;
|
|||
import com.android.launcher3.dragndrop.DragView;
|
||||
import com.android.launcher3.graphics.IconPalette;
|
||||
import com.android.launcher3.graphics.TriangleShape;
|
||||
import com.android.launcher3.notification.NotificationItemView;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutView;
|
||||
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.*;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
|
||||
|
||||
/**
|
||||
* A container for shortcuts to deep links within apps.
|
||||
|
@ -137,19 +141,22 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
}
|
||||
ItemInfo itemInfo = (ItemInfo) icon.getTag();
|
||||
List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
|
||||
if (shortcutIds.size() > 0) {
|
||||
String[] notificationKeys = launcher.getPopupDataProvider()
|
||||
.getNotificationKeysForItem(itemInfo);
|
||||
if (shortcutIds.size() > 0 || notificationKeys.length > 0) {
|
||||
final PopupContainerWithArrow container =
|
||||
(PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
|
||||
R.layout.popup_container, launcher.getDragLayer(), false);
|
||||
container.setVisibility(View.INVISIBLE);
|
||||
launcher.getDragLayer().addView(container);
|
||||
container.populateAndShow(icon, shortcutIds);
|
||||
container.populateAndShow(icon, shortcutIds, notificationKeys);
|
||||
return container;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds) {
|
||||
public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
|
||||
final String[] notificationKeys) {
|
||||
final Resources resources = getResources();
|
||||
final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
|
||||
final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
|
||||
|
@ -159,8 +166,9 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
R.dimen.deep_shortcuts_arrow_vertical_offset);
|
||||
|
||||
// Add dummy views first, and populate with real info when ready.
|
||||
PopupPopulator.Item[] itemsToPopulate = PopupPopulator.getItemsToPopulate(shortcutIds);
|
||||
addDummyViews(originalIcon, itemsToPopulate);
|
||||
PopupPopulator.Item[] itemsToPopulate = PopupPopulator
|
||||
.getItemsToPopulate(shortcutIds, notificationKeys);
|
||||
addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
|
||||
|
||||
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
|
||||
orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
|
||||
|
@ -169,13 +177,14 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
if (reverseOrder) {
|
||||
removeAllViews();
|
||||
itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
|
||||
addDummyViews(originalIcon, itemsToPopulate);
|
||||
addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
|
||||
|
||||
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
|
||||
orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
|
||||
}
|
||||
|
||||
List<DeepShortcutView> shortcutViews = new ArrayList<>();
|
||||
NotificationItemView notificationView = null;
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
View item = getChildAt(i);
|
||||
switch (itemsToPopulate[i]) {
|
||||
|
@ -186,6 +195,11 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
shortcutViews.add((DeepShortcutView) item);
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION:
|
||||
notificationView = (NotificationItemView) item;
|
||||
IconPalette iconPalette = originalIcon.getIconPalette();
|
||||
notificationView.applyColors(iconPalette);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,6 +207,8 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
|
||||
mArrow.setPivotX(arrowWidth / 2);
|
||||
mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
|
||||
PopupItemView firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0);
|
||||
mArrow.setBackgroundTintList(firstItem.getAttachedArrowColor());
|
||||
|
||||
animateOpen();
|
||||
|
||||
|
@ -204,16 +220,24 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
final Looper workerLooper = LauncherModel.getWorkerLooper();
|
||||
new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
|
||||
mLauncher, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()),
|
||||
this, shortcutIds, shortcutViews));
|
||||
this, shortcutIds, shortcutViews, notificationKeys, notificationView));
|
||||
}
|
||||
|
||||
private void addDummyViews(BubbleTextView originalIcon, PopupPopulator.Item[] itemsToPopulate) {
|
||||
final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
|
||||
private void addDummyViews(BubbleTextView originalIcon,
|
||||
PopupPopulator.Item[] itemsToPopulate, boolean secondaryNotificationViewHasIcons) {
|
||||
final Resources res = getResources();
|
||||
final int spacing = res.getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
|
||||
final LayoutInflater inflater = mLauncher.getLayoutInflater();
|
||||
int numItems = itemsToPopulate.length;
|
||||
for (int i = 0; i < numItems; i++) {
|
||||
final PopupItemView item = (PopupItemView) inflater.inflate(
|
||||
itemsToPopulate[i].layoutId, this, false);
|
||||
if (itemsToPopulate[i] == PopupPopulator.Item.NOTIFICATION) {
|
||||
int secondaryHeight = secondaryNotificationViewHasIcons ?
|
||||
res.getDimensionPixelSize(R.dimen.notification_footer_height) :
|
||||
res.getDimensionPixelSize(R.dimen.notification_footer_collapsed_height);
|
||||
item.findViewById(R.id.footer).getLayoutParams().height = secondaryHeight;
|
||||
}
|
||||
if (i < numItems - 1) {
|
||||
((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
|
||||
}
|
||||
|
@ -550,6 +574,78 @@ public class PopupContainerWithArrow extends AbstractFloatingView
|
|||
return false;
|
||||
}
|
||||
|
||||
public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
|
||||
final NotificationItemView notificationView = (NotificationItemView) findViewById(R.id.notification_view);
|
||||
if (notificationView == null) {
|
||||
return;
|
||||
}
|
||||
ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
|
||||
BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
|
||||
if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
|
||||
AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
|
||||
final int duration = getResources().getInteger(
|
||||
R.integer.config_removeNotificationViewDuration);
|
||||
final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
|
||||
removeNotification.play(animateTranslationYBy(notificationView.getHeight() + spacing,
|
||||
duration));
|
||||
Animator reduceHeight = notificationView.createRemovalAnimation(duration);
|
||||
final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
|
||||
: notificationView;
|
||||
if (removeMarginView != null) {
|
||||
ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
|
||||
removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
|
||||
= (int) (spacing * (float) valueAnimator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
removeNotification.play(removeMargin);
|
||||
}
|
||||
removeNotification.play(reduceHeight);
|
||||
Animator fade = new LauncherViewPropertyAnimator(notificationView).alpha(0)
|
||||
.setDuration(duration);
|
||||
fade.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
removeView(notificationView);
|
||||
if (getItemCount() == 0) {
|
||||
close(false);
|
||||
return;
|
||||
}
|
||||
View firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0);
|
||||
mArrow.setBackgroundTintList(firstItem.getBackgroundTintList());
|
||||
}
|
||||
});
|
||||
removeNotification.play(fade);
|
||||
final long arrowScaleDuration = getResources().getInteger(
|
||||
R.integer.config_deepShortcutArrowOpenDuration);
|
||||
Animator hideArrow = new LauncherViewPropertyAnimator(mArrow)
|
||||
.scaleX(0).scaleY(0).setDuration(arrowScaleDuration);
|
||||
hideArrow.setStartDelay(0);
|
||||
Animator showArrow = new LauncherViewPropertyAnimator(mArrow)
|
||||
.scaleX(1).scaleY(1).setDuration(arrowScaleDuration);
|
||||
showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
|
||||
removeNotification.playSequentially(hideArrow, showArrow);
|
||||
removeNotification.start();
|
||||
return;
|
||||
}
|
||||
notificationView.trimNotifications(badgeInfo.getNotificationKeys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the translationY of this container if it is open above the icon.
|
||||
* If it is below the icon, the container already shifts up when the height
|
||||
* of a child (e.g. NotificationView) changes, so the translation isn't necessary.
|
||||
*/
|
||||
public @Nullable Animator animateTranslationYBy(int translationY, int duration) {
|
||||
if (mIsAboveIcon) {
|
||||
return new LauncherViewPropertyAnimator(this)
|
||||
.translationY(getTranslationY() + translationY).setDuration(duration);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppInfoDropTarget() {
|
||||
return true;
|
||||
|
|
|
@ -23,7 +23,7 @@ import android.util.Log;
|
|||
import com.android.launcher3.ItemInfo;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.badge.BadgeInfo;
|
||||
import com.android.launcher3.badge.NotificationListener;
|
||||
import com.android.launcher3.notification.NotificationListener;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutManager;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.MultiHashMap;
|
||||
|
@ -75,6 +75,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
|
|||
mPackageUserToBadgeInfos.remove(removedPackageUserKey);
|
||||
}
|
||||
mLauncher.updateIconBadges(Collections.singleton(removedPackageUserKey));
|
||||
|
||||
PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
|
||||
if (openContainer != null) {
|
||||
openContainer.trimNotifications(mPackageUserToBadgeInfos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +115,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
|
|||
if (!updatedBadges.isEmpty()) {
|
||||
mLauncher.updateIconBadges(updatedBadges.keySet());
|
||||
}
|
||||
|
||||
PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
|
||||
if (openContainer != null) {
|
||||
openContainer.trimNotifications(updatedBadges);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
|
||||
|
@ -140,6 +150,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
|
|||
|
||||
public String[] getNotificationKeysForItem(ItemInfo info) {
|
||||
BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
|
||||
if (badgeInfo == null) { return new String[0]; }
|
||||
Set<String> notificationKeys = badgeInfo.getNotificationKeys();
|
||||
return notificationKeys.toArray(new String[notificationKeys.size()]);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.animation.Animator;
|
|||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
|
@ -72,6 +73,10 @@ public abstract class PopupItemView extends FrameLayout
|
|||
mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
|
||||
}
|
||||
|
||||
protected ColorStateList getAttachedArrowColor() {
|
||||
return getBackgroundTintList();
|
||||
}
|
||||
|
||||
public boolean willDrawIcon() {
|
||||
return true;
|
||||
}
|
||||
|
@ -158,7 +163,8 @@ public abstract class PopupItemView extends FrameLayout
|
|||
|
||||
public ZoomRevealOutlineProvider(int x, int y, Rect pillRect,
|
||||
View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) {
|
||||
super(x, y, pillRect);
|
||||
super(x, y, pillRect, zoomView.getResources().getDimensionPixelSize(
|
||||
R.dimen.bg_pill_radius));
|
||||
mTranslateView = translateView;
|
||||
mZoomView = zoomView;
|
||||
mFullHeight = pillRect.height();
|
||||
|
|
|
@ -19,12 +19,15 @@ package com.android.launcher3.popup;
|
|||
import android.content.ComponentName;
|
||||
import android.os.Handler;
|
||||
import android.os.UserHandle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.launcher3.ItemInfo;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.ShortcutInfo;
|
||||
import com.android.launcher3.notification.NotificationInfo;
|
||||
import com.android.launcher3.notification.NotificationItemView;
|
||||
import com.android.launcher3.graphics.LauncherIcons;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutManager;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutView;
|
||||
|
@ -45,7 +48,8 @@ public class PopupPopulator {
|
|||
@VisibleForTesting static final int NUM_DYNAMIC = 2;
|
||||
|
||||
public enum Item {
|
||||
SHORTCUT(R.layout.deep_shortcut);
|
||||
SHORTCUT(R.layout.deep_shortcut),
|
||||
NOTIFICATION(R.layout.notification);
|
||||
|
||||
public final int layoutId;
|
||||
|
||||
|
@ -54,12 +58,18 @@ public class PopupPopulator {
|
|||
}
|
||||
}
|
||||
|
||||
public static Item[] getItemsToPopulate(List<String> shortcutIds) {
|
||||
int numItems = Math.min(MAX_ITEMS, shortcutIds.size());
|
||||
public static Item[] getItemsToPopulate(List<String> shortcutIds, String[] notificationKeys) {
|
||||
boolean hasNotifications = notificationKeys.length > 0;
|
||||
int numNotificationItems = hasNotifications ? 1 : 0;
|
||||
int numItems = Math.min(MAX_ITEMS, shortcutIds.size() + numNotificationItems);
|
||||
Item[] items = new Item[numItems];
|
||||
for (int i = 0; i < numItems; i++) {
|
||||
items[i] = Item.SHORTCUT;
|
||||
}
|
||||
if (hasNotifications) {
|
||||
// The notification layout is always first.
|
||||
items[0] = Item.NOTIFICATION;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
|
@ -134,12 +144,24 @@ public class PopupPopulator {
|
|||
|
||||
public static Runnable createUpdateRunnable(final Launcher launcher, ItemInfo originalInfo,
|
||||
final Handler uiHandler, final PopupContainerWithArrow container,
|
||||
final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews) {
|
||||
final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews,
|
||||
final String[] notificationKeys, final NotificationItemView notificationView) {
|
||||
final ComponentName activity = originalInfo.getTargetComponent();
|
||||
final UserHandle user = originalInfo.user;
|
||||
return new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (notificationView != null) {
|
||||
List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
|
||||
.getStatusBarNotificationsForKeys(notificationKeys);
|
||||
List<NotificationInfo> infos = new ArrayList<>(notifications.size());
|
||||
for (int i = 0; i < notifications.size(); i++) {
|
||||
StatusBarNotification notification = notifications.get(i);
|
||||
infos.add(new NotificationInfo(launcher, notification));
|
||||
}
|
||||
uiHandler.post(new UpdateNotificationChild(notificationView, infos));
|
||||
}
|
||||
|
||||
final List<ShortcutInfoCompat> shortcuts = PopupPopulator.sortAndFilterShortcuts(
|
||||
DeepShortcutManager.getInstance(launcher).queryForShortcutsContainer(
|
||||
activity, shortcutIds, user));
|
||||
|
@ -176,4 +198,21 @@ public class PopupPopulator {
|
|||
mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the child of this container at the given index based on the given shortcut info. */
|
||||
private static class UpdateNotificationChild implements Runnable {
|
||||
private NotificationItemView mNotificationView;
|
||||
private List<NotificationInfo> mNotificationInfos;
|
||||
|
||||
public UpdateNotificationChild(NotificationItemView notificationView,
|
||||
List<NotificationInfo> notificationInfos) {
|
||||
mNotificationView = notificationView;
|
||||
mNotificationInfos = notificationInfos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mNotificationView.applyNotificationInfos(mNotificationInfos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ public class PillRevealOutlineProvider extends RevealOutlineAnimation {
|
|||
|
||||
private int mCenterX;
|
||||
private int mCenterY;
|
||||
private float mFinalRadius;
|
||||
protected Rect mPillRect;
|
||||
|
||||
/**
|
||||
|
@ -36,10 +37,14 @@ public class PillRevealOutlineProvider extends RevealOutlineAnimation {
|
|||
* @param pillRect round rect that represents the final pill shape
|
||||
*/
|
||||
public PillRevealOutlineProvider(int x, int y, Rect pillRect) {
|
||||
this(x, y, pillRect, pillRect.height() / 2f);
|
||||
}
|
||||
|
||||
public PillRevealOutlineProvider(int x, int y, Rect pillRect, float radius) {
|
||||
mCenterX = x;
|
||||
mCenterY = y;
|
||||
mPillRect = pillRect;
|
||||
mOutlineRadius = pillRect.height() / 2f;
|
||||
mOutlineRadius = mFinalRadius = radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -58,6 +63,6 @@ public class PillRevealOutlineProvider extends RevealOutlineAnimation {
|
|||
mOutline.top = Math.max(mPillRect.top, mCenterY - currentSize);
|
||||
mOutline.right = Math.min(mPillRect.right, mCenterX + currentSize);
|
||||
mOutline.bottom = Math.min(mPillRect.bottom, mCenterY + currentSize);
|
||||
mOutlineRadius = mOutline.height() / 2;
|
||||
mOutlineRadius = Math.min(mFinalRadius, mOutline.height() / 2);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue