Commit 11a2d832 authored by Sinan Sahin's avatar Sinan Sahin Committed by Commit Bot

[Messages] Update show/hide animations with specs

This CL also integrates the animations with the swipe gestures.

Bug: 1137941
Change-Id: I48ace538ebf345c4c78fc58b1de154422a416dcb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2510993Reviewed-by: default avatarLijin Shen <lazzzis@google.com>
Reviewed-by: default avatarPavel Yatsuk <pavely@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Commit-Queue: Sinan Sahin <sinansahin@google.com>
Cr-Commit-Position: refs/heads/master@{#825144}
parent d3f380f1
......@@ -39,11 +39,8 @@ public class MessageContainerCoordinator implements BrowserControlsStateProvider
private void updateMargins() {
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) mContainer.getLayoutParams();
Resources res = mContainer.getResources();
// TODO(crbug.com/1123947): Update dimens for PWAs.
params.topMargin = mControlsManager.getTopControlsHeight()
- res.getDimensionPixelOffset(R.dimen.message_bubble_inset)
- res.getDimensionPixelOffset(R.dimen.message_shadow_top_margin);
params.topMargin = getContainerTopOffset();
mContainer.setLayoutParams(params);
}
......@@ -56,6 +53,17 @@ public class MessageContainerCoordinator implements BrowserControlsStateProvider
mContainer.setVisibility(View.GONE);
}
/**
* @return The maximum translation Y value the message banner can have as a result of the
* animations or the gestures. Positive values mean the message banner can be translated
* upward from the top of the MessagesContainer.
*/
public int getMessageMaxTranslation() {
// TODO(sinansahin): We need to account for other scenarios where there are no browser
// controls visible (e.g. PWAs).
return getContainerTopOffset();
}
@Override
public void onControlsOffsetChanged(int topOffset, int topControlsMinHeightOffset,
int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {
......@@ -66,4 +74,12 @@ public class MessageContainerCoordinator implements BrowserControlsStateProvider
public void onTopControlsHeightChanged(int topControlsHeight, int topControlsMinHeight) {
updateMargins();
}
/** @return Offset of the message container from the top of the screen. */
private int getContainerTopOffset() {
final Resources res = mContainer.getResources();
return mControlsManager.getTopControlsHeight()
- res.getDimensionPixelOffset(R.dimen.message_bubble_inset)
- res.getDimensionPixelOffset(R.dimen.message_shadow_top_margin);
}
}
......@@ -392,7 +392,8 @@ public class RootUiCoordinator
MessageContainer container = mActivity.findViewById(R.id.message_container);
mMessageContainerCoordinator =
new MessageContainerCoordinator(container, getBrowserControlsManager());
mMessageDispatcher = MessagesFactory.createMessageDispatcher(container);
mMessageDispatcher = MessagesFactory.createMessageDispatcher(
container, mMessageContainerCoordinator::getMessageMaxTranslation);
mMessageQueueMediator = new ChromeMessageQueueMediator(
mActivity.getBrowserControlsManager(), mMessageContainerCoordinator,
mActivity.getFullscreenManager(), mMessageDispatcher);
......
......@@ -4,6 +4,7 @@
package org.chromium.components.messages;
import org.chromium.base.supplier.Supplier;
import org.chromium.ui.modelutil.PropertyModel;
/**
......@@ -13,19 +14,24 @@ import org.chromium.ui.modelutil.PropertyModel;
public class MessageDispatcherImpl implements ManagedMessageDispatcher {
private final MessageQueueManager mMessageQueueManager = new MessageQueueManager();
private final MessageContainer mMessageContainer;
private final Supplier<Integer> mMessageMaxTranslationSupplier;
/**
* Build a new message dispatcher
* @param messageContainer A container view for displaying message banners.
* @param messageMaxTranslationSupplier A {@link Supplier} that supplies the maximum translation
* Y value the message banner can have as a result of the animations or the gestures.
*/
public MessageDispatcherImpl(MessageContainer messageContainer) {
public MessageDispatcherImpl(
MessageContainer messageContainer, Supplier<Integer> messageMaxTranslation) {
mMessageContainer = messageContainer;
mMessageMaxTranslationSupplier = messageMaxTranslation;
}
@Override
public void enqueueMessage(PropertyModel messageProperties) {
MessageStateHandler messageStateHandler =
new SingleActionMessage(mMessageContainer, messageProperties, this::dismissMessage);
MessageStateHandler messageStateHandler = new SingleActionMessage(mMessageContainer,
messageProperties, this::dismissMessage, mMessageMaxTranslationSupplier);
mMessageQueueManager.enqueueMessage(messageStateHandler, messageProperties);
}
......
......@@ -4,6 +4,7 @@
package org.chromium.components.messages;
import org.chromium.base.supplier.Supplier;
import org.chromium.ui.base.WindowAndroid;
/** A factory for constructing different Messages related objects. */
......@@ -11,10 +12,16 @@ public class MessagesFactory {
/**
* Creates an instance of ManagedMessageDispatcher.
* @param container The MessageContainer for displaying message banners.
* @param messageMaxTranslation A {@link Supplier} that supplies the maximum translation Y value
* the message banner can have as a result of the animations or the gestures, relative
* to the MessageContainer. When messages are shown, they will be animated down the
* screen, starting at the negative |messageMaxTranslation| y translation to the resting
* position in the MessageContainer.
* @return The constructed ManagedMessageDispatcher.
*/
public static ManagedMessageDispatcher createMessageDispatcher(MessageContainer container) {
return new MessageDispatcherImpl(container);
public static ManagedMessageDispatcher createMessageDispatcher(
MessageContainer container, Supplier<Integer> messageMaxTranslation) {
return new MessageDispatcherImpl(container, messageMaxTranslation);
}
/**
......
......@@ -81,6 +81,9 @@
android:layout_weight="0"
android:gravity="center_vertical"
android:minWidth="@dimen/message_banner_button_min_width"
android:minHeight="@dimen/min_touch_target_size"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/selectableItemBackground" />
</org.chromium.components.messages.MessageBannerView>
\ No newline at end of file
......@@ -18,5 +18,6 @@
<dimen name="message_shadow_lateral_margin">12dp</dimen>
<dimen name="message_shadow_bottom_margin">16dp</dimen>
<dimen name="message_bubble_inset">8dp</dimen>
<dimen name="message_hide_threshold">16dp</dimen>
</resources>
\ No newline at end of file
......@@ -4,8 +4,9 @@
package org.chromium.components.messages;
import android.content.Context;
import android.content.res.Resources;
import org.chromium.base.supplier.Supplier;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
......@@ -20,11 +21,15 @@ class MessageBannerCoordinator {
*
* @param view The inflated {@link MessageBannerView}.
* @param model The model for the message banner.
* @param context The context used to get dimen resources.
* @param maxTranslationSupplier A {@link Supplier} that supplies the maximum translation Y
* value the message banner can have as a result of the animations or the gestures.
* @param resources The {@link Resources}.
*/
MessageBannerCoordinator(MessageBannerView view, PropertyModel model, Context context) {
MessageBannerCoordinator(MessageBannerView view, PropertyModel model,
Supplier<Integer> maxTranslationSupplier, Resources resources) {
PropertyModelChangeProcessor.create(model, view, MessageBannerViewBinder::bind);
mMediator = new MessageBannerMediator(model, context);
mMediator = new MessageBannerMediator(model, maxTranslationSupplier, resources);
view.setSwipeHandler(mMediator);
}
/**
......
......@@ -9,31 +9,47 @@ import static org.chromium.components.messages.MessageBannerProperties.TRANSLATI
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.content.Context;
import android.animation.TimeInterpolator;
import android.content.res.Resources;
import android.view.MotionEvent;
import org.chromium.base.MathUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.components.browser_ui.widget.animation.CancelAwareAnimatorListener;
import org.chromium.components.browser_ui.widget.animation.Interpolators;
import org.chromium.ui.base.ViewUtils;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.ScrollDirection;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelAnimatorFactory;
/**
* Mediator responsible for the business logic in a message banner.
*/
class MessageBannerMediator {
private static final int ANIMATION_DURATION_MS = 100;
private static final float ANIMATION_OFFSET_DP = 50.f;
class MessageBannerMediator implements SwipeHandler {
private static final int ANIMATION_DURATION_MS = 400;
private static final TimeInterpolator TRANSLATION_SHOW_INTERPOLATOR =
Interpolators.FAST_OUT_LINEAR_IN_INTERPOLATOR;
private static final TimeInterpolator TRANSLATION_HIDE_INTERPOLATOR =
Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR;
private static final TimeInterpolator ALPHA_INTERPOLATOR = Interpolators.LINEAR_INTERPOLATOR;
private PropertyModel mModel;
private AnimatorSet mAnimatorSet;
private Context mContext;
private Supplier<Integer> mMaxTranslationSupplier;
private final float mHideThresholdPx;
private float mSwipeStartTranslationY;
/**
* Constructs the message banner mediator.
*/
MessageBannerMediator(PropertyModel model, Context context) {
MessageBannerMediator(
PropertyModel model, Supplier<Integer> maxTranslationSupplier, Resources resources) {
mModel = model;
mContext = context;
mMaxTranslationSupplier = maxTranslationSupplier;
mModel.set(TRANSLATION_Y, -mMaxTranslationSupplier.get());
mHideThresholdPx = resources.getDimensionPixelSize(R.dimen.message_hide_threshold);
}
/**
......@@ -41,9 +57,7 @@ class MessageBannerMediator {
* @param messageShown The {@link Runnable} that will run once the message banner is shown.
*/
void show(Runnable messageShown) {
if (mAnimatorSet != null && mAnimatorSet.isStarted()) {
mAnimatorSet.cancel();
}
cancelAnyAnimations();
mAnimatorSet = createAnimatorSet(true);
mAnimatorSet.addListener(new CancelAwareAnimatorListener() {
@Override
......@@ -60,9 +74,7 @@ class MessageBannerMediator {
* @param messageHidden The {@link Runnable} that will run once the message banner is hidden.
*/
void hide(Runnable messageHidden) {
if (mAnimatorSet != null && mAnimatorSet.isStarted()) {
mAnimatorSet.cancel();
}
cancelAnyAnimations();
mAnimatorSet = createAnimatorSet(false);
mAnimatorSet.addListener(new CancelAwareAnimatorListener() {
@Override
......@@ -78,21 +90,74 @@ class MessageBannerMediator {
mModel.set(MessageBannerProperties.ON_TOUCH_RUNNABLE, runnable);
}
// region SwipeHandler implementation
// ---------------------------------------------------------------------------------------------
@Override
public void onSwipeStarted(@ScrollDirection int direction, MotionEvent ev) {
mSwipeStartTranslationY = mModel.get(TRANSLATION_Y);
}
@Override
public void onSwipeUpdated(
MotionEvent current, float tx, float ty, float distanceX, float distanceY) {
final float currentGesturePositionY = mSwipeStartTranslationY + ty;
final float currentTranslationY =
MathUtils.clamp(currentGesturePositionY, -mMaxTranslationSupplier.get(), 0);
mModel.set(TRANSLATION_Y, currentTranslationY);
}
// TODO(sinansahin): See if we need special handling for #onFling.
@Override
public void onSwipeFinished(MotionEvent end) {
cancelAnyAnimations();
// No need to animate if the message banner is in resting position.
if (mModel.get(TRANSLATION_Y) == 0.f) return;
mAnimatorSet = createAnimatorSet(mModel.get(TRANSLATION_Y) > -mHideThresholdPx);
mAnimatorSet.addListener(new CancelAwareAnimatorListener() {
@Override
public void onEnd(Animator animator) {
// TODO(sinansahin): We need a way to notify someone to dismiss the message once
// it's hidden.
mAnimatorSet = null;
}
});
mAnimatorSet.start();
}
@Override
public boolean isSwipeEnabled(@ScrollDirection int direction) {
// TODO(sinansahin): We will implement swiping left/right to dismiss.
return (direction == ScrollDirection.UP || direction == ScrollDirection.DOWN)
&& (mAnimatorSet == null || !mAnimatorSet.isRunning());
}
// ---------------------------------------------------------------------------------------------
// endregion
private AnimatorSet createAnimatorSet(boolean isShow) {
final float alphaTo = isShow ? 1.f : 0.f;
final Animator alphaAnimation =
PropertyModelAnimatorFactory.ofFloat(mModel, ALPHA, alphaTo);
alphaAnimation.setInterpolator(ALPHA_INTERPOLATOR);
final float animationOffsetPx = ViewUtils.dpToPx(mContext, ANIMATION_OFFSET_DP);
final float translateTo = isShow ? 0.f : -animationOffsetPx;
final float translateTo = isShow ? 0.f : -mMaxTranslationSupplier.get();
final Animator translationAnimation =
PropertyModelAnimatorFactory.ofFloat(mModel, TRANSLATION_Y, translateTo);
translationAnimation.setInterpolator(
isShow ? TRANSLATION_SHOW_INTERPOLATOR : TRANSLATION_HIDE_INTERPOLATOR);
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(alphaAnimation).with(translationAnimation);
animatorSet.setDuration(ANIMATION_DURATION_MS);
animatorSet.setInterpolator(Interpolators.FAST_OUT_SLOW_IN_INTERPOLATOR);
return animatorSet;
}
private void cancelAnyAnimations() {
if (mAnimatorSet != null && mAnimatorSet.isStarted()) mAnimatorSet.cancel();
mAnimatorSet = null;
}
}
......@@ -4,6 +4,7 @@
package org.chromium.components.messages;
import static org.chromium.components.messages.MessageBannerProperties.ALPHA;
import static org.chromium.components.messages.MessageBannerProperties.DESCRIPTION;
import static org.chromium.components.messages.MessageBannerProperties.ICON;
import static org.chromium.components.messages.MessageBannerProperties.ICON_RESOURCE_ID;
......@@ -13,6 +14,7 @@ import static org.chromium.components.messages.MessageBannerProperties.PRIMARY_B
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON;
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON_CONTENT_DESCRIPTION;
import static org.chromium.components.messages.MessageBannerProperties.TITLE;
import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_Y;
import android.annotation.SuppressLint;
......@@ -54,6 +56,10 @@ public class MessageBannerViewBinder {
return false;
});
}
} else if (propertyKey == ALPHA) {
view.setAlpha(model.get(ALPHA));
} else if (propertyKey == TRANSLATION_Y) {
view.setTranslationY(model.get(TRANSLATION_Y));
}
}
}
......@@ -12,6 +12,8 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.ui.base.ViewUtils;
/**
* Container holding messages.
*/
......@@ -31,6 +33,8 @@ public class MessageContainer extends FrameLayout {
"Should not contain any view when adding a new message.");
}
addView(view);
// TODO(sinansahin): clipChildren should be set to false only when the message is in motion.
ViewUtils.setAncestorsShouldClipChildren(this, false);
}
/**
......@@ -41,6 +45,7 @@ public class MessageContainer extends FrameLayout {
if (indexOfChild(view) < 0) {
throw new IllegalStateException("The given view is not being shown.");
}
ViewUtils.setAncestorsShouldClipChildren(this, true);
removeAllViews();
}
}
......@@ -11,6 +11,7 @@ import android.view.LayoutInflater;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.ui.modelutil.PropertyModel;
/**
......@@ -23,6 +24,7 @@ public class SingleActionMessage implements MessageStateHandler {
private final PropertyModel mModel;
private final Callback<PropertyModel> mDismissHandler;
private MessageAutoDismissTimer mAutoDismissTimer;
private final Supplier<Integer> mMaxTranslationSupplier;
/**
* @param container The container holding messages.
......@@ -30,13 +32,16 @@ public class SingleActionMessage implements MessageStateHandler {
* MessageBannerProperties#SINGLE_ACTION_MESSAGE_KEYS}.
* @param dismissHandler The {@link Callback<PropertyModel>} able to dismiss a message by given
* property model.
* @param maxTranslationSupplier A {@link Supplier} that supplies the maximum translation Y
* value the message banner can have as a result of the animations or the gestures.
*/
public SingleActionMessage(MessageContainer container, PropertyModel model,
Callback<PropertyModel> dismissHandler) {
Callback<PropertyModel> dismissHandler, Supplier<Integer> maxTranslationSupplier) {
mModel = model;
mContainer = container;
mDismissHandler = dismissHandler;
mAutoDismissTimer = new MessageAutoDismissTimer(10 * DateUtils.SECOND_IN_MILLIS);
mMaxTranslationSupplier = maxTranslationSupplier;
}
/**
......@@ -48,7 +53,8 @@ public class SingleActionMessage implements MessageStateHandler {
if (mMessageBanner == null) {
mView = (MessageBannerView) LayoutInflater.from(mContainer.getContext())
.inflate(R.layout.message_banner_view, mContainer, false);
mMessageBanner = new MessageBannerCoordinator(mView, mModel, mContainer.getContext());
mMessageBanner = new MessageBannerCoordinator(
mView, mModel, mMaxTranslationSupplier, mContainer.getResources());
}
mContainer.addMessage(mView);
mMessageBanner.show(() -> {
......
......@@ -50,7 +50,7 @@ public class SingleActionMessageTest extends DummyUiActivityTestCase {
MessageContainer container = new MessageContainer(getActivity(), null);
PropertyModel model = createBasicSingleActionMessageModel();
SingleActionMessage message =
new SingleActionMessage(container, model, mEmptyDismissCallback);
new SingleActionMessage(container, model, mEmptyDismissCallback, () -> 0);
final MessageBannerCoordinator messageBanner = Mockito.mock(MessageBannerCoordinator.class);
doNothing().when(messageBanner).show(any(Runnable.class));
doNothing().when(messageBanner).setOnTouchRunnable(any(Runnable.class));
......@@ -81,7 +81,7 @@ public class SingleActionMessageTest extends DummyUiActivityTestCase {
PropertyModel m1 = createBasicSingleActionMessageModel();
PropertyModel m2 = createBasicSingleActionMessageModel();
SingleActionMessage message1 =
new SingleActionMessage(container, m1, mEmptyDismissCallback);
new SingleActionMessage(container, m1, mEmptyDismissCallback, () -> 0);
final MessageBannerCoordinator messageBanner1 =
Mockito.mock(MessageBannerCoordinator.class);
doNothing().when(messageBanner1).show(any(Runnable.class));
......@@ -89,7 +89,7 @@ public class SingleActionMessageTest extends DummyUiActivityTestCase {
message1.setMessageBannerForTesting(messageBanner1);
message1.setViewForTesting(view1);
SingleActionMessage message2 =
new SingleActionMessage(container, m2, mEmptyDismissCallback);
new SingleActionMessage(container, m2, mEmptyDismissCallback, () -> 0);
final MessageBannerCoordinator messageBanner2 =
Mockito.mock(MessageBannerCoordinator.class);
doNothing().when(messageBanner2).show(any(Runnable.class));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment