Merge changes from topic "inline-reply-animation" into tm-qpr-dev am: 02358a560e

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20462188

Change-Id: Iba4b8ab0ca578aba91f38eb32fd80fd59cd51981
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Johannes Gallmann
2022-12-20 08:59:31 +00:00
committed by Automerger Merge Worker
11 changed files with 311 additions and 23 deletions

View File

@@ -27,6 +27,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="end" android:gravity="end"
android:layout_gravity="bottom"
android:orientation="horizontal" android:orientation="horizontal"
android:background="@color/notification_action_list_background_color" android:background="@color/notification_action_list_background_color"
> >

View File

@@ -221,6 +221,7 @@ android_library {
"WindowManager-Shell", "WindowManager-Shell",
"LowLightDreamLib", "LowLightDreamLib",
"motion_tool_lib", "motion_tool_lib",
"androidx.core_core-animation-testing-nodeps",
], ],
} }

View File

@@ -20,6 +20,7 @@
<com.android.systemui.statusbar.policy.RemoteInputView <com.android.systemui.statusbar.policy.RemoteInputView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/remote_input" android:id="@+id/remote_input"
android:forceHasOverlappingRendering="false"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent"> android:layout_width="match_parent">
<LinearLayout <LinearLayout

View File

@@ -103,6 +103,11 @@ object Flags {
// TODO(b/257315550): Tracking Bug // TODO(b/257315550): Tracking Bug
val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when", teamfood = true) val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when", teamfood = true)
// 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 = val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true) unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)

View File

@@ -463,7 +463,11 @@ public class NotificationRemoteInputManager implements Dumpable {
riv.getController().setRemoteInput(input); riv.getController().setRemoteInput(input);
riv.getController().setRemoteInputs(inputs); riv.getController().setRemoteInputs(inputs);
riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); 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) { if (userMessageContent != null) {
riv.setEditTextContent(userMessageContent); riv.setEditTextContent(userMessageContent);
} }

View File

@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.collection.inflation; package com.android.systemui.statusbar.notification.collection.inflation;
import static com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -30,6 +31,7 @@ import android.view.ViewGroup;
import com.android.internal.util.NotificationMessagingUtil; import com.android.internal.util.NotificationMessagingUtil;
import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -71,6 +73,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
private NotificationListContainer mListContainer; private NotificationListContainer mListContainer;
private BindRowCallback mBindRowCallback; private BindRowCallback mBindRowCallback;
private NotificationClicker mNotificationClicker; private NotificationClicker mNotificationClicker;
private FeatureFlags mFeatureFlags;
@Inject @Inject
public NotificationRowBinderImpl( public NotificationRowBinderImpl(
@@ -82,7 +85,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
RowContentBindStage rowContentBindStage, RowContentBindStage rowContentBindStage,
Provider<RowInflaterTask> rowInflaterTaskProvider, Provider<RowInflaterTask> rowInflaterTaskProvider,
ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder, ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder,
IconManager iconManager) { IconManager iconManager,
FeatureFlags featureFlags) {
mContext = context; mContext = context;
mNotifBindPipeline = notifBindPipeline; mNotifBindPipeline = notifBindPipeline;
mRowContentBindStage = rowContentBindStage; mRowContentBindStage = rowContentBindStage;
@@ -92,6 +96,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
mRowInflaterTaskProvider = rowInflaterTaskProvider; mRowInflaterTaskProvider = rowInflaterTaskProvider;
mExpandableNotificationRowComponentBuilder = expandableNotificationRowComponentBuilder; mExpandableNotificationRowComponentBuilder = expandableNotificationRowComponentBuilder;
mIconManager = iconManager; mIconManager = iconManager;
mFeatureFlags = featureFlags;
} }
/** /**
@@ -176,6 +181,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
entry.setRow(row); entry.setRow(row);
mNotifBindPipeline.manageRow(entry, row); mNotifBindPipeline.manageRow(entry, row);
mBindRowCallback.onBindRow(row); mBindRowCallback.onBindRow(row);
row.setInlineReplyAnimationFlagEnabled(
mFeatureFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION));
} }
/** /**

View File

@@ -284,6 +284,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private View.OnClickListener mOnAppClickListener; private View.OnClickListener mOnAppClickListener;
private View.OnClickListener mOnFeedbackClickListener; private View.OnClickListener mOnFeedbackClickListener;
private Path mExpandingClipPath; private Path mExpandingClipPath;
private boolean mIsInlineReplyAnimationFlagEnabled = false;
// Listener will be called when receiving a long click event. // Listener will be called when receiving a long click event.
// Use #setLongPressPosition to optionally assign positional data with the long press. // Use #setLongPressPosition to optionally assign positional data with the long press.
@@ -3079,6 +3080,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return 0; return 0;
} }
public void setInlineReplyAnimationFlagEnabled(boolean isEnabled) {
mIsInlineReplyAnimationFlagEnabled = isEnabled;
}
@Override @Override
public void setActualHeight(int height, boolean notifyListeners) { public void setActualHeight(int height, boolean notifyListeners) {
boolean changed = height != getActualHeight(); boolean changed = height != getActualHeight();
@@ -3098,7 +3103,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
} }
int contentHeight = Math.max(getMinHeight(), height); int contentHeight = Math.max(getMinHeight(), height);
for (NotificationContentView l : mLayouts) { for (NotificationContentView l : mLayouts) {
l.setContentHeight(contentHeight); if (mIsInlineReplyAnimationFlagEnabled) {
l.setContentHeight(height);
} else {
l.setContentHeight(contentHeight);
}
} }
if (mIsSummaryWithChildren) { if (mIsSummaryWithChildren) {
mChildrenContainer.setActualHeight(height); mChildrenContainer.setActualHeight(height);

View File

@@ -70,7 +70,7 @@ import java.util.List;
/** /**
* A frame layout containing the actual payload of the notification, including the contracted, * 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. * switching between the expanded, contracted and the heads up view depending on its clipped size.
*/ */
public class NotificationContentView extends FrameLayout implements NotificationFadeAware { public class NotificationContentView extends FrameLayout implements NotificationFadeAware {
@@ -627,6 +627,13 @@ public class NotificationContentView extends FrameLayout implements Notification
int hint; int hint;
if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) { if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
hint = getViewHeight(VISIBLE_TYPE_HEADSUP); hint = getViewHeight(VISIBLE_TYPE_HEADSUP);
if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance()
&& mHeadsUpRemoteInputController.isFocusAnimationFlagActive()) {
// While the RemoteInputView is animating its appearance, it should be allowed
// to overlap the hint, therefore no space is reserved for the hint during the
// appearance animation of the RemoteInputView
hint = 0;
}
} else if (mExpandedChild != null) { } else if (mExpandedChild != null) {
hint = getViewHeight(VISIBLE_TYPE_EXPANDED); hint = getViewHeight(VISIBLE_TYPE_EXPANDED);
} else if (mContractedChild != null) { } else if (mContractedChild != null) {

View File

@@ -18,8 +18,8 @@ package com.android.systemui.statusbar.policy;
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
import android.animation.Animator; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
import android.animation.AnimatorListenerAdapter;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
@@ -57,6 +57,7 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@@ -67,6 +68,11 @@ import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils; import com.android.internal.graphics.ColorUtils;
@@ -74,6 +80,7 @@ import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.animation.InterpolatorsAndroidX;
import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -97,6 +104,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
// A marker object that let's us easily find views of this class. // A marker object that let's us easily find views of this class.
public static final Object VIEW_TAG = new Object(); 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(); public final Object mToken = new Object();
private final SendButtonTextWatcher mTextWatcher; private final SendButtonTextWatcher mTextWatcher;
@@ -108,6 +121,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
private RemoteEditText mEditText; private RemoteEditText mEditText;
private ImageButton mSendButton; private ImageButton mSendButton;
private LinearLayout mContentView;
private GradientDrawable mContentBackground; private GradientDrawable mContentBackground;
private ProgressBar mProgressBar; private ProgressBar mProgressBar;
private ImageView mDelete; private ImageView mDelete;
@@ -115,7 +129,10 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
private boolean mColorized; private boolean mColorized;
private int mTint; private int mTint;
private boolean mResetting; 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 // TODO(b/193539698): move these to a Controller
private RemoteInputController mController; private RemoteInputController mController;
@@ -125,6 +142,10 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
private boolean mSending; private boolean mSending;
private NotificationViewWrapper mWrapper; private NotificationViewWrapper mWrapper;
private Integer mDefocusTargetHeight = null;
private boolean mIsAnimatingAppearance = false;
// TODO(b/193539698): remove this; views shouldn't have access to their controller, and places // 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 // that need the controller shouldn't have access to the view
private RemoteInputViewController mViewController; private RemoteInputViewController mViewController;
@@ -255,8 +276,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN); mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN);
mDelete.setImageTintBlendMode(BlendMode.SRC_IN); mDelete.setImageTintBlendMode(BlendMode.SRC_IN);
mDelete.setOnClickListener(v -> setAttachment(null)); mDelete.setOnClickListener(v -> setAttachment(null));
LinearLayout contentView = findViewById(R.id.remote_input_content); mContentView = findViewById(R.id.remote_input_content);
contentView.setBackground(mContentBackground); mContentView.setBackground(mContentBackground);
mEditText = findViewById(R.id.remote_input_text); mEditText = findViewById(R.id.remote_input_text);
mEditText.setInnerFocusable(false); mEditText.setInnerFocusable(false);
// TextView initializes the spell checked when the view is attached to a window. // TextView initializes the spell checked when the view is attached to a window.
@@ -398,20 +419,74 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
return true; return true;
} }
private void onDefocus(boolean animate, boolean logClose) { public boolean isAnimatingAppearance() {
return mIsAnimatingAppearance;
}
/**
* 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); mController.removeRemoteInput(mEntry, mToken);
mEntry.remoteInputText = mEditText.getText(); mEntry.remoteInputText = mEditText.getText();
// During removal, we get reattached and lose focus. Not hiding in that // During removal, we get reattached and lose focus. Not hiding in that
// case to prevent flicker. // case to prevent flicker.
if (!mRemoved) { if (!mRemoved) {
if (animate && mRevealParams != null && mRevealParams.radius > 0) { if (animate && mIsFocusAnimationFlagActive) {
Animator reveal = mRevealParams.createCircularHideAnimator(this); Animator animator = getDefocusAnimator();
reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT); // When defocusing, the notification needs to shrink. Therefore, we need to free
reveal.addListener(new AnimatorListenerAdapter() { // 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 @Override
public void onAnimationEnd(Animator animation) { 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); setVisibility(GONE);
if (mWrapper != null) { if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false); mWrapper.setRemoteInputVisible(false);
@@ -533,12 +608,37 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
mEditText.setText(editTextContent); mEditText.setText(editTextContent);
} }
public void focusAnimated() { /**
if (getVisibility() != VISIBLE && mRevealParams != null) { * Sets whether the feature flag for the updated inline reply animation is active or not.
Animator animator = mRevealParams.createCircularRevealAnimator(this); * @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.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
animator.start(); animator.start();
} else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
mIsAnimatingAppearance = true;
setAlpha(0f);
Animator focusAnimator = getFocusAnimator(crossFadeView);
focusAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
mIsAnimatingAppearance = false;
}
});
focusAnimator.start();
} }
focus(); focus();
} }
@@ -737,6 +837,81 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
mOnSendListeners.remove(listener); 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. */ /** Handler for button click on send action in IME. */
private class EditorActionHandler implements TextView.OnEditorActionListener { private class EditorActionHandler implements TextView.OnEditorActionListener {
@@ -991,11 +1166,11 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
this.radius = radius; this.radius = radius;
} }
Animator createCircularHideAnimator(View view) { android.animation.Animator createCircularHideAnimator(View view) {
return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, radius, 0); 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); return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, 0, radius);
} }
} }

View File

@@ -30,6 +30,8 @@ import android.util.Log
import android.view.View import android.view.View
import com.android.internal.logging.UiEventLogger import com.android.internal.logging.UiEventLogger
import com.android.systemui.R 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.NotificationRemoteInputManager
import com.android.systemui.statusbar.RemoteInputController import com.android.systemui.statusbar.RemoteInputController
import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -61,6 +63,8 @@ interface RemoteInputViewController {
var revealParams: RevealParams? var revealParams: RevealParams?
val isFocusAnimationFlagActive: Boolean
/** /**
* Sets the smart reply that should be inserted in the remote input, or `null` if the user is * Sets the smart reply that should be inserted in the remote input, or `null` if the user is
* not editing a smart reply. * not editing a smart reply.
@@ -117,7 +121,8 @@ class RemoteInputViewControllerImpl @Inject constructor(
private val remoteInputQuickSettingsDisabler: RemoteInputQuickSettingsDisabler, private val remoteInputQuickSettingsDisabler: RemoteInputQuickSettingsDisabler,
private val remoteInputController: RemoteInputController, private val remoteInputController: RemoteInputController,
private val shortcutManager: ShortcutManager, private val shortcutManager: ShortcutManager,
private val uiEventLogger: UiEventLogger private val uiEventLogger: UiEventLogger,
private val mFlags: FeatureFlags
) : RemoteInputViewController { ) : RemoteInputViewController {
private val onSendListeners = ArraySet<OnSendRemoteInputListener>() private val onSendListeners = ArraySet<OnSendRemoteInputListener>()
@@ -149,6 +154,9 @@ class RemoteInputViewControllerImpl @Inject constructor(
override val isActive: Boolean get() = view.isActive override val isActive: Boolean get() = view.isActive
override val isFocusAnimationFlagActive: Boolean
get() = mFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION)
override fun bind() { override fun bind() {
if (isBound) return if (isBound) return
isBound = true isBound = true
@@ -159,6 +167,7 @@ class RemoteInputViewControllerImpl @Inject constructor(
view.setSupportedMimeTypes(it.allowedDataTypes) view.setSupportedMimeTypes(it.allowedDataTypes)
} }
view.setRevealParameters(revealParams) view.setRevealParameters(revealParams)
view.setIsFocusAnimationFlagActive(isFocusAnimationFlagActive)
view.addOnEditTextFocusChangedListener(onFocusChangeListener) view.addOnEditTextFocusChangedListener(onFocusChangeListener)
view.addOnSendRemoteInputListener(onSendRemoteInputListener) view.addOnSendRemoteInputListener(onSendRemoteInputListener)

View File

@@ -16,10 +16,14 @@ package com.android.systemui.statusbar.policy;
import static android.view.ContentInfo.SOURCE_CLIPBOARD; 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 com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
@@ -57,6 +61,7 @@ import android.window.OnBackInvokedDispatcher;
import android.window.WindowOnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.animation.AnimatorTestRule;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger;
@@ -64,15 +69,19 @@ import com.android.internal.logging.testing.UiEventLoggerFake;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; 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.NotificationRemoteInputManager;
import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.LightBarController;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -99,6 +108,9 @@ public class RemoteInputViewTest extends SysuiTestCase {
private BlockingQueueIntentReceiver mReceiver; private BlockingQueueIntentReceiver mReceiver;
private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
@ClassRule
public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
allowTestableLooperAsMainThread(); allowTestableLooperAsMainThread();
@@ -294,6 +306,9 @@ public class RemoteInputViewTest extends SysuiTestCase {
/* invoke the captured callback */ /* invoke the captured callback */
onBackInvokedCallbackCaptor.getValue().onBackInvoked(); onBackInvokedCallbackCaptor.getValue().onBackInvoked();
/* wait for RemoteInputView disappear animation to finish */
mAnimatorTestRule.advanceTimeBy(StackStateAnimator.ANIMATION_DURATION_STANDARD);
/* verify that the RemoteInputView goes away */ /* verify that the RemoteInputView goes away */
assertEquals(view.getVisibility(), View.GONE); assertEquals(view.getVisibility(), View.GONE);
} }
@@ -363,19 +378,73 @@ public class RemoteInputViewTest extends SysuiTestCase {
mUiEventLoggerFake.eventId(1)); 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);
assertTrue(view.isAnimatingAppearance());
// 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());
assertFalse(view.isAnimatingAppearance());
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 // 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 @NonNull
private RemoteInputViewController bindController( private RemoteInputViewController bindController(
RemoteInputView view, RemoteInputView view,
NotificationEntry entry) { NotificationEntry entry) {
FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
fakeFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl( RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl(
view, view,
entry, entry,
mRemoteInputQuickSettingsDisabler, mRemoteInputQuickSettingsDisabler,
mController, mController,
mShortcutManager, mShortcutManager,
mUiEventLoggerFake); mUiEventLoggerFake,
fakeFeatureFlags
);
viewController.bind(); viewController.bind();
return viewController; return viewController;
} }