From 8b4929dbbffe1110f29f55ce81b95d74416016cf Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Tue, 2 Jun 2020 13:38:04 -0400 Subject: [PATCH] Add importance ring animation on convo priority change Bug: 157480039 Test: manual Change-Id: I0c3b2a857c871fc344705f98aa8463e7de0c4b6b --- .../internal/widget/ConversationLayout.java | 79 +++++++++++++++++-- .../conversation_badge_background.xml | 4 +- .../res/drawable/conversation_badge_ring.xml | 15 ++-- ...ication_template_material_conversation.xml | 13 ++- core/res/res/values/dimens.xml | 6 ++ core/res/res/values/symbols.xml | 6 ++ .../notification/ConversationNotifications.kt | 62 ++++++++++----- .../row/NotificationConversationInfo.java | 9 +-- .../NotificationHeaderViewWrapper.java | 5 +- 9 files changed, 156 insertions(+), 43 deletions(-) diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 1f2ae5f96449f..b7cdeadd482b3 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -21,6 +21,10 @@ import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_ import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_IN; import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; import android.annotation.AttrRes; import android.annotation.NonNull; import android.annotation.Nullable; @@ -36,6 +40,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.Parcelable; @@ -93,8 +98,12 @@ public class ConversationLayout extends FrameLayout public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + public static final Interpolator OVERSHOOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f); public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR = new MessagingPropertyAnimator(); + public static final int IMPORTANCE_ANIM_GROW_DURATION = 250; + public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200; + public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25; private List mMessages = new ArrayList<>(); private List mHistoricMessages = new ArrayList<>(); private MessagingLinearLayout mMessagingLinearLayout; @@ -331,14 +340,74 @@ public class ConversationLayout extends FrameLayout mNameReplacement = nameReplacement; } - /** - * Sets this conversation as "important", adding some additional UI treatment. - */ + /** Sets this conversation as "important", adding some additional UI treatment. */ @RemotableViewMethod public void setIsImportantConversation(boolean isImportantConversation) { + setIsImportantConversation(isImportantConversation, false); + } + + /** @hide **/ + public void setIsImportantConversation(boolean isImportantConversation, boolean animate) { mImportantConversation = isImportantConversation; - mImportanceRingView.setVisibility(isImportantConversation - && mIcon.getVisibility() != GONE ? VISIBLE : GONE); + mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE + ? VISIBLE : GONE); + + if (animate && isImportantConversation) { + GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable(); + ring.mutate(); + GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable(); + bg.mutate(); + int ringColor = getResources() + .getColor(R.color.conversation_important_highlight); + int standardThickness = getResources() + .getDimensionPixelSize(R.dimen.importance_ring_stroke_width); + int largeThickness = getResources() + .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width); + int standardSize = getResources().getDimensionPixelSize( + R.dimen.importance_ring_size); + int baseSize = standardSize - standardThickness * 2; + int bgSize = getResources() + .getDimensionPixelSize(R.dimen.conversation_icon_size_badged); + + ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> { + int strokeWidth = Math.round((float) animation.getAnimatedValue()); + ring.setStroke(strokeWidth, ringColor); + int newSize = baseSize + strokeWidth * 2; + ring.setSize(newSize, newSize); + mImportanceRingView.invalidate(); + }; + + ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness); + growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN); + growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION); + growAnimation.addUpdateListener(animatorUpdateListener); + + ValueAnimator shrinkAnimation = + ValueAnimator.ofFloat(largeThickness, standardThickness); + shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION); + shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY); + shrinkAnimation.setInterpolator(OVERSHOOT); + shrinkAnimation.addUpdateListener(animatorUpdateListener); + shrinkAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Shrink the badge bg so that it doesn't peek behind the animation + bg.setSize(baseSize, baseSize); + mConversationIconBadgeBg.invalidate(); + } + + @Override + public void onAnimationEnd(Animator animation) { + // Reset bg back to normal size + bg.setSize(bgSize, bgSize); + mConversationIconBadgeBg.invalidate(); + } + }); + + AnimatorSet anims = new AnimatorSet(); + anims.playSequentially(growAnimation, shrinkAnimation); + anims.start(); + } } public boolean isImportantConversation() { diff --git a/core/res/res/drawable/conversation_badge_background.xml b/core/res/res/drawable/conversation_badge_background.xml index 0dd0dcda40fb8..9e6405dc10401 100644 --- a/core/res/res/drawable/conversation_badge_background.xml +++ b/core/res/res/drawable/conversation_badge_background.xml @@ -22,7 +22,7 @@ android:color="#ffffff"/> + android:width="20dp" + android:height="20dp"/> diff --git a/core/res/res/drawable/conversation_badge_ring.xml b/core/res/res/drawable/conversation_badge_ring.xml index 11ba8ad69505e..eee53d1c21b5d 100644 --- a/core/res/res/drawable/conversation_badge_ring.xml +++ b/core/res/res/drawable/conversation_badge_ring.xml @@ -16,17 +16,18 @@ --> - - + android:shape="oval" +> + + android:width="@dimen/importance_ring_stroke_width" + /> + android:width="@dimen/importance_ring_size" + android:height="@dimen/importance_ring_size" + /> diff --git a/core/res/res/layout/notification_template_material_conversation.xml b/core/res/res/layout/notification_template_material_conversation.xml index 9a9d8b96c677d..0411f55e60061 100644 --- a/core/res/res/layout/notification_template_material_conversation.xml +++ b/core/res/res/layout/notification_template_material_conversation.xml @@ -38,6 +38,8 @@ @@ -63,13 +65,17 @@ android:layout_height="@dimen/conversation_icon_size_badged" android:layout_marginLeft="@dimen/conversation_badge_side_margin" android:layout_marginTop="@dimen/conversation_badge_side_margin" + android:clipChildren="false" + android:clipToPadding="false" > diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 59bb052cbdf5b..a771904f115b5 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -720,6 +720,12 @@ 1dp 14dp + + 2dp + + 10dp + + 20dp 9dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ab4005b4893cb..23ae1e7d271ed 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3903,6 +3903,12 @@ + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index fc6c2be1ce9ae..1972b869ba25e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -19,13 +19,16 @@ package com.android.systemui.statusbar.notification import android.app.Notification import android.content.Context import android.content.pm.LauncherApps +import android.os.Handler import android.service.notification.NotificationListenerService.Ranking import android.service.notification.NotificationListenerService.RankingMap import com.android.internal.statusbar.NotificationVisibility import com.android.internal.widget.ConversationLayout +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationContentView +import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.NotificationGroupManager import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -62,7 +65,8 @@ class ConversationNotificationProcessor @Inject constructor( class ConversationNotificationManager @Inject constructor( private val notificationEntryManager: NotificationEntryManager, private val notificationGroupManager: NotificationGroupManager, - private val context: Context + private val context: Context, + @Main private val mainHandler: Handler ) { // Need this state to be thread safe, since it's accessed from the ui thread // (NotificationEntryListener) and a bg thread (NotificationContentInflater) @@ -72,32 +76,41 @@ class ConversationNotificationManager @Inject constructor( init { notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener { - override fun onNotificationRankingUpdated(rankingMap: RankingMap) { fun getLayouts(view: NotificationContentView) = sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild) val ranking = Ranking() - states.keys.asSequence() + val activeConversationEntries = states.keys.asSequence() .mapNotNull { notificationEntryManager.getActiveNotificationUnfiltered(it) } - .forEach { entry -> - if (rankingMap.getRanking(entry.sbn.key, ranking) && - ranking.isConversation) { - val important = ranking.channel.isImportantConversation - var changed = false - entry.row?.layouts?.asSequence() - ?.flatMap(::getLayouts) - ?.mapNotNull { it as? ConversationLayout } - ?.forEach { - if (important != it.isImportantConversation) { - it.setIsImportantConversation(important) - changed = true - } - } - if (changed) { - notificationGroupManager.updateIsolation(entry) - } + for (entry in activeConversationEntries) { + if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) { + val important = ranking.channel.isImportantConversation + val layouts = entry.row?.layouts?.asSequence() + ?.flatMap(::getLayouts) + ?.mapNotNull { it as? ConversationLayout } + ?: emptySequence() + var changed = false + for (layout in layouts) { + if (important == layout.isImportantConversation) { + continue + } + changed = true + if (important && entry.isMarkedForUserTriggeredMovement) { + // delay this so that it doesn't animate in until after + // the notif has been moved in the shade + mainHandler.postDelayed({ + layout.setIsImportantConversation( + important, true /* animate */) + }, IMPORTANCE_ANIMATION_DELAY.toLong()) + } else { + layout.setIsImportantConversation(important) } } + if (changed) { + notificationGroupManager.updateIsolation(entry) + } + } + } } override fun onEntryInflated(entry: NotificationEntry) { @@ -177,9 +190,16 @@ class ConversationNotificationManager @Inject constructor( private fun resetBadgeUi(row: ExpandableNotificationRow): Unit = (row.layouts?.asSequence() ?: emptySequence()) - .flatMap { layout -> layout.allViews.asSequence()} + .flatMap { layout -> layout.allViews.asSequence() } .mapNotNull { view -> view as? ConversationLayout } .forEach { convoLayout -> convoLayout.setUnreadCount(0) } private data class ConversationState(val unreadCount: Int, val notification: Notification) + + companion object { + private const val IMPORTANCE_ANIMATION_DELAY = + StackStateAnimator.ANIMATION_DURATION_STANDARD + + StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE + + 100 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index e9d89589172e8..f4afb91396b58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -53,7 +53,6 @@ import android.transition.TransitionManager; import android.transition.TransitionSet; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -508,10 +507,10 @@ public class NotificationConversationInfo extends LinearLayout implements mBgHandler.post( new UpdateChannelRunnable(mINotificationManager, mPackageName, mAppUid, mSelectedAction, mNotificationChannel)); - mMainHandler.postDelayed(() -> { - mEntry.markForUserTriggeredMovement(true); - mVisualStabilityManager.temporarilyAllowReordering(); - }, StackStateAnimator.ANIMATION_DURATION_STANDARD); + mEntry.markForUserTriggeredMovement(true); + mMainHandler.postDelayed( + mVisualStabilityManager::temporarilyAllowReordering, + StackStateAnimator.ANIMATION_DURATION_STANDARD); } private boolean shouldShowPriorityOnboarding() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 4c9cb209424a4..c747a7c300b7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -262,7 +262,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { stack.push(mView); while (!stack.isEmpty()) { View child = stack.pop(); - if (child instanceof ImageView) { + if (child instanceof ImageView + // Skip the importance ring for conversations, disabled cropping is needed for + // its animation + && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) { ((ImageView) child).setCropToPadding(true); } else if (child instanceof ViewGroup){ ViewGroup group = (ViewGroup) child;