Commit 62542ee6 authored by Sinan Sahin's avatar Sinan Sahin Committed by Commit Bot

[Offline indicator v2] Add Java animations for grid tab switcher

This CL adds browser controls animations for the tab switcher. As
changing the margins in every animation frame would cause a relayout and
be expensive, we use the translationY property to animate the page. The
margins are applied only once at the start or end of the animation.

Bug: 1075640
Change-Id: Ic8af8fa9e043bf24bdedcf87ce2ca793f73baa6e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2269197
Commit-Queue: Sinan Sahin <sinansahin@google.com>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#784198}
parent d6c547b8
......@@ -28,16 +28,19 @@ class TabListContainerProperties {
public static final PropertyModel.WritableBooleanPropertyKey ANIMATE_VISIBILITY_CHANGES =
new PropertyModel.WritableBooleanPropertyKey();
public static final PropertyModel.WritableIntPropertyKey TOP_CONTROLS_HEIGHT =
public static final PropertyModel.WritableIntPropertyKey TOP_MARGIN =
new PropertyModel.WritableIntPropertyKey();
public static final PropertyModel.WritableIntPropertyKey TRANSLATION_Y =
new PropertyModel.WritableIntPropertyKey();
public static final PropertyModel.WritableIntPropertyKey BOTTOM_CONTROLS_HEIGHT =
new PropertyModel.WritableIntPropertyKey();
public static final PropertyModel.WritableIntPropertyKey SHADOW_TOP_MARGIN =
public static final PropertyModel.WritableIntPropertyKey SHADOW_TOP_OFFSET =
new PropertyModel.WritableIntPropertyKey();
public static final PropertyKey[] ALL_KEYS = new PropertyKey[] {IS_VISIBLE, IS_INCOGNITO,
VISIBILITY_LISTENER, INITIAL_SCROLL_INDEX, ANIMATE_VISIBILITY_CHANGES,
TOP_CONTROLS_HEIGHT, BOTTOM_CONTROLS_HEIGHT, SHADOW_TOP_MARGIN};
VISIBILITY_LISTENER, INITIAL_SCROLL_INDEX, ANIMATE_VISIBILITY_CHANGES, TRANSLATION_Y,
TOP_MARGIN, BOTTOM_CONTROLS_HEIGHT, SHADOW_TOP_OFFSET};
}
......@@ -9,8 +9,9 @@ import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerP
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.SHADOW_TOP_MARGIN;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TOP_CONTROLS_HEIGHT;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.SHADOW_TOP_OFFSET;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TOP_MARGIN;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TRANSLATION_Y;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.VISIBILITY_LISTENER;
import android.widget.FrameLayout;
......@@ -48,16 +49,21 @@ class TabListContainerViewBinder {
// RecyclerView#scrollToPosition(int) behaves incorrectly first time after cold start.
int index = (Integer) model.get(INITIAL_SCROLL_INDEX);
((LinearLayoutManager) view.getLayoutManager()).scrollToPositionWithOffset(index, 0);
} else if (TOP_CONTROLS_HEIGHT == propertyKey) {
} else if (TOP_MARGIN == propertyKey) {
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams();
params.topMargin = model.get(TOP_CONTROLS_HEIGHT);
final int newTopMargin = model.get(TOP_MARGIN);
if (newTopMargin == params.topMargin) return;
params.topMargin = newTopMargin;
view.requestLayout();
} else if (TRANSLATION_Y == propertyKey) {
view.setTranslationY(model.get(TRANSLATION_Y));
} else if (BOTTOM_CONTROLS_HEIGHT == propertyKey) {
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams();
params.bottomMargin = model.get(BOTTOM_CONTROLS_HEIGHT);
view.requestLayout();
} else if (SHADOW_TOP_MARGIN == propertyKey) {
view.setShadowTopMargin(model.get(SHADOW_TOP_MARGIN));
} else if (SHADOW_TOP_OFFSET == propertyKey) {
view.setShadowTopOffset(model.get(SHADOW_TOP_OFFSET));
}
}
}
......@@ -143,7 +143,7 @@ class TabListRecyclerView
private boolean mIsDynamicViewRegistered;
private long mLastDirtyTime;
private ImageView mShadowImageView;
private int mShadowTopMargin;
private int mShadowTopOffset;
private TabListOnScrollListener mScrollListener;
private final RemoveItemAnimator mRemoveItemAnimator = new RemoveItemAnimator();
......@@ -229,8 +229,8 @@ class TabListRecyclerView
res.getDimensionPixelSize(
org.chromium.chrome.R.dimen.toolbar_shadow_height),
Gravity.TOP);
params.topMargin = mShadowTopMargin;
mShadowImageView.setLayoutParams(params);
mShadowImageView.setTranslationY(mShadowTopOffset);
FrameLayout parent = (FrameLayout) getParent();
parent.addView(mShadowImageView);
} else if (getParent() instanceof RelativeLayout) {
......@@ -255,31 +255,24 @@ class TabListRecyclerView
}
}
void setShadowTopMargin(int shadowTopMargin) {
mShadowTopMargin = shadowTopMargin;
void setShadowTopOffset(int shadowTopOffset) {
mShadowTopOffset = shadowTopOffset;
if (mShadowImageView != null && getParent() instanceof FrameLayout) {
final ViewGroup.MarginLayoutParams layoutParams =
((ViewGroup.MarginLayoutParams) mShadowImageView.getLayoutParams());
layoutParams.topMargin = shadowTopMargin;
mShadowImageView.setLayoutParams(layoutParams);
// Wait for a layout and set the shadow visibility using the newly computed scroll
// offset in case the new layout requires us to toggle the shadow visibility. E.g. the
// height increases and the grid isn't scrolled anymore.
mShadowImageView.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) {
// Since the shadow has no functionality, other than just existing visually, we can use
// translationY to position it using the top offset. This is preferable to setting a
// margin because translation doesn't require a relayout.
mShadowImageView.setTranslationY(mShadowTopOffset);
// Set the shadow visibility using the newly computed scroll offset in case the new
// layout requires us to toggle the shadow visibility. E.g. the height increases and the
// grid isn't scrolled anymore.
final int scrollOffset = computeVerticalScrollOffset();
if (scrollOffset == 0) {
setShadowVisibility(false);
} else if (scrollOffset > 0) {
setShadowVisibility(true);
}
v.removeOnLayoutChangeListener(this);
}
});
}
}
......
......@@ -9,8 +9,9 @@ import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerP
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.SHADOW_TOP_MARGIN;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TOP_CONTROLS_HEIGHT;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.SHADOW_TOP_OFFSET;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TOP_MARGIN;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TRANSLATION_Y;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.VISIBILITY_LISTENER;
import android.graphics.Bitmap;
......@@ -302,19 +303,20 @@ class TabSwitcherMediator implements TabSwitcher.Controller, TabListRecyclerView
};
mBrowserControlsObserver = new BrowserControlsStateProvider.Observer() {
@Override
public void onContentOffsetChanged(int offset) {}
@Override
public void onControlsOffsetChanged(int topOffset, int topControlsMinHeightOffset,
int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {}
int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {
if (mMode == TabListCoordinator.TabListMode.CAROUSEL) return;
updateTopControlsProperties();
}
@Override
public void onTopControlsHeightChanged(
int topControlsHeight, int topControlsMinHeight) {
if (mMode == TabListCoordinator.TabListMode.CAROUSEL) return;
updateTopControlsProperties(topControlsHeight);
updateTopControlsProperties();
}
@Override
......@@ -355,7 +357,7 @@ class TabSwitcherMediator implements TabSwitcher.Controller, TabListRecyclerView
// Container view takes care of padding and margin in start surface.
if (mMode != TabListCoordinator.TabListMode.CAROUSEL) {
updateTopControlsProperties(browserControlsStateProvider.getTopControlsHeight());
updateTopControlsProperties();
mContainerViewModel.set(
BOTTOM_CONTROLS_HEIGHT, browserControlsStateProvider.getBottomControlsHeight());
}
......@@ -443,13 +445,32 @@ class TabSwitcherMediator implements TabSwitcher.Controller, TabListRecyclerView
mContainerViewModel.set(IS_VISIBLE, isVisible);
}
private void updateTopControlsProperties(int topControlsHeight) {
// The start surface checks in this block are for top controls height and shadow
// margin to be set correctly for displaying the omnibox above the tab switcher.
topControlsHeight =
StartSurfaceConfiguration.isStartSurfaceEnabled() ? 0 : topControlsHeight;
mContainerViewModel.set(TOP_CONTROLS_HEIGHT, topControlsHeight);
mContainerViewModel.set(SHADOW_TOP_MARGIN, topControlsHeight);
private void updateTopControlsProperties() {
// If the Start surface is enabled, it will handle the margins and positioning of the tab
// switcher. So, we shouldn't do it here.
if (StartSurfaceConfiguration.isStartSurfaceEnabled()) {
mContainerViewModel.set(TOP_MARGIN, 0);
mContainerViewModel.set(SHADOW_TOP_OFFSET, 0);
return;
}
final int controlsHeight = mBrowserControlsStateProvider.getTopControlsHeight();
final int contentOffset = mBrowserControlsStateProvider.getContentOffset();
// If the top controls are at the resting position or their height is decreasing, we want to
// update the margin. We don't do this if the controls height is increasing because changing
// the margin shrinks the view height to its final value, leaving a gap at the bottom until
// the animation finishes.
if (contentOffset >= controlsHeight) {
mContainerViewModel.set(TOP_MARGIN, controlsHeight);
}
// If the content offset is different from the margin, we use translationY to position the
// view in line with the content offset.
mContainerViewModel.set(TRANSLATION_Y, contentOffset - mContainerViewModel.get(TOP_MARGIN));
// Offsetting the shadow using the content offset will position it right below the top
// controls.
mContainerViewModel.set(SHADOW_TOP_OFFSET, contentOffset);
}
/**
......
......@@ -28,6 +28,7 @@ import org.junit.runner.RunWith;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.MathUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
......@@ -235,12 +236,12 @@ public class TabListContainerViewBinderTest extends DummyUiActivityTestCase {
@Test
@MediumTest
@UiThreadTest
public void testTopContainerHeightSetsTopMargin() {
public void testTopMarginSetsTopMargin() {
assertThat(mRecyclerView.getLayoutParams(), instanceOf(FrameLayout.LayoutParams.class));
assertThat(
((FrameLayout.LayoutParams) mRecyclerView.getLayoutParams()).topMargin, equalTo(0));
mContainerModel.set(TabListContainerProperties.TOP_CONTROLS_HEIGHT, CONTAINER_HEIGHT);
mContainerModel.set(TabListContainerProperties.TOP_MARGIN, CONTAINER_HEIGHT);
assertThat(((FrameLayout.LayoutParams) mRecyclerView.getLayoutParams()).topMargin,
equalTo(CONTAINER_HEIGHT));
}
......@@ -270,13 +271,34 @@ public class TabListContainerViewBinderTest extends DummyUiActivityTestCase {
ImageView shadowImageView = mRecyclerView.getShadowImageViewForTesting();
assertThat(mRecyclerView.getLayoutParams(), instanceOf(FrameLayout.LayoutParams.class));
assertEquals(0, ((FrameLayout.LayoutParams) shadowImageView.getLayoutParams()).topMargin);
assertEquals(0, shadowImageView.getTranslationY(), MathUtils.EPSILON);
mContainerModel.set(
TabListContainerProperties.SHADOW_TOP_OFFSET, INCREASED_CONTAINER_HEIGHT);
assertEquals(
INCREASED_CONTAINER_HEIGHT, shadowImageView.getTranslationY(), MathUtils.EPSILON);
}
@Test
@MediumTest
@UiThreadTest
public void testTranslationYSetsTranslation() {
mContainerModel.set(
TabListContainerProperties.SHADOW_TOP_MARGIN, INCREASED_CONTAINER_HEIGHT);
assertEquals(INCREASED_CONTAINER_HEIGHT,
((FrameLayout.LayoutParams) shadowImageView.getLayoutParams()).topMargin);
TabListContainerProperties.VISIBILITY_LISTENER, mMockVisibilityListener);
mContainerModel.set(TabListContainerProperties.ANIMATE_VISIBILITY_CHANGES, false);
mContainerModel.set(TabListContainerProperties.IS_VISIBLE, true);
assertEquals("Wrong initial translationY.", 0, mRecyclerView.getTranslationY(),
MathUtils.EPSILON);
mContainerModel.set(TabListContainerProperties.TRANSLATION_Y, INCREASED_CONTAINER_HEIGHT);
assertEquals("translationY is not set to the correct value.", INCREASED_CONTAINER_HEIGHT,
mRecyclerView.getTranslationY(), MathUtils.EPSILON);
mContainerModel.set(TabListContainerProperties.TRANSLATION_Y, 0);
assertEquals("translationY is not set to the correct value.", 0,
mRecyclerView.getTranslationY(), MathUtils.EPSILON);
}
@Override
......
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