Fix notification inline reply animation
This CL fixes the animation for the appearance of the inline reply RemoteInputView, bringing it as close to the Direct spec (https://direct.googleplex.com/#/spec/283080003) as possible. Intentional deviations from the Direct specs are the following:
1. The animation duration is 360 ms instead of 500 ms (in order to be consistent with other notification shade resize animations)
2. The interpolator is FAST_IN_SLOW_OUT, which might differ slightly from the Direct specs "DECELERATE_WITH_EASING" interpolator. (same reason as for 1.)
3. The height of the notification is increased slightly more than in the Direct spec. (This is done in order to keep the paddings and margins as they currently are. It seems to me that if there was more text in the Direct visualization, the top margin of the inline reply EditText would be too small.)
This CL does not fix the following related issues:
1. The collapsed heads up view does not allocate enough space to fit the RemoteInputView. This causes the top part of the notification to get shrunk (Icons jump up by a few pixels), when clicking the "Reply" button. It also affects the beginning of the RemoteInputView expand animation to be partly shifted out of the HeadsUp View.
2. When defocusing the RemoteInputView, the "Reply" button is clipped during the beginning of the animation.
3. When focusing the RemoteInputView, the notification shade is not scrolled correctly. (Keyboard is overlapping RemoteInputView)
4. The expand/collapse animation is broken while the RemoteInputView is visible.
I will address these issues in seperate CLs.
Bug: 174148361
Test: atest RemoteInputViewTest, Manual, i.e. posting various types of Notifications from the Notify2-RVC application with a reply action added to them. Then observing the animations visually and analyzing frames from screen recordings.
Change-Id: I8b2e821b254d6d7fdfbca22426eceafff95d5322
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e624441..25fb24a 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -221,6 +221,7 @@
"WindowManager-Shell",
"LowLightDreamLib",
"motion_tool_lib",
+ "androidx.core_core-animation-testing-nodeps",
],
}
diff --git a/packages/SystemUI/res/layout/remote_input.xml b/packages/SystemUI/res/layout/remote_input.xml
index a5b2f80..f4b0a45 100644
--- a/packages/SystemUI/res/layout/remote_input.xml
+++ b/packages/SystemUI/res/layout/remote_input.xml
@@ -20,6 +20,7 @@
<com.android.systemui.statusbar.policy.RemoteInputView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/remote_input"
+ android:forceHasOverlappingRendering="false"
android:layout_height="match_parent"
android:layout_width="match_parent">
<LinearLayout
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 389639d..f3c0386 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -100,6 +100,11 @@
// TODO(b/257315550): Tracking Bug
val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when")
+ // TODO(b/260335638): Tracking Bug
+ @JvmField
+ val NOTIFICATION_INLINE_REPLY_ANIMATION =
+ unreleasedFlag(174148361, "notification_inline_reply_animation", teamfood = true)
+
val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index f668528..3516037 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -463,7 +463,11 @@
riv.getController().setRemoteInput(input);
riv.getController().setRemoteInputs(inputs);
riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
- riv.focusAnimated();
+ ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
+ if (parent != null) {
+ riv.setDefocusTargetHeight(parent.getHeight());
+ }
+ riv.focusAnimated(parent);
if (userMessageContent != null) {
riv.setEditTextContent(userMessageContent);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 277ad8e..83d6abb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -70,7 +70,7 @@
/**
* A frame layout containing the actual payload of the notification, including the contracted,
- * expanded and heads up layout. This class is responsible for clipping the content and and
+ * expanded and heads up layout. This class is responsible for clipping the content and
* switching between the expanded, contracted and the heads up view depending on its clipped size.
*/
public class NotificationContentView extends FrameLayout implements NotificationFadeAware {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index dd400b3..9f5ad1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -18,8 +18,8 @@
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
+
import android.app.ActivityManager;
import android.app.Notification;
import android.content.Context;
@@ -57,6 +57,7 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
+import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -67,6 +68,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.animation.Animator;
+import androidx.core.animation.AnimatorListenerAdapter;
+import androidx.core.animation.AnimatorSet;
+import androidx.core.animation.ObjectAnimator;
+import androidx.core.animation.ValueAnimator;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
@@ -74,6 +80,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.animation.InterpolatorsAndroidX;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -97,6 +104,12 @@
// A marker object that let's us easily find views of this class.
public static final Object VIEW_TAG = new Object();
+ private static final long FOCUS_ANIMATION_TOTAL_DURATION = ANIMATION_DURATION_STANDARD;
+ private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50;
+ private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
+ private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
+ private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
+
public final Object mToken = new Object();
private final SendButtonTextWatcher mTextWatcher;
@@ -108,6 +121,7 @@
private RemoteEditText mEditText;
private ImageButton mSendButton;
+ private LinearLayout mContentView;
private GradientDrawable mContentBackground;
private ProgressBar mProgressBar;
private ImageView mDelete;
@@ -115,7 +129,10 @@
private boolean mColorized;
private int mTint;
private boolean mResetting;
- @Nullable private RevealParams mRevealParams;
+ @Nullable
+ private RevealParams mRevealParams;
+ private Rect mContentBackgroundBounds;
+ private boolean mIsFocusAnimationFlagActive;
// TODO(b/193539698): move these to a Controller
private RemoteInputController mController;
@@ -125,6 +142,9 @@
private boolean mSending;
private NotificationViewWrapper mWrapper;
+ private Integer mDefocusTargetHeight = null;
+
+
// TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
// that need the controller shouldn't have access to the view
private RemoteInputViewController mViewController;
@@ -255,8 +275,8 @@
mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN);
mDelete.setImageTintBlendMode(BlendMode.SRC_IN);
mDelete.setOnClickListener(v -> setAttachment(null));
- LinearLayout contentView = findViewById(R.id.remote_input_content);
- contentView.setBackground(mContentBackground);
+ mContentView = findViewById(R.id.remote_input_content);
+ mContentView.setBackground(mContentBackground);
mEditText = findViewById(R.id.remote_input_text);
mEditText.setInnerFocusable(false);
// TextView initializes the spell checked when the view is attached to a window.
@@ -398,20 +418,70 @@
return true;
}
- private void onDefocus(boolean animate, boolean logClose) {
+ /**
+ * View will ensure to use at most the provided defocusTargetHeight, when defocusing animated.
+ * This is to ensure that the parent can resize itself to the targetHeight while the defocus
+ * animation of the RemoteInputView is running.
+ *
+ * @param defocusTargetHeight The target height the parent will resize itself to. If null, the
+ * RemoteInputView will not resize itself.
+ */
+ public void setDefocusTargetHeight(Integer defocusTargetHeight) {
+ mDefocusTargetHeight = defocusTargetHeight;
+ }
+
+ @VisibleForTesting
+ void onDefocus(boolean animate, boolean logClose) {
mController.removeRemoteInput(mEntry, mToken);
mEntry.remoteInputText = mEditText.getText();
// During removal, we get reattached and lose focus. Not hiding in that
// case to prevent flicker.
if (!mRemoved) {
- if (animate && mRevealParams != null && mRevealParams.radius > 0) {
- Animator reveal = mRevealParams.createCircularHideAnimator(this);
- reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
- reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
- reveal.addListener(new AnimatorListenerAdapter() {
+ if (animate && mIsFocusAnimationFlagActive) {
+ Animator animator = getDefocusAnimator();
+
+ // When defocusing, the notification needs to shrink. Therefore, we need to free
+ // up the space that is needed for the RemoteInputView. This is done by setting
+ // a negative top margin of the height difference of the RemoteInputView and its
+ // sibling (the actions_container_layout containing the Reply button)
+ if (mDefocusTargetHeight != null && mDefocusTargetHeight < getHeight()
+ && mDefocusTargetHeight >= 0
+ && getLayoutParams() instanceof FrameLayout.LayoutParams) {
+ int heightToShrink = getHeight() - mDefocusTargetHeight;
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) getLayoutParams();
+ layoutParams.topMargin = -heightToShrink;
+ setLayoutParams(layoutParams);
+ ((ViewGroup) getParent().getParent()).setClipChildren(false);
+ }
+
+ animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
+ //reset top margin after the animation
+ if (getLayoutParams() instanceof FrameLayout.LayoutParams) {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) getLayoutParams();
+ layoutParams.topMargin = 0;
+ setLayoutParams(layoutParams);
+ ((ViewGroup) getParent().getParent()).setClipChildren(true);
+ }
+ setVisibility(GONE);
+ if (mWrapper != null) {
+ mWrapper.setRemoteInputVisible(false);
+ }
+ }
+ });
+ animator.start();
+
+ } else if (animate && mRevealParams != null && mRevealParams.radius > 0) {
+ android.animation.Animator reveal = mRevealParams.createCircularHideAnimator(this);
+ reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
+ reveal.addListener(new android.animation.AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(android.animation.Animator animation) {
setVisibility(GONE);
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false);
@@ -533,12 +603,29 @@
mEditText.setText(editTextContent);
}
- public void focusAnimated() {
- if (getVisibility() != VISIBLE && mRevealParams != null) {
- Animator animator = mRevealParams.createCircularRevealAnimator(this);
+ /**
+ * Sets whether the feature flag for the updated inline reply animation is active or not.
+ * @param active
+ */
+ public void setIsFocusAnimationFlagActive(boolean active) {
+ mIsFocusAnimationFlagActive = active;
+ }
+
+ /**
+ * Focuses the RemoteInputView and animates its appearance
+ *
+ * @param crossFadeView view that will be crossfaded during the appearance animation
+ */
+ public void focusAnimated(View crossFadeView) {
+ if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE
+ && mRevealParams != null) {
+ android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this);
animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
animator.start();
+ } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
+ setAlpha(0f);
+ getFocusAnimator(crossFadeView).start();
}
focus();
}
@@ -737,6 +824,81 @@
mOnSendListeners.remove(listener);
}
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (mIsFocusAnimationFlagActive) setPivotY(getMeasuredHeight());
+ if (mContentBackgroundBounds != null) {
+ mContentBackground.setBounds(mContentBackgroundBounds);
+ }
+ }
+
+ private Animator getFocusAnimator(View crossFadeView) {
+ final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
+ alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
+ alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
+ alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+
+ ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f);
+ scaleAnimator.addUpdateListener(valueAnimator -> {
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ });
+ scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
+ scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
+
+ final Animator crossFadeViewAlphaAnimator =
+ ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f);
+ crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+ crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+ alphaAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ crossFadeView.setAlpha(1f);
+ }
+ });
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator);
+ return animatorSet;
+ }
+
+ private Animator getDefocusAnimator() {
+ final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
+ alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+ alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+
+ ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
+ scaleAnimator.addUpdateListener(valueAnimator -> {
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ });
+ scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
+ scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
+ scaleAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ setFocusAnimationScaleY(1f);
+ }
+ });
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(alphaAnimator, scaleAnimator);
+ return animatorSet;
+ }
+
+ /**
+ * Sets affected view properties for a vertical scale animation
+ *
+ * @param scaleY desired vertical view scale
+ */
+ private void setFocusAnimationScaleY(float scaleY) {
+ int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight());
+ mContentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
+ mContentView.getHeight() - verticalBoundOffset);
+ mContentBackground.setBounds(mContentBackgroundBounds);
+ mContentView.setBackground(mContentBackground);
+ setTranslationY(verticalBoundOffset);
+ }
+
/** Handler for button click on send action in IME. */
private class EditorActionHandler implements TextView.OnEditorActionListener {
@@ -991,11 +1153,11 @@
this.radius = radius;
}
- Animator createCircularHideAnimator(View view) {
+ android.animation.Animator createCircularHideAnimator(View view) {
return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, radius, 0);
}
- Animator createCircularRevealAnimator(View view) {
+ android.animation.Animator createCircularRevealAnimator(View view) {
return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, 0, radius);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
index f845101..22b4c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
@@ -30,6 +30,8 @@
import android.view.View
import com.android.internal.logging.UiEventLogger
import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.RemoteInputController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -61,6 +63,8 @@
var revealParams: RevealParams?
+ val isFocusAnimationFlagActive: Boolean
+
/**
* Sets the smart reply that should be inserted in the remote input, or `null` if the user is
* not editing a smart reply.
@@ -117,7 +121,8 @@
private val remoteInputQuickSettingsDisabler: RemoteInputQuickSettingsDisabler,
private val remoteInputController: RemoteInputController,
private val shortcutManager: ShortcutManager,
- private val uiEventLogger: UiEventLogger
+ private val uiEventLogger: UiEventLogger,
+ private val mFlags: FeatureFlags
) : RemoteInputViewController {
private val onSendListeners = ArraySet<OnSendRemoteInputListener>()
@@ -149,6 +154,9 @@
override val isActive: Boolean get() = view.isActive
+ override val isFocusAnimationFlagActive: Boolean
+ get() = mFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION)
+
override fun bind() {
if (isBound) return
isBound = true
@@ -159,6 +167,7 @@
view.setSupportedMimeTypes(it.allowedDataTypes)
}
view.setRevealParameters(revealParams)
+ view.setIsFocusAnimationFlagActive(isFocusAnimationFlagActive)
view.addOnEditTextFocusChangedListener(onFocusChangeListener)
view.addOnSendRemoteInputListener(onSendRemoteInputListener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 915e999..1fe2008 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -16,6 +16,8 @@
import static android.view.ContentInfo.SOURCE_CLIPBOARD;
+import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
+
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
@@ -57,6 +59,7 @@
import android.window.WindowOnBackInvokedDispatcher;
import androidx.annotation.NonNull;
+import androidx.core.animation.AnimatorTestRule;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
@@ -64,15 +67,19 @@
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.LightBarController;
import org.junit.After;
import org.junit.Before;
+import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -99,6 +106,9 @@
private BlockingQueueIntentReceiver mReceiver;
private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
+ @ClassRule
+ public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+
@Before
public void setUp() throws Exception {
allowTestableLooperAsMainThread();
@@ -294,6 +304,9 @@
/* invoke the captured callback */
onBackInvokedCallbackCaptor.getValue().onBackInvoked();
+ /* wait for RemoteInputView disappear animation to finish */
+ mAnimatorTestRule.advanceTimeBy(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+
/* verify that the RemoteInputView goes away */
assertEquals(view.getVisibility(), View.GONE);
}
@@ -363,19 +376,70 @@
mUiEventLoggerFake.eventId(1));
}
+ @Test
+ public void testFocusAnimation() throws Exception {
+ NotificationTestHelper helper = new NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this));
+ ExpandableNotificationRow row = helper.createRow();
+ RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+ bindController(view, row.getEntry());
+ view.setVisibility(View.GONE);
+
+ View crossFadeView = new View(mContext);
+
+ // Start focus animation
+ view.focusAnimated(crossFadeView);
+
+ // fast forward to end of animation
+ mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+
+ // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind
+ // RemoteInputView)
+ assertEquals(1f, crossFadeView.getAlpha());
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(1f, view.getAlpha());
+ }
+
+ @Test
+ public void testDefocusAnimation() throws Exception {
+ NotificationTestHelper helper = new NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this));
+ ExpandableNotificationRow row = helper.createRow();
+ RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+ bindController(view, row.getEntry());
+
+ // Start defocus animation
+ view.onDefocus(true, false);
+ assertEquals(View.VISIBLE, view.getVisibility());
+
+ // fast forward to end of animation
+ mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+
+ // assert that RemoteInputView is no longer visible
+ assertEquals(View.GONE, view.getVisibility());
+ }
+
// NOTE: because we're refactoring the RemoteInputView and moving logic into the
- // RemoteInputViewController, it's easiest to just test the system of the two classes together.
+ // RemoteInputViewController, it's easiest to just test the system of the two classes together.
@NonNull
private RemoteInputViewController bindController(
RemoteInputView view,
NotificationEntry entry) {
+ FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
+ fakeFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl(
view,
entry,
mRemoteInputQuickSettingsDisabler,
mController,
mShortcutManager,
- mUiEventLoggerFake);
+ mUiEventLoggerFake,
+ fakeFeatureFlags
+ );
viewController.bind();
return viewController;
}