Commit 6b961bc3 authored by Yue Zhang's avatar Yue Zhang Committed by Commit Bot

Enable drop-to-merge in GTS

This CL enables dropping a tab on another tab to form a group. It is
behind the TabGroupsAndroidUiImprovements flag.

Bug: 963692
Change-Id: I1cb49a20459972af3c02bdc7c9ebb9480209de29
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1615606Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Commit-Queue: Yue Zhang <yuezhanggg@google.com>
Cr-Commit-Position: refs/heads/master@{#666450}
parent 7ad40e8d
...@@ -29,6 +29,7 @@ android_library("java") { ...@@ -29,6 +29,7 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogParent.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogParent.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridItemTouchHelperCallback.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java",
"java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetContent.java", "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetContent.java",
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
<dimen name="tab_grid_close_button_size">18dp</dimen> <dimen name="tab_grid_close_button_size">18dp</dimen>
<dimen name="tab_grid_dialog_side_margin">16dp</dimen> <dimen name="tab_grid_dialog_side_margin">16dp</dimen>
<dimen name="tab_grid_dialog_top_margin">85dp</dimen> <dimen name="tab_grid_dialog_top_margin">85dp</dimen>
<dimen name="tab_grid_merge_threshold">45dp</dimen>
<dimen name="tab_grid_thumbnail_card_default_size">152dp</dimen> <dimen name="tab_grid_thumbnail_card_default_size">152dp</dimen>
<dimen name="tab_grid_thumbnail_favicon_frame_padding">16dp</dimen> <dimen name="tab_grid_thumbnail_favicon_frame_padding">16dp</dimen>
<dimen name="tab_grid_thumbnail_favicon_padding">24dp</dimen> <dimen name="tab_grid_thumbnail_favicon_padding">24dp</dimen>
......
...@@ -12,6 +12,7 @@ import org.chromium.chrome.browser.ChromeFeatureList; ...@@ -12,6 +12,7 @@ import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory; import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver; import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.chrome.browser.tasks.tabgroup.TabGroupModelFilter; import org.chromium.chrome.browser.tasks.tabgroup.TabGroupModelFilter;
...@@ -110,25 +111,25 @@ public class TabGroupUtils { ...@@ -110,25 +111,25 @@ public class TabGroupUtils {
/** /**
* This method gets the index in TabModel of the first tab in {@code tabs}. * This method gets the index in TabModel of the first tab in {@code tabs}.
* @param selector The selector that owns the {@code tab}. * @param tabModel The tabModel that owns the {@code tab}.
* @param tabs The list of tabs among which we need to find the first tab index. * @param tabs The list of tabs among which we need to find the first tab index.
* @return The index in TabModel of the first tab in {@code tabs} * @return The index in TabModel of the first tab in {@code tabs}
*/ */
public static int getFirstTabModelIndexForList(TabModelSelector selector, List<Tab> tabs) { public static int getFirstTabModelIndexForList(TabModel tabModel, List<Tab> tabs) {
assert tabs != null && tabs.size() != 0; assert tabs != null && tabs.size() != 0;
return selector.getCurrentModel().indexOf(tabs.get(0)); return tabModel.indexOf(tabs.get(0));
} }
/** /**
* This method gets the index in TabModel of the last tab in {@code tabs}. * This method gets the index in TabModel of the last tab in {@code tabs}.
* @param selector The selector that owns the {@code tab}. * @param tabModel The tabModel that owns the {@code tab}.
* @param tabs The list of tabs among which we need to find the last tab index. * @param tabs The list of tabs among which we need to find the last tab index.
* @return The index in TabModel of the last tab in {@code tabs} * @return The index in TabModel of the last tab in {@code tabs}
*/ */
public static int getLastTabModelIndexForList(TabModelSelector selector, List<Tab> tabs) { public static int getLastTabModelIndexForList(TabModel tabModel, List<Tab> tabs) {
assert tabs != null && tabs.size() != 0; assert tabs != null && tabs.size() != 0;
return selector.getCurrentModel().indexOf(tabs.get(tabs.size() - 1)); return tabModel.indexOf(tabs.get(tabs.size() - 1));
} }
} }
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.tasks.tab_management;
import android.graphics.Canvas;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupUtils;
import org.chromium.chrome.browser.tasks.tabgroup.TabGroupModelFilter;
import java.util.List;
/**
* A {@link ItemTouchHelper.SimpleCallback} implementation to host the logic for swipe and drag
* related actions in grid related layouts.
* TODO(yuezhanggg): Get rid of using notifyDataSetChanged in adapter.
*/
public class TabGridItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
/**
* An interface to update information in {@link TabListModel}.
*/
public interface UpdateTabInfoHandler {
void updateTabInfo(int index, Tab tab, boolean isSelected);
}
private final TabListModel mModel;
private final TabModelSelector mTabModelSelector;
private final TabListMediator.TabActionListener mTabClosedListener;
private final String mComponentName;
private final UpdateTabInfoHandler mUpdateTabHandler;
private float mSwipeToDismissThreshold;
private float mMergeThreshold;
private boolean mActionsOnAllRelatedTabs;
private int mDragFlags;
private int mSelectedTabIndex = TabModel.INVALID_TAB_INDEX;
private int mHoveredTabIndex = TabModel.INVALID_TAB_INDEX;
private RecyclerView mRecyclerView;
public TabGridItemTouchHelperCallback(TabListModel tabListModel,
TabModelSelector tabModelSelector, TabListMediator.TabActionListener tabClosedListener,
UpdateTabInfoHandler updateTabHandler, String componentName,
boolean actionsOnAllRelatedTabs) {
super(0, 0);
mModel = tabListModel;
mTabModelSelector = tabModelSelector;
mTabClosedListener = tabClosedListener;
mUpdateTabHandler = updateTabHandler;
mComponentName = componentName;
mActionsOnAllRelatedTabs = actionsOnAllRelatedTabs;
}
/**
* This method sets up parameters that are used by the {@link ItemTouchHelper} to make decisions
* about user actions.
* @param swipeToDismissThreshold Defines the threshold that user needs to swipe in order to
* be considered as a remove operation.
* @param mergeThreshold Defines the threshold of how much two items need to be
* overlapped in order to be considered as a merge operation.
* @param isDragEnabled Whether drag related behavior should handled in this
* callback.
*/
void setupCallback(float swipeToDismissThreshold, float mergeThreshold, boolean isDragEnabled) {
mSwipeToDismissThreshold = swipeToDismissThreshold;
mMergeThreshold = mergeThreshold;
mDragFlags = isDragEnabled ? ItemTouchHelper.START | ItemTouchHelper.END
| ItemTouchHelper.UP | ItemTouchHelper.DOWN
: 0;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
final int dragFlags = mDragFlags;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
mRecyclerView = recyclerView;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder fromViewHolder,
RecyclerView.ViewHolder toViewHolder) {
assert fromViewHolder instanceof TabGridViewHolder;
assert toViewHolder instanceof TabGridViewHolder;
RecordUserAction.record("TabGrid.DragToReorder." + mComponentName);
mSelectedTabIndex = toViewHolder.getAdapterPosition();
if (mHoveredTabIndex != -1) {
mModel.updateHoveredTabForMergeToGroup(mHoveredTabIndex, false);
mHoveredTabIndex = -1;
}
int currentTabId = ((TabGridViewHolder) fromViewHolder).getTabId();
int destinationTabId = ((TabGridViewHolder) toViewHolder).getTabId();
int distance = toViewHolder.getAdapterPosition() - fromViewHolder.getAdapterPosition();
TabModelFilter filter =
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter();
TabModel tabModel = mTabModelSelector.getCurrentModel();
if (filter instanceof EmptyTabModelFilter) {
tabModel.moveTab(currentTabId,
mModel.indexFromId(currentTabId) + (distance > 0 ? distance + 1 : distance));
} else if (!mActionsOnAllRelatedTabs) {
int destinationIndex = tabModel.indexOf(mTabModelSelector.getTabById(destinationTabId));
tabModel.moveTab(currentTabId, distance > 0 ? destinationIndex + 1 : destinationIndex);
} else {
List<Tab> destinationTabGroup = getRelatedTabsForId(destinationTabId);
int newIndex = distance >= 0
? TabGroupUtils.getLastTabModelIndexForList(tabModel, destinationTabGroup) + 1
: TabGroupUtils.getFirstTabModelIndexForList(tabModel, destinationTabGroup);
((TabGroupModelFilter) filter).moveRelatedTabs(currentTabId, newIndex);
}
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
assert viewHolder instanceof TabGridViewHolder;
mTabClosedListener.run(((TabGridViewHolder) viewHolder).getTabId());
RecordUserAction.record("MobileStackViewSwipeCloseTab." + mComponentName);
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
mSelectedTabIndex = viewHolder.getAdapterPosition();
mModel.updateSelectedTabForMergeToGroup(mSelectedTabIndex, true);
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
if (mHoveredTabIndex != -1 && mActionsOnAllRelatedTabs) {
RecordUserAction.record("GridTabSwitcher.DropTabToMerge");
onTabMergeToGroup(mSelectedTabIndex, mHoveredTabIndex);
mRecyclerView.getAdapter().notifyDataSetChanged();
}
if (mHoveredTabIndex == -1) {
mModel.updateSelectedTabForMergeToGroup(mSelectedTabIndex, false);
}
mHoveredTabIndex = -1;
mSelectedTabIndex = -1;
}
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float alpha = Math.max(0.2f, 1f - 0.8f * Math.abs(dX) / mSwipeToDismissThreshold);
int index = mModel.indexFromId(((TabGridViewHolder) viewHolder).getTabId());
if (index == -1) return;
mModel.get(index).set(TabProperties.ALPHA, alpha);
} else if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && mActionsOnAllRelatedTabs) {
int prev_hovered = mHoveredTabIndex;
mHoveredTabIndex = TabListRecyclerView.getHoveredTabIndex(
recyclerView, viewHolder.itemView, dX, dY, mMergeThreshold);
mModel.updateHoveredTabForMergeToGroup(mHoveredTabIndex, true);
if (prev_hovered != mHoveredTabIndex) {
mModel.updateHoveredTabForMergeToGroup(prev_hovered, false);
}
}
}
@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return mSwipeToDismissThreshold / viewHolder.itemView.getWidth();
}
private List<Tab> getRelatedTabsForId(int id) {
return mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.getRelatedTabList(id);
}
private void onTabMergeToGroup(int selectedCardIndex, int hoveredCardIndex) {
mModel.updateHoveredTabForMergeToGroup(hoveredCardIndex, false);
TabGroupModelFilter filter =
(TabGroupModelFilter) mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter();
filter.mergeTabsToGroup(filter.getTabAt(selectedCardIndex).getId(),
filter.getTabAt(hoveredCardIndex).getId());
int destinationIndex =
selectedCardIndex > hoveredCardIndex ? hoveredCardIndex : hoveredCardIndex - 1;
Tab newSelectedTab = filter.getTabAt(destinationIndex);
boolean isSelected = newSelectedTab.getId() == mTabModelSelector.getCurrentTabId();
mUpdateTabHandler.updateTabInfo(destinationIndex, newSelectedTab, isSelected);
}
@VisibleForTesting
void setActionsOnAllRelatedTabsForTest(boolean flag) {
mActionsOnAllRelatedTabs = flag;
}
@VisibleForTesting
void setHoveredTabIndexForTest(int index) {
mHoveredTabIndex = index;
}
@VisibleForTesting
void setSelectedTabIndexForTest(int index) {
mSelectedTabIndex = index;
}
}
...@@ -111,6 +111,9 @@ class TabGridViewBinder { ...@@ -111,6 +111,9 @@ class TabGridViewBinder {
} else if (TabProperties.IPH_PROVIDER == propertyKey) { } else if (TabProperties.IPH_PROVIDER == propertyKey) {
TabListMediator.IphProvider provider = item.get(TabProperties.IPH_PROVIDER); TabListMediator.IphProvider provider = item.get(TabProperties.IPH_PROVIDER);
if (provider != null) provider.showIPH(holder.thumbnail); if (provider != null) provider.showIPH(holder.thumbnail);
} else if (TabProperties.CARD_ANIMATION_STATUS == propertyKey) {
TabListRecyclerView.scaleTabGridCardView(
holder.itemView, item.get(TabProperties.CARD_ANIMATION_STATUS));
} }
} }
......
...@@ -22,6 +22,7 @@ import org.chromium.chrome.browser.profiles.Profile; ...@@ -22,6 +22,7 @@ import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupUtils; import org.chromium.chrome.browser.tasks.tab_groups.TabGroupUtils;
import org.chromium.chrome.browser.util.FeatureUtilities;
import org.chromium.chrome.tab_ui.R; import org.chromium.chrome.tab_ui.R;
import org.chromium.components.feature_engagement.FeatureConstants; import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
...@@ -77,7 +78,7 @@ public class TabListCoordinator implements Destroyable { ...@@ -77,7 +78,7 @@ public class TabListCoordinator implements Destroyable {
*/ */
TabListCoordinator(@TabListMode int mode, Context context, TabModelSelector tabModelSelector, TabListCoordinator(@TabListMode int mode, Context context, TabModelSelector tabModelSelector,
@Nullable TabListMediator.ThumbnailProvider thumbnailProvider, @Nullable TabListMediator.ThumbnailProvider thumbnailProvider,
@Nullable TabListMediator.TitleProvider titleProvider, boolean closeRelatedTabs, @Nullable TabListMediator.TitleProvider titleProvider, boolean actionOnRelatedTabs,
@Nullable TabListMediator.CreateGroupButtonProvider createGroupButtonProvider, @Nullable TabListMediator.CreateGroupButtonProvider createGroupButtonProvider,
@Nullable TabListMediator @Nullable TabListMediator
.GridCardOnClickListenerProvider gridCardOnClickListenerProvider, .GridCardOnClickListenerProvider gridCardOnClickListenerProvider,
...@@ -140,18 +141,21 @@ public class TabListCoordinator implements Destroyable { ...@@ -140,18 +141,21 @@ public class TabListCoordinator implements Destroyable {
new TabListFaviconProvider(context, Profile.getLastUsedProfile()); new TabListFaviconProvider(context, Profile.getLastUsedProfile());
mMediator = new TabListMediator(tabListModel, tabModelSelector, thumbnailProvider, mMediator = new TabListMediator(tabListModel, tabModelSelector, thumbnailProvider,
titleProvider, tabListFaviconProvider, closeRelatedTabs, createGroupButtonProvider, titleProvider, tabListFaviconProvider, actionOnRelatedTabs,
gridCardOnClickListenerProvider, componentName); createGroupButtonProvider, gridCardOnClickListenerProvider, componentName);
if (mMode == TabListMode.GRID) { if (mMode == TabListMode.GRID) {
ItemTouchHelper touchHelper = new ItemTouchHelper(mMediator.getItemTouchHelperCallback( ItemTouchHelper touchHelper = new ItemTouchHelper(mMediator.getItemTouchHelperCallback(
context.getResources().getDimension(R.dimen.swipe_to_dismiss_threshold))); context.getResources().getDimension(R.dimen.swipe_to_dismiss_threshold),
context.getResources().getDimension(R.dimen.tab_grid_merge_threshold),
!FeatureUtilities.isTabGroupsAndroidEnabled()
|| FeatureUtilities.isTabGroupsAndroidUiImprovementsEnabled()));
touchHelper.attachToRecyclerView(mRecyclerView); touchHelper.attachToRecyclerView(mRecyclerView);
mMediator.registerOrientationListener( mMediator.registerOrientationListener(
(GridLayoutManager) mRecyclerView.getLayoutManager()); (GridLayoutManager) mRecyclerView.getLayoutManager());
} }
if (closeRelatedTabs) { if (actionOnRelatedTabs) {
// Only do this for Grid Tab Switcher. // Only do this for Grid Tab Switcher.
// TODO(crbug.com/964406): unregister the listener when we don't need it. // TODO(crbug.com/964406): unregister the listener when we don't need it.
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener( mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
......
...@@ -6,11 +6,16 @@ package org.chromium.chrome.browser.tasks.tab_management; ...@@ -6,11 +6,16 @@ package org.chromium.chrome.browser.tasks.tab_management;
import static org.chromium.chrome.browser.tasks.tab_management.TabProperties.TAB_ID; import static org.chromium.chrome.browser.tasks.tab_management.TabProperties.TAB_ID;
import android.util.Pair;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyListModel; import org.chromium.ui.modelutil.PropertyListModel;
import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModel;
import java.util.List;
/** /**
* A {@link PropertyListModel} implementation to keep information about a list of * A {@link PropertyListModel} implementation to keep information about a list of
* {@link org.chromium.chrome.browser.tab.Tab}s. * {@link org.chromium.chrome.browser.tab.Tab}s.
...@@ -27,4 +32,77 @@ class TabListModel extends PropertyListModel<PropertyModel, PropertyKey> { ...@@ -27,4 +32,77 @@ class TabListModel extends PropertyListModel<PropertyModel, PropertyKey> {
} }
return TabModel.INVALID_TAB_INDEX; return TabModel.INVALID_TAB_INDEX;
} }
/**
* Sync the {@link TabListModel} with updated information. Update tab id of
* the item in {@code index} with the current selected {@code tab} of the group.
* @param selectedTab The current selected tab in the group.
* @param index The index of the item in {@link TabListModel} that needs to be updated.
*/
void updateTabListModelIdForGroup(Tab selectedTab, int index) {
get(index).set(TabProperties.TAB_ID, selectedTab.getId());
}
/**
* This method gets indexes in the {@link TabListModel} of the two tabs that are merged into one
* group.
* @param tabModel The tabModel that owns the tabs.
* @param tabs The list that contains tabs of the newly merged group.
* @return A Pair with its first member as the index of the tab that is selected to merge and
* the second member as the index of the tab that is being merged into.
*/
Pair<Integer, Integer> getIndexesForMergeToGroup(TabModel tabModel, List<Tab> tabs) {
int desIndex = TabModel.INVALID_TAB_INDEX;
int srcIndex = TabModel.INVALID_TAB_INDEX;
int lastTabModelIndex = tabModel.indexOf(tabs.get(tabs.size() - 1));
for (int i = lastTabModelIndex; i >= 0; i--) {
Tab curTab = tabModel.getTabAt(i);
if (!tabs.contains(curTab)) break;
int index = indexFromId(curTab.getId());
if (index != TabModel.INVALID_TAB_INDEX && srcIndex == TabModel.INVALID_TAB_INDEX) {
srcIndex = index;
} else if (index != TabModel.INVALID_TAB_INDEX
&& desIndex == TabModel.INVALID_TAB_INDEX) {
desIndex = index;
}
}
return new Pair<>(desIndex, srcIndex);
}
/**
* This method updates the information in {@link TabListModel} of the selected tab when a merge
* related operation happens.
* @param index The index of the item in {@link TabListModel} that needs to be updated.
* @param isSelected Whether the tab is selected or not in a merge related operation. If
* selected, update the corresponding item in {@link TabListModel} to the selected
* state. If not, restore it to original state.
*/
void updateSelectedTabForMergeToGroup(int index, boolean isSelected) {
int status = isSelected ? TabListRecyclerView.ANIMATION_STATUS_ZOOM_IN
: TabListRecyclerView.ANIMATION_STATUS_ZOOM_OUT;
if (index < 0 || index >= size()
|| get(index).get(TabProperties.CARD_ANIMATION_STATUS) == status)
return;
get(index).set(TabProperties.CARD_ANIMATION_STATUS, status);
get(index).set(TabProperties.ALPHA, isSelected ? 0.8f : 1f);
}
/**
* This method updates the information in {@link TabListModel} of the hovered tab when a merge
* related operation happens.
* @param index The index of the item in {@link TabListModel} that needs to be updated.
* @param isHovered Whether the tab is hovered or not in a merge related operation. If
* hovered, update the corresponding item in {@link TabListModel} to the hovered state.
* If not, restore it to original state.
*/
void updateHoveredTabForMergeToGroup(int index, boolean isHovered) {
int status = isHovered ? TabListRecyclerView.ANIMATION_STATUS_ZOOM_IN
: TabListRecyclerView.ANIMATION_STATUS_ZOOM_OUT;
if (index < 0 || index >= size()
|| get(index).get(TabProperties.CARD_ANIMATION_STATUS) == status)
return;
get(index).set(TabProperties.CARD_ANIMATION_STATUS, status);
}
} }
...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.tasks.tab_management; ...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.tasks.tab_management;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
...@@ -28,6 +29,10 @@ import org.chromium.ui.resources.dynamics.ViewResourceAdapter; ...@@ -28,6 +29,10 @@ import org.chromium.ui.resources.dynamics.ViewResourceAdapter;
*/ */
class TabListRecyclerView extends RecyclerView { class TabListRecyclerView extends RecyclerView {
public static final long BASE_ANIMATION_DURATION_MS = 218; public static final long BASE_ANIMATION_DURATION_MS = 218;
public static final long RESTORE_ANIMATION_DURATION_MS = 10;
public static final int ANIMATION_STATUS_RESTORE = 0;
public static final int ANIMATION_STATUS_ZOOM_OUT = 1;
public static final int ANIMATION_STATUS_ZOOM_IN = 2;
/** /**
* Field trial parameter for downsampling scaling factor. * Field trial parameter for downsampling scaling factor.
...@@ -235,4 +240,46 @@ class TabListRecyclerView extends RecyclerView { ...@@ -235,4 +240,46 @@ class TabListRecyclerView extends RecyclerView {
rect.bottom -= loc[1]; rect.bottom -= loc[1];
return rect; return rect;
} }
static void scaleTabGridCardView(View view, int status) {
AnimatorSet scaleAnimator = new AnimatorSet();
float scale = status == ANIMATION_STATUS_ZOOM_IN ? 0.8f : 1f;
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", scale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", scale);
scaleX.setDuration(status == ANIMATION_STATUS_RESTORE ? RESTORE_ANIMATION_DURATION_MS
: BASE_ANIMATION_DURATION_MS);
scaleY.setDuration(status == ANIMATION_STATUS_RESTORE ? RESTORE_ANIMATION_DURATION_MS
: BASE_ANIMATION_DURATION_MS);
scaleAnimator.play(scaleX).with(scaleY);
scaleAnimator.start();
}
/**
* This method finds out the index of the hovered tab's viewHolder in {@code recyclerView}.
* @param recyclerView The recyclerview that owns the tabs' viewHolders.
* @param view The view of the selected tab.
* @param dX The X offset of the selected tab.
* @param dY The Y offset of the selected tab.
* @param threshold The threshold to judge whether two tabs are overlapped.
* @return The index of the hovered tab.
*/
static int getHoveredTabIndex(
RecyclerView recyclerView, View view, float dX, float dY, float threshold) {
for (int i = 0; i < recyclerView.getChildCount(); i++) {
View child = recyclerView.getChildAt(i);
if (child.getLeft() == view.getLeft() && child.getTop() == view.getTop()) {
continue;
}
if (isOverlap(child.getLeft(), child.getTop(), view.getLeft() + dX, view.getTop() + dY,
threshold)) {
return i;
}
}
return -1;
}
private static boolean isOverlap(
float left1, float top1, float left2, float top2, float threshold) {
return Math.abs(left1 - left2) < threshold && Math.abs(top1 - top2) < threshold;
}
} }
...@@ -8,7 +8,6 @@ import android.graphics.drawable.Drawable; ...@@ -8,7 +8,6 @@ import android.graphics.drawable.Drawable;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModel.ReadableIntPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey; import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey; import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
...@@ -16,7 +15,8 @@ import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey; ...@@ -16,7 +15,8 @@ import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
* List of properties to designate information about a single tab. * List of properties to designate information about a single tab.
*/ */
public class TabProperties { public class TabProperties {
public static final PropertyModel.ReadableIntPropertyKey TAB_ID = new ReadableIntPropertyKey(); public static final PropertyModel.WritableIntPropertyKey TAB_ID =
new PropertyModel.WritableIntPropertyKey();
public static final WritableObjectPropertyKey<TabListMediator.TabActionListener> public static final WritableObjectPropertyKey<TabListMediator.TabActionListener>
TAB_SELECTED_LISTENER = new WritableObjectPropertyKey<>(); TAB_SELECTED_LISTENER = new WritableObjectPropertyKey<>();
...@@ -45,9 +45,12 @@ public class TabProperties { ...@@ -45,9 +45,12 @@ public class TabProperties {
public static final PropertyModel.WritableFloatPropertyKey ALPHA = public static final PropertyModel.WritableFloatPropertyKey ALPHA =
new PropertyModel.WritableFloatPropertyKey(); new PropertyModel.WritableFloatPropertyKey();
public static final PropertyModel.WritableIntPropertyKey CARD_ANIMATION_STATUS =
new PropertyModel.WritableIntPropertyKey();
public static final PropertyKey[] ALL_KEYS_TAB_GRID = new PropertyKey[] {TAB_ID, public static final PropertyKey[] ALL_KEYS_TAB_GRID = new PropertyKey[] {TAB_ID,
TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON, THUMBNAIL_FETCHER, IPH_PROVIDER, TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON, THUMBNAIL_FETCHER, IPH_PROVIDER,
TITLE, IS_SELECTED, IS_HIDDEN, CREATE_GROUP_LISTENER, ALPHA}; TITLE, IS_SELECTED, IS_HIDDEN, CREATE_GROUP_LISTENER, ALPHA, CARD_ANIMATION_STATUS};
public static final PropertyKey[] ALL_KEYS_TAB_STRIP = new PropertyKey[] { public static final PropertyKey[] ALL_KEYS_TAB_STRIP = new PropertyKey[] {
TAB_ID, TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON, IS_SELECTED, TITLE}; TAB_ID, TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON, IS_SELECTED, TITLE};
......
...@@ -24,6 +24,7 @@ import android.graphics.Color; ...@@ -24,6 +24,7 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.View; import android.view.View;
import org.junit.After; import org.junit.After;
...@@ -89,6 +90,8 @@ public class TabListMediatorUnitTest { ...@@ -89,6 +90,8 @@ public class TabListMediatorUnitTest {
@Mock @Mock
RecyclerView mRecyclerView; RecyclerView mRecyclerView;
@Mock @Mock
RecyclerView.Adapter mAdapter;
@Mock
TabGroupModelFilter mTabGroupModelFilter; TabGroupModelFilter mTabGroupModelFilter;
@Mock @Mock
EmptyTabModelFilter mEmptyTabModelFilter; EmptyTabModelFilter mEmptyTabModelFilter;
...@@ -225,7 +228,8 @@ public class TabListMediatorUnitTest { ...@@ -225,7 +228,8 @@ public class TabListMediatorUnitTest {
doReturn(mEmptyTabModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter(); doReturn(mEmptyTabModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter();
mMediator.getItemTouchHelperCallback(0f).onMove(mRecyclerView, mViewHolder1, mViewHolder2); mMediator.getItemTouchHelperCallback(0f, 0f, true)
.onMove(mRecyclerView, mViewHolder1, mViewHolder2);
verify(mTabModel).moveTab(eq(TAB1_ID), eq(2)); verify(mTabModel).moveTab(eq(TAB1_ID), eq(2));
} }
...@@ -233,11 +237,13 @@ public class TabListMediatorUnitTest { ...@@ -233,11 +237,13 @@ public class TabListMediatorUnitTest {
@Test @Test
public void sendsMoveTabSignalCorrectlyWithGroup() { public void sendsMoveTabSignalCorrectlyWithGroup() {
initAndAssertAllProperties(); initAndAssertAllProperties();
TabGridItemTouchHelperCallback itemTouchHelperCallback =
(TabGridItemTouchHelperCallback) mMediator.getItemTouchHelperCallback(0f, 0f, true);
itemTouchHelperCallback.setActionsOnAllRelatedTabsForTest(true);
mMediator.setCloseAllRelatedTabs(true);
doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter(); doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter();
mMediator.getItemTouchHelperCallback(0f).onMove(mRecyclerView, mViewHolder1, mViewHolder2); itemTouchHelperCallback.onMove(mRecyclerView, mViewHolder1, mViewHolder2);
verify(mTabGroupModelFilter).moveRelatedTabs(eq(TAB1_ID), eq(2)); verify(mTabGroupModelFilter).moveRelatedTabs(eq(TAB1_ID), eq(2));
} }
...@@ -248,11 +254,32 @@ public class TabListMediatorUnitTest { ...@@ -248,11 +254,32 @@ public class TabListMediatorUnitTest {
doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter(); doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter();
mMediator.getItemTouchHelperCallback(0f).onMove(mRecyclerView, mViewHolder1, mViewHolder2); mMediator.getItemTouchHelperCallback(0f, 0f, true)
.onMove(mRecyclerView, mViewHolder1, mViewHolder2);
verify(mTabModel).moveTab(eq(TAB1_ID), eq(2)); verify(mTabModel).moveTab(eq(TAB1_ID), eq(2));
} }
@Test
public void sendsMergeTabSignalCorrectly() {
initAndAssertAllProperties();
mMediator.setActionOnAllRelatedTabsForTest(true);
TabGridItemTouchHelperCallback itemTouchHelperCallback =
(TabGridItemTouchHelperCallback) mMediator.getItemTouchHelperCallback(0f, 0f, true);
itemTouchHelperCallback.setActionsOnAllRelatedTabsForTest(true);
itemTouchHelperCallback.setHoveredTabIndexForTest(POSITION1);
itemTouchHelperCallback.setSelectedTabIndexForTest(POSITION2);
itemTouchHelperCallback.getMovementFlags(mRecyclerView, mViewHolder1);
doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter();
doReturn(mAdapter).when(mRecyclerView).getAdapter();
// Simulate the drop action.
itemTouchHelperCallback.onSelectedChanged(mViewHolder1, ItemTouchHelper.ACTION_STATE_IDLE);
verify(mTabGroupModelFilter).mergeTabsToGroup(eq(TAB2_ID), eq(TAB1_ID));
}
@Test @Test
public void tabClosure() { public void tabClosure() {
initAndAssertAllProperties(); initAndAssertAllProperties();
...@@ -277,7 +304,7 @@ public class TabListMediatorUnitTest { ...@@ -277,7 +304,7 @@ public class TabListMediatorUnitTest {
@Test @Test
public void tabAddition_GTS() { public void tabAddition_GTS() {
initAndAssertAllProperties(); initAndAssertAllProperties();
mMediator.setCloseAllRelatedTabsForTest(true); mMediator.setActionOnAllRelatedTabsForTest(true);
Tab newTab = prepareTab(TAB3_ID, TAB3_TITLE); Tab newTab = prepareTab(TAB3_ID, TAB3_TITLE);
doReturn(mTab1).when(mTabModelFilter).getTabAt(0); doReturn(mTab1).when(mTabModelFilter).getTabAt(0);
...@@ -298,7 +325,7 @@ public class TabListMediatorUnitTest { ...@@ -298,7 +325,7 @@ public class TabListMediatorUnitTest {
@Test @Test
public void tabAddition_GTS_Skip() { public void tabAddition_GTS_Skip() {
initAndAssertAllProperties(); initAndAssertAllProperties();
mMediator.setCloseAllRelatedTabsForTest(true); mMediator.setActionOnAllRelatedTabsForTest(true);
// Add a new tab to the group with mTab2. // Add a new tab to the group with mTab2.
Tab newTab = prepareTab(TAB3_ID, TAB3_TITLE); Tab newTab = prepareTab(TAB3_ID, TAB3_TITLE);
...@@ -317,7 +344,7 @@ public class TabListMediatorUnitTest { ...@@ -317,7 +344,7 @@ public class TabListMediatorUnitTest {
@Test @Test
public void tabAddition_GTS_Middle() { public void tabAddition_GTS_Middle() {
initAndAssertAllProperties(); initAndAssertAllProperties();
mMediator.setCloseAllRelatedTabsForTest(true); mMediator.setActionOnAllRelatedTabsForTest(true);
Tab newTab = prepareTab(TAB3_ID, TAB3_TITLE); Tab newTab = prepareTab(TAB3_ID, TAB3_TITLE);
doReturn(mTab1).when(mTabModelFilter).getTabAt(0); doReturn(mTab1).when(mTabModelFilter).getTabAt(0);
...@@ -398,6 +425,34 @@ public class TabListMediatorUnitTest { ...@@ -398,6 +425,34 @@ public class TabListMediatorUnitTest {
assertThat(mModel.get(2).get(TabProperties.TITLE), equalTo(TAB3_TITLE)); assertThat(mModel.get(2).get(TabProperties.TITLE), equalTo(TAB3_TITLE));
} }
@Test
public void tabMergeIntoGroup() {
setUpForTabGroupOperation();
mMediator.setActionOnAllRelatedTabsForTest(true);
// Assume that moveTab in TabModel is finished.
doReturn(mTab1).when(mTabModel).getTabAt(POSITION2);
doReturn(mTab2).when(mTabModel).getTabAt(POSITION1);
doReturn(mTabGroupModelFilter).when(mTabModelFilterProvider).getCurrentTabModelFilter();
// Assume that reset in TabGroupModelFilter is finished.
doReturn(new ArrayList<>(Arrays.asList(mTab1, mTab2)))
.when(mTabGroupModelFilter)
.getRelatedTabList(TAB1_ID);
assertThat(mModel.size(), equalTo(2));
assertThat(mModel.get(1).get(TabProperties.TAB_ID), equalTo(TAB2_ID));
assertThat(mModel.get(1).get(TabProperties.TITLE), equalTo(TAB2_TITLE));
assertThat(mModel.indexFromId(TAB1_ID), equalTo(POSITION1));
assertThat(mModel.indexFromId(TAB2_ID), equalTo(POSITION2));
mTabGroupModelFilterObserverCaptor.getValue().didMergeTabToGroup(mTab1, TAB2_ID);
assertThat(mModel.size(), equalTo(1));
assertThat(mModel.get(0).get(TabProperties.TAB_ID), equalTo(TAB2_ID));
assertThat(mModel.get(0).get(TabProperties.TITLE), equalTo(TAB2_TITLE));
}
@Test @Test
public void tabMovementWithoutGroup_Forward() { public void tabMovementWithoutGroup_Forward() {
initAndAssertAllProperties(); initAndAssertAllProperties();
...@@ -585,8 +640,8 @@ public class TabListMediatorUnitTest { ...@@ -585,8 +640,8 @@ public class TabListMediatorUnitTest {
mMediator = new TabListMediator(mModel, mTabModelSelector, mMediator = new TabListMediator(mModel, mTabModelSelector,
mTabContentManager::getTabThumbnailWithCallback, null, mTabListFaviconProvider, mTabContentManager::getTabThumbnailWithCallback, null, mTabListFaviconProvider,
false, null, null, getClass().getSimpleName()); true, null, null, getClass().getSimpleName());
initAndAssertAllProperties(); initAndAssertAllProperties();
} }
} }
\ No newline at end of file
...@@ -13,6 +13,7 @@ import org.chromium.chrome.browser.tabmodel.TabModel; ...@@ -13,6 +13,7 @@ import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver; import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver; import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tasks.tabgroup.TabGroupModelFilter;
import java.util.List; import java.util.List;
...@@ -39,6 +40,11 @@ public class TabCountProvider { ...@@ -39,6 +40,11 @@ public class TabCountProvider {
/** The {@link TabModelObserver} that observes when the tab count may have changed. */ /** The {@link TabModelObserver} that observes when the tab count may have changed. */
private TabModelObserver mTabModelFilterObserver; private TabModelObserver mTabModelFilterObserver;
/**
* The {@link TabGroupModelFilter.Observer} that observes when the tab count may have changed.
*/
private TabGroupModelFilter.Observer mTabGroupModelFilterObserver;
private int mTabCount; private int mTabCount;
private boolean mIsIncognito; private boolean mIsIncognito;
...@@ -136,6 +142,28 @@ public class TabCountProvider { ...@@ -136,6 +142,28 @@ public class TabCountProvider {
mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver( mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(
mTabModelFilterObserver); mTabModelFilterObserver);
mTabGroupModelFilterObserver = new TabGroupModelFilter.Observer() {
@Override
public void didMergeTabToGroup(Tab movedTab, int selectedTabIdInGroup) {
updateTabCount();
}
@Override
public void didMoveTabGroup(Tab movedTab, int tabModelOldIndex, int tabModelNewIndex) {}
@Override
public void didMoveWithinGroup(
Tab movedTab, int tabModelOldIndex, int tabModelNewIndex) {}
};
if (mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter()
instanceof TabGroupModelFilter) {
((TabGroupModelFilter) mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter())
.addTabGroupObserver(mTabGroupModelFilterObserver);
}
updateTabCount(); updateTabCount();
} }
...@@ -148,6 +176,14 @@ public class TabCountProvider { ...@@ -148,6 +176,14 @@ public class TabCountProvider {
mTabModelFilterObserver); mTabModelFilterObserver);
} }
if (mTabGroupModelFilterObserver != null
&& mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter()
instanceof TabGroupModelFilter) {
((TabGroupModelFilter) mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter())
.removeTabGroupObserver(mTabGroupModelFilterObserver);
}
if (mTabModelSelector != null) { if (mTabModelSelector != null) {
mTabModelSelector.removeObserver(mTabModelSelectorObserver); mTabModelSelector.removeObserver(mTabModelSelectorObserver);
mTabModelSelector = null; mTabModelSelector = null;
......
...@@ -6050,6 +6050,15 @@ should be able to be added at any place in this file. ...@@ -6050,6 +6050,15 @@ should be able to be added at any place in this file.
</description> </description>
</action> </action>
<action name="GridTabSwitcher.DropTabToMerge">
<owner>yusufo@chromium.org</owner>
<owner>wychen@chromium.org</owner>
<description>
User drags a tab and drops it on another tab to form a group in grid tab
switcher.
</description>
</action>
<action name="GridTabSwitcher.UndoCloseTabGroup"> <action name="GridTabSwitcher.UndoCloseTabGroup">
<owner>yusufo@chromium.org</owner> <owner>yusufo@chromium.org</owner>
<owner>wychen@chromium.org</owner> <owner>wychen@chromium.org</owner>
...@@ -20630,6 +20639,12 @@ should be able to be added at any place in this file. ...@@ -20630,6 +20639,12 @@ should be able to be added at any place in this file.
<description>Please enter the description of this user action.</description> <description>Please enter the description of this user action.</description>
</action> </action>
<action name="TabGrid.DragToReorder">
<owner>yusufo@chromium.org</owner>
<owner>wychen@chromium.org</owner>
<description>User drags a tab to reorder it.</description>
</action>
<action name="TabGridSheet.UndoCloseTab"> <action name="TabGridSheet.UndoCloseTab">
<owner>yusufo@chromium.org</owner> <owner>yusufo@chromium.org</owner>
<owner>wychen@chromium.org</owner> <owner>wychen@chromium.org</owner>
...@@ -22156,4 +22171,14 @@ should be able to be added at any place in this file. ...@@ -22156,4 +22171,14 @@ should be able to be added at any place in this file.
<affected-action name="MobileStackViewSwipeCloseTab"/> <affected-action name="MobileStackViewSwipeCloseTab"/>
</action-suffix> </action-suffix>
<action-suffix separator="." ordering="suffix">
<suffix name="GridTabSwitcher"
label="Users drag to reorder tabs in grid layout TabSwitcher."/>
<suffix name="TabGridDialog"
label="Users drag to reorder tabs in tab grid dialog."/>
<suffix name="TabGridSheet"
label="Users drag to reorder tabs in tab group bottom sheet."/>
<affected-action name="TabGrid.DragToReorder"/>
</action-suffix>
</actions> </actions>
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