Commit 6d9a9e8e authored by Sinan Sahin's avatar Sinan Sahin Committed by Chromium LUCI CQ

[Messages] Add horizontal swipe-to-dismiss gesture

This CL adds support for horizontal swipe-to-dismiss gestures, adds
fling support and updates the existing motion specs. It also adds unit
tests for the mediator.

Flings currently don't affect the speed of the resulting animations as
it turned out to be complicated to implement.

There are several cases that are handled in this CL:
- No fling:
  - If the message is within the dismiss threshold, vertical or
    horizontal, the message is animated back to the idle position, i.e.
    center.
  - If it's outside the threshold, the message is dismissed by being
    animated to out of screen.
- Fling:
  - If the message is flung toward the center from outside the dismiss
    threshold, it is animated to the idle position.
  - If it's flung from within the threshold to the outside, the message
    is dismissed with animation.
  - If it's flung toward out of screen from outside the dismiss
    threshold, the message is dismissed with animation.

Bug: 1157213
Change-Id: I82cfac7111e5ada8687be74a90162784a1e7c992
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2602706
Commit-Queue: Sinan Sahin <sinansahin@google.com>
Reviewed-by: default avatarPavel Yatsuk <pavely@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#843290}
parent 3fce8349
......@@ -54,20 +54,20 @@ public class MessageContainerCoordinator implements BrowserControlsStateProvider
}
/**
* If there are no browser controls visible, the {@link MessageContainer} view should be laid
* out for this method to return a meaningful value.
* The {@link MessageContainer} view should be laid out for this method to return a meaningful
* value.
*
* @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() {
final int containerTopOffset = getContainerTopOffset();
if (containerTopOffset == 0) {
return mContainer.getHeight();
}
return containerTopOffset;
// The max translation is message height + message shadow + controls height (adjusted for
// Message container offsets)
final int messageHeightWithShadow = mContainer.findViewById(R.id.message_banner).getHeight()
+ mContainer.getResources().getDimensionPixelOffset(
R.dimen.message_shadow_top_margin);
return messageHeightWithShadow + getContainerTopOffset();
}
@Override
......
......@@ -7,6 +7,7 @@ package org.chromium.components.browser_ui.widget.animation;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
......@@ -25,4 +26,5 @@ public class Interpolators {
public static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
public static final LinearOutSlowInInterpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR =
new LinearOutSlowInInterpolator();
public static final OvershootInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator();
}
......@@ -139,14 +139,19 @@ java_library("junit") {
testonly = true
sources = [
"java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java",
"java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java",
"java/src/org/chromium/components/messages/MessageWrapperTest.java",
]
deps = [
":java",
"//base:base_java",
"//base:base_java_test_support",
"//base:base_junit_test_support",
"//components/browser_ui/widget/android:java",
"//third_party/android_deps:androidx_test_runner_java",
"//third_party/android_deps:robolectric_all_java",
"//third_party/hamcrest:hamcrest_core_java",
"//third_party/hamcrest:hamcrest_library_java",
"//third_party/junit",
"//third_party/mockito:mockito_java",
"//ui/android:ui_java",
......
......@@ -6,6 +6,7 @@
<org.chromium.components.messages.MessageBannerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/message_banner"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="@dimen/message_banner_height"
......
......@@ -17,7 +17,9 @@
<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>
<dimen name="message_vertical_hide_threshold">16dp</dimen>
<dimen name="message_horizontal_hide_threshold">24dp</dimen>
<dimen name="message_max_horizontal_translation">360dp</dimen>
<dimen name="message_max_width">380dp</dimen>
</resources>
\ No newline at end of file
......@@ -42,6 +42,7 @@ public class MessageBannerProperties {
new WritableObjectPropertyKey<>();
// Following properties should only be accessed by the message banner component.
static final WritableFloatPropertyKey TRANSLATION_X = new WritableFloatPropertyKey();
static final WritableFloatPropertyKey TRANSLATION_Y = new WritableFloatPropertyKey();
static final WritableFloatPropertyKey ALPHA = new WritableFloatPropertyKey();
static final WritableObjectPropertyKey<Runnable> ON_TOUCH_RUNNABLE =
......@@ -53,15 +54,15 @@ public class MessageBannerProperties {
// TODO(pavely): There is no need to maintain two lists of property keys. Remove one and clean
// up references.
public static final PropertyKey[] ALL_KEYS =
new PropertyKey[] {PRIMARY_BUTTON_TEXT, PRIMARY_BUTTON_CLICK_LISTENER, TITLE,
DESCRIPTION, ICON, ICON_RESOURCE_ID, SECONDARY_ICON, SECONDARY_ICON_RESOURCE_ID,
SECONDARY_ACTION_TEXT, SECONDARY_ICON_CONTENT_DESCRIPTION, TRANSLATION_Y, ALPHA,
ON_TOUCH_RUNNABLE, ON_PRIMARY_ACTION, ON_SECONDARY_ACTION};
public static final PropertyKey[] ALL_KEYS = new PropertyKey[] {PRIMARY_BUTTON_TEXT,
PRIMARY_BUTTON_CLICK_LISTENER, TITLE, DESCRIPTION, ICON, ICON_RESOURCE_ID,
SECONDARY_ICON, SECONDARY_ICON_RESOURCE_ID, SECONDARY_ACTION_TEXT,
SECONDARY_ICON_CONTENT_DESCRIPTION, TRANSLATION_X, TRANSLATION_Y, ALPHA,
ON_TOUCH_RUNNABLE, ON_PRIMARY_ACTION, ON_SECONDARY_ACTION};
public static final PropertyKey[] SINGLE_ACTION_MESSAGE_KEYS =
new PropertyKey[] {PRIMARY_BUTTON_TEXT, PRIMARY_BUTTON_CLICK_LISTENER, TITLE,
DESCRIPTION, ICON, ICON_RESOURCE_ID, SECONDARY_ICON, SECONDARY_ICON_RESOURCE_ID,
SECONDARY_ACTION_TEXT, ON_DISMISSED, TRANSLATION_Y, ALPHA, ON_TOUCH_RUNNABLE,
ON_PRIMARY_ACTION, ON_SECONDARY_ACTION};
SECONDARY_ACTION_TEXT, ON_DISMISSED, TRANSLATION_X, TRANSLATION_Y, ALPHA,
ON_TOUCH_RUNNABLE, ON_PRIMARY_ACTION, ON_SECONDARY_ACTION};
}
......@@ -17,6 +17,7 @@ import static org.chromium.components.messages.MessageBannerProperties.SECONDARY
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON_CONTENT_DESCRIPTION;
import static org.chromium.components.messages.MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID;
import static org.chromium.components.messages.MessageBannerProperties.TITLE;
import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_X;
import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_Y;
import android.annotation.SuppressLint;
......@@ -68,6 +69,8 @@ public class MessageBannerViewBinder {
}
} else if (propertyKey == ALPHA) {
view.setAlpha(model.get(ALPHA));
} else if (propertyKey == TRANSLATION_X) {
view.setTranslationX(model.get(TRANSLATION_X));
} else if (propertyKey == TRANSLATION_Y) {
view.setTranslationY(model.get(TRANSLATION_Y));
}
......
......@@ -50,17 +50,19 @@ public class MessageContainer extends FrameLayout {
}
/**
* Runs a {@link Runnable} after the initial layout. If the view is already laid out, the
* {@link Runnable} will be called immediately.
* Runs a {@link Runnable} after the message's initial layout. If the view is already laid out,
* the {@link Runnable} will be called immediately.
* @param runnable The {@link Runnable}.
*/
void runAfterInitialLayout(Runnable runnable) {
if (getHeight() > 0) {
void runAfterInitialMessageLayout(Runnable runnable) {
final View message = findViewById(R.id.message_banner);
assert message != null;
if (message.getHeight() > 0) {
runnable.run();
return;
}
addOnLayoutChangeListener(new OnLayoutChangeListener() {
message.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
......
......@@ -74,7 +74,7 @@ public class SingleActionMessage implements MessageStateHandler {
// Wait until the message and the container are measured before showing the message. This
// is required in case the animation set-up requires the height of the container, e.g.
// showing messages without the top controls visible.
mContainer.runAfterInitialLayout(showRunnable);
mContainer.runAfterInitialMessageLayout(showRunnable);
}
/**
......
......@@ -6,6 +6,7 @@ package org.chromium.components.messages;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
......@@ -59,17 +60,18 @@ public class SingleActionMessageTest extends DummyUiActivityTestCase {
final MessageBannerCoordinator messageBanner = Mockito.mock(MessageBannerCoordinator.class);
doNothing().when(messageBanner).show(any(Runnable.class));
doNothing().when(messageBanner).setOnTouchRunnable(any(Runnable.class));
final MessageBannerView view = Mockito.mock(MessageBannerView.class);
final MessageBannerView view = new MessageBannerView(getActivity(), null);
view.setId(R.id.message_banner);
message.setMessageBannerForTesting(messageBanner);
message.setViewForTesting(view);
message.show();
Assert.assertEquals(
"Message container should have one message view after the message is shown.", 1,
container.getChildCount());
final ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
doNothing().when(messageBanner).hide(runnableCaptor.capture());
message.hide(true, () -> {});
// Let's pretend the animation ended, and the mediator called the callback as a result.
final ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(messageBanner).hide(runnableCaptor.capture());
runnableCaptor.getValue().run();
Assert.assertEquals(
"Message container should not have any view after the message is hidden.", 0,
......@@ -90,7 +92,8 @@ public class SingleActionMessageTest extends DummyUiActivityTestCase {
final MessageBannerCoordinator messageBanner1 =
Mockito.mock(MessageBannerCoordinator.class);
doNothing().when(messageBanner1).show(any(Runnable.class));
final MessageBannerView view1 = Mockito.mock(MessageBannerView.class);
final MessageBannerView view1 = new MessageBannerView(getActivity(), null);
view1.setId(R.id.message_banner);
message1.setMessageBannerForTesting(messageBanner1);
message1.setViewForTesting(view1);
SingleActionMessage message2 =
......@@ -98,7 +101,8 @@ public class SingleActionMessageTest extends DummyUiActivityTestCase {
final MessageBannerCoordinator messageBanner2 =
Mockito.mock(MessageBannerCoordinator.class);
doNothing().when(messageBanner2).show(any(Runnable.class));
final MessageBannerView view2 = Mockito.mock(MessageBannerView.class);
final MessageBannerView view2 = new MessageBannerView(getActivity(), null);
view2.setId(R.id.message_banner);
message2.setMessageBannerForTesting(messageBanner2);
message2.setViewForTesting(view2);
message1.show();
......
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