Commit 327c6845 authored by Roberto Moura's avatar Roberto Moura Committed by Commit Bot

Show a plus sign button when the Thumb Strip's plus sign cell is hidden.

Create a plus sign button that sticks to right of the thumb strip. It
has a plus sign and a transparency gradient and is shown when the plus
sign cell is not visible.
This is done so that the user can open a new tab even if they haven't
scrolled to the end of the collection view. When the user scrolls the
collection view or when the toolbars are shown, check whether the user
has scrolled past the end of the scroll view and, if so, hide the plus
sign button. Show it otherwise.

Rename one of the LayoutSwitcherStates from "Full" to "Grid".

Add the constats that specify the new thumb strip plus sign button's
dimensions to the grid constants file.

Add a |didChangeLastItemVisibility| method to the grid view controller
delegate. Create a CGFloat property |fractionVisibleOflastItem| inside
the grid view controller, which indicates the visibility fraction of the
last item. Only call |didChangeLastItemVisibility| on the delegate if
the value of |fractionVisibleOflastItem| changes.

Bug: 1094335
Change-Id: I056d6d0ece4eb22f84e4904208bd0e83d9b93389
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2485167
Commit-Queue: Roberto Moura <mouraroberto@google.com>
Auto-Submit: Roberto Moura <mouraroberto@google.com>
Reviewed-by: default avatarGauthier Ambard <gambard@chromium.org>
Reviewed-by: default avataredchin <edchin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#824659}
parent a354722e
......@@ -9,7 +9,7 @@
// pan handler class.
enum class LayoutSwitcherState {
Horizontal, // The view layout when the view is a horizontal strip.
Full, // The view layout when the view is at its full size.
Grid, // The view layout when the view is a grid of elements.
};
// Interface to manage interactive animated transitions of layout.
......
......@@ -184,7 +184,7 @@ const CGFloat kAnimationDuration = 0.25f;
}
if (self.nextState == ViewRevealState::Revealed) {
[self willTransitionToLayout:LayoutSwitcherState::Full];
[self willTransitionToLayout:LayoutSwitcherState::Grid];
} else if (self.currentState == ViewRevealState::Revealed &&
(self.nextState == ViewRevealState::Peeked ||
self.nextState == ViewRevealState::Hidden)) {
......
......@@ -186,7 +186,7 @@ TEST_F(ViewRevealingVerticalPanHandlerTest, DetectPan) {
// should transition to full state.
SimulatePanGesture(pan_handler, remaining_height * kRevealThreshold);
EXPECT_EQ(ViewRevealState::Revealed, fake_animatee.state);
EXPECT_EQ(LayoutSwitcherState::Full, fake_layout_switcher.state);
EXPECT_EQ(LayoutSwitcherState::Grid, fake_layout_switcher.state);
// Simulate a pan gesture from Revealed state to Peeked state. The layout
// should transition back to horizontal state.
......@@ -224,7 +224,7 @@ TEST_F(ViewRevealingVerticalPanHandlerTest, ManualStateChange) {
[pan_handler setState:ViewRevealState::Revealed animated:NO];
EXPECT_EQ(ViewRevealState::Revealed, fake_animatee.state);
EXPECT_EQ(LayoutSwitcherState::Full, fake_layout_switcher.state);
EXPECT_EQ(LayoutSwitcherState::Grid, fake_layout_switcher.state);
[pan_handler setState:ViewRevealState::Hidden animated:NO];
EXPECT_EQ(ViewRevealState::Hidden, fake_animatee.state);
......
......@@ -92,6 +92,8 @@ source_set("tab_grid_ui") {
"tab_grid_top_toolbar.mm",
"tab_grid_view_controller.h",
"tab_grid_view_controller.mm",
"thumb_strip_plus_sign_button.h",
"thumb_strip_plus_sign_button.mm",
]
configs += [ "//build/config/compiler:enable_arc" ]
......
......@@ -89,4 +89,15 @@ extern const CGFloat kGridCellIconDiameter;
extern const CGFloat kGridCellSelectionRingGapWidth;
extern const CGFloat kGridCellSelectionRingTintWidth;
// Horizontal distance from the center of the plus sign image to the trailing of
// the tab grid.
extern const CGFloat kPlusSignImageTrailingCenterDistance;
// Threshold after which the thumb strip's plus sign button should be hidden.
extern const CGFloat kScrollThresholdForPlusSignButtonHide;
// Vertical distance from the center of the plus sign image and the top of the
// tab grid.
extern const CGFloat kPlusSignImageYCenterConstant;
// With of the plus sign button.
extern const CGFloat kPlusSignButtonWidth;
#endif // IOS_CHROME_BROWSER_UI_TAB_GRID_GRID_GRID_CONSTANTS_H_
......@@ -84,3 +84,26 @@ const CGFloat kGridCellTitleLabelContentInset = 4.0f;
const CGFloat kGridCellIconDiameter = 16.0f;
const CGFloat kGridCellSelectionRingGapWidth = 2.0f;
const CGFloat kGridCellSelectionRingTintWidth = 5.0f;
// The plus sign image should be at the center of a view with width equal to
// kGridCellSizeSmall.width / 2.
const CGFloat kPlusSignImageTrailingCenterDistance =
kGridCellSizeSmall.width / 4;
// The threshold is set so that the hide transition starts at the moment the
// plus sign image from the button and the one from the plus sign cell coincide
// in position.
const CGFloat kScrollThresholdForPlusSignButtonHide =
kPlusSignImageTrailingCenterDistance -
kGridLayoutLineSpacingCompactCompactLimitedWidth -
kGridCellSizeSmall.width / 2;
const CGFloat kPlusSignImageYCenterConstant =
kGridLayoutLineSpacingCompactCompactLimitedWidth +
kGridCellSelectionRingGapWidth + kGridCellSelectionRingTintWidth +
kGridCellSizeSmall.height / 2 - 1.5f;
// The width is set so that when the scrollThresholdForPlusSignButtonHide is
// attained by the user scroll, the plus sign button does not overlap with
// second cell from right to left (the first being the plus sign cell)
const CGFloat kPlusSignButtonWidth =
kGridCellSizeSmall.width / 2 +
kGridLayoutLineSpacingCompactCompactLimitedWidth +
kPlusSignImageTrailingCenterDistance;
......@@ -40,6 +40,11 @@
// changed to |count|.
- (void)gridViewController:(GridViewController*)gridViewController
didChangeItemCount:(NSUInteger)count;
// Tells the delegate that the visibility of the last item of the grid changed.
- (void)didChangeLastItemVisibilityInGridViewController:
(GridViewController*)gridViewController;
@end
// A view controller that contains a grid of items.
......@@ -64,6 +69,8 @@
// YES if the gid should show cell selection updates. This would be set to NO,
// for example, if the grid was about to be transitioned out of.
@property(nonatomic, assign) BOOL showsSelectionUpdates;
// The fraction of the last item of the grid that is visible.
@property(nonatomic, assign, readonly) CGFloat fractionVisibleOfLastItem;
// Returns the layout of the grid for use in an animated transition.
- (GridTransitionLayout*)transitionLayout;
......
......@@ -11,6 +11,7 @@
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/notreached.h"
#include "base/numerics/ranges.h"
#import "base/numerics/safe_conversions.h"
#include "ios/chrome/browser/drag_and_drop/drag_and_drop_flag.h"
#include "ios/chrome/browser/procedural_block_types.h"
......@@ -87,6 +88,9 @@ NSIndexPath* CreateIndexPath(NSInteger index) {
@property(nonatomic, strong) UICollectionViewLayout* horizontalReorderingLayout;
// YES if, when reordering is enabled, the order of the cells has changed.
@property(nonatomic, assign) BOOL hasChangedOrder;
// By how much the user scrolled past the view's content size. A negative value
// means the user hasn't scrolled past the end of the scroll view.
@property(nonatomic, assign, readonly) CGFloat offsetPastEndOfScrollView;
#if defined(__IPHONE_13_4)
// Cells for which pointer interactions have been added. Pointer interactions
// should only be added to displayed cells (not transition cells). This is only
......@@ -577,6 +581,14 @@ NSIndexPath* CreateIndexPath(NSInteger index) {
self.emptyStateView.scrollViewContentInsets = scrollView.contentInset;
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
if (!IsThumbStripEnabled())
return;
CGFloat offset = self.offsetPastEndOfScrollView;
self.fractionVisibleOfLastItem = base::ClampToRange<CGFloat>(
1 - offset / kScrollThresholdForPlusSignButtonHide, 0, 1);
}
#pragma mark - GridCellDelegate
- (void)closeButtonTappedForCell:(GridCell*)cell {
......@@ -761,7 +773,7 @@ NSIndexPath* CreateIndexPath(NSInteger index) {
case LayoutSwitcherState::Horizontal:
nextLayout = self.horizontalLayout;
break;
case LayoutSwitcherState::Full:
case LayoutSwitcherState::Grid:
nextLayout = self.gridLayout;
break;
}
......@@ -797,6 +809,35 @@ NSIndexPath* CreateIndexPath(NSInteger index) {
return [self indexOfItemWithID:self.selectedItemID];
}
- (CGFloat)offsetPastEndOfScrollView {
CGFloat offset;
if (self.currentLayout == self.horizontalLayout) {
offset = self.collectionView.contentOffset.x +
self.collectionView.frame.size.width -
self.collectionView.contentSize.width;
} else {
DCHECK_EQ(self.gridLayout, self.currentLayout);
offset = self.collectionView.contentOffset.y +
self.collectionView.frame.size.height -
self.collectionView.contentSize.height;
}
return offset;
}
- (void)setFractionVisibleOfLastItem:(CGFloat)fractionVisibleOfLastItem {
if (fractionVisibleOfLastItem == _fractionVisibleOfLastItem)
return;
_fractionVisibleOfLastItem = fractionVisibleOfLastItem;
if (self.currentLayout == self.horizontalLayout) {
[self.delegate didChangeLastItemVisibilityInGridViewController:self];
} else {
DCHECK_EQ(self.gridLayout, self.currentLayout);
// No-op because behaviour of the tab grid's bottom toolbar when the plus
// sign cell is visible hasn't been decided yet. TODO(crbug.com/1146130)
}
}
#pragma mark - Private
// Checks whether |indexPath| corresponds to the index path of the plus sign
......
......@@ -56,6 +56,10 @@
// No-op for unittests. This is only called when a user taps on a
// plus sign cell, not generically when items are added to the data source.
}
- (void)didChangeLastItemVisibilityInGridViewController:
(GridViewController*)gridViewController {
// No-op for unittests.
}
@end
class GridViewControllerTest : public RootViewControllerTest {
......
......@@ -56,4 +56,8 @@ extern const int64_t kTabGridScrollAnimationDelayInMilliseconds;
// animation of the thumb strip reveal transition.
extern const CGFloat kThumbStripSlideInHeight;
// The distance travelled by the thumb strip's plus sign button during the
// slide-out animation of the transition from Peeked to Revealed state.
extern const CGFloat kThumbStripPlusSignButtonSlideOutDistance;
#endif // IOS_CHROME_BROWSER_UI_TAB_GRID_TAB_GRID_CONSTANTS_H_
......@@ -58,3 +58,6 @@ const int64_t kTabGridScrollAnimationDelayInMilliseconds = 300;
// The distance travelled by the thumb strip thumbnails during the slide-in
// animation of the thumb strip reveal transition.
const CGFloat kThumbStripSlideInHeight = 40.0f;
// The distance travelled by the thumb strip's plus sign button during the
// slide-out animation of the transition from Peeked to Revealed state.
const CGFloat kThumbStripPlusSignButtonSlideOutDistance = 400.0f;
......@@ -26,6 +26,7 @@
#import "ios/chrome/browser/ui/tab_grid/tab_grid_new_tab_button.h"
#import "ios/chrome/browser/ui/tab_grid/tab_grid_page_control.h"
#import "ios/chrome/browser/ui/tab_grid/tab_grid_top_toolbar.h"
#import "ios/chrome/browser/ui/tab_grid/thumb_strip_plus_sign_button.h"
#import "ios/chrome/browser/ui/tab_grid/transitions/grid_transition_layout.h"
#import "ios/chrome/browser/ui/table_view/chrome_table_view_styler.h"
#import "ios/chrome/browser/ui/thumb_strip/thumb_strip_feature.h"
......@@ -143,6 +144,9 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
// UIView whose background color changes to create a fade-in / fade-out effect
// when revealing / hiding the Thumb Strip.
@property(nonatomic, weak) UIView* foregroundView;
// Button with a plus sign that opens a new tab, located on the right side of
// the thumb strip, shown when the plus sign cell isn't visible.
@property(nonatomic, weak) ThumbStripPlusSignButton* plusSignButton;
@end
@implementation TabGridViewController
......@@ -174,6 +178,7 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
[self setupTopToolbar];
[self setupBottomToolbar];
if (IsThumbStripEnabled()) {
[self setupThumbStripPlusSignButton];
[self setupForegroundView];
}
......@@ -442,12 +447,14 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
#pragma mark - LayoutSwitcherProvider
- (id<LayoutSwitcher>)layoutSwitcher {
DCHECK_NE(TabGridPageRemoteTabs, self.activePage);
return [self gridViewControllerForPage:self.activePage];
}
#pragma mark - ViewRevealingAnimatee
- (void)willAnimateViewReveal:(ViewRevealState)currentViewRevealState {
DCHECK_NE(TabGridPageRemoteTabs, self.currentPage);
self.scrollView.scrollEnabled = NO;
switch (currentViewRevealState) {
case ViewRevealState::Hidden:
......@@ -459,36 +466,45 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
case ViewRevealState::Peeked:
break;
case ViewRevealState::Revealed:
self.plusSignButton.alpha = 0;
break;
}
}
- (void)animateViewReveal:(ViewRevealState)nextViewRevealState {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
DCHECK(gridViewController);
switch (nextViewRevealState) {
case ViewRevealState::Hidden:
self.foregroundView.alpha = 1;
self.topToolbar.transform = CGAffineTransformMakeTranslation(
0, [self hiddenTopToolbarYTranslation]);
[self gridViewControllerForPage:self.currentPage].gridView.transform =
gridViewController.gridView.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
self.topToolbar.alpha = 0;
[self showPlusSignButtonWithAlpha:1 - gridViewController
.fractionVisibleOfLastItem];
self.plusSignButton.transform =
CGAffineTransformMakeTranslation(0, kThumbStripSlideInHeight);
break;
case ViewRevealState::Peeked:
self.foregroundView.alpha = 0;
self.topToolbar.transform = CGAffineTransformMakeTranslation(
0, [self hiddenTopToolbarYTranslation]);
[self gridViewControllerForPage:self.currentPage].gridView.transform =
CGAffineTransformIdentity;
gridViewController.gridView.transform = CGAffineTransformIdentity;
self.topToolbar.alpha = 0;
[self showPlusSignButtonWithAlpha:1 - gridViewController
.fractionVisibleOfLastItem];
break;
case ViewRevealState::Revealed:
self.foregroundView.alpha = 0;
self.topToolbar.transform = CGAffineTransformIdentity;
[self gridViewControllerForPage:self.currentPage].gridView.transform =
CGAffineTransformMakeTranslation(
0, self.topToolbar.intrinsicContentSize.height);
gridViewController.gridView.transform = CGAffineTransformMakeTranslation(
0, self.topToolbar.intrinsicContentSize.height);
[self contentWillAppearAnimated:YES];
self.topToolbar.alpha = 1;
[self hidePlusSignButton];
break;
}
}
......@@ -501,6 +517,21 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
#pragma mark - Private
// Hides the thumb strip's plus sign button by translating it away and making it
// transparent.
- (void)hidePlusSignButton {
self.plusSignButton.transform = CGAffineTransformMakeTranslation(
kThumbStripPlusSignButtonSlideOutDistance, 0);
self.plusSignButton.alpha = 0;
}
// Show the thumb strip's plus sign button by translating it back into position
// and setting its alpha to |opacity|.
- (void)showPlusSignButtonWithAlpha:(CGFloat)opacity {
self.plusSignButton.transform = CGAffineTransformIdentity;
self.plusSignButton.alpha = opacity;
}
// Returns the ammount by which the top toolbar should be translated in the y
// direction when hidden. Used for the slide-in animation.
- (CGFloat)hiddenTopToolbarYTranslation {
......@@ -910,6 +941,29 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
AddSameConstraints(foregroundView, self.view);
}
// Adds the thumb strip's plus sign button, which is visible when the plus sign
// cell isn't.
- (void)setupThumbStripPlusSignButton {
ThumbStripPlusSignButton* plusSignButton =
[[ThumbStripPlusSignButton alloc] init];
self.plusSignButton = plusSignButton;
plusSignButton.translatesAutoresizingMaskIntoConstraints = NO;
[plusSignButton addTarget:self
action:@selector(newTabButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:plusSignButton];
NSArray* constraints = @[
[plusSignButton.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[plusSignButton.bottomAnchor
constraintEqualToAnchor:self.view.bottomAnchor],
[plusSignButton.trailingAnchor
constraintEqualToAnchor:self.view.trailingAnchor],
[plusSignButton.widthAnchor constraintEqualToConstant:kPlusSignButtonWidth],
];
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)configureViewControllerForCurrentSizeClassesAndPage {
self.configuration = TabGridConfigurationFloatingButton;
if (self.traitCollection.verticalSizeClass ==
......@@ -987,6 +1041,18 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
// animations.
- (void)showToolbars {
[self.topToolbar show];
if (IsThumbStripEnabled()) {
GridViewController* gridViewController =
[self gridViewControllerForPage:self.currentPage];
DCHECK(gridViewController);
if (gridViewController.fractionVisibleOfLastItem >= 0.999) {
// Don't show the bottom new tab button because the plus sign cell is
// visible.
return;
}
self.plusSignButton.alpha =
1 - gridViewController.fractionVisibleOfLastItem;
}
[self.bottomToolbar show];
}
......@@ -1313,6 +1379,17 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
[self broadcastIncognitoContentVisibility];
}
- (void)didChangeLastItemVisibilityInGridViewController:
(GridViewController*)gridViewController {
CGFloat lastItemVisiblity = gridViewController.fractionVisibleOfLastItem;
self.plusSignButton.alpha = 1 - lastItemVisiblity;
self.plusSignButton.plusSignImage.transform =
lastItemVisiblity < 1
? CGAffineTransformMakeTranslation(
lastItemVisiblity * kScrollThresholdForPlusSignButtonHide, 0)
: CGAffineTransformIdentity;
}
#pragma mark - Control actions
- (void)doneButtonTapped:(id)sender {
......
// Copyright 2020 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.
#ifndef IOS_CHROME_BROWSER_UI_TAB_GRID_THUMB_STRIP_PLUS_SIGN_BUTTON_H_
#define IOS_CHROME_BROWSER_UI_TAB_GRID_THUMB_STRIP_PLUS_SIGN_BUTTON_H_
#import <UIKit/UIKit.h>
// The button that sticks to the right of the screen when the thumb strip is
// visible. It has a plus sign and a transparency gradient.
@interface ThumbStripPlusSignButton : UIButton
// The image view with a plus sign.
@property(nonatomic, strong) UIImageView* plusSignImage;
@end
#endif // IOS_CHROME_BROWSER_UI_TAB_GRID_THUMB_STRIP_PLUS_SIGN_BUTTON_H_
// Copyright 2020 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.
#import "ios/chrome/browser/ui/tab_grid/thumb_strip_plus_sign_button.h"
#import "ios/chrome/browser/ui/tab_grid/grid/grid_constants.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface ThumbStripPlusSignButton ()
// The transparency gradient of the button.
@property(nonatomic, strong) CAGradientLayer* gradient;
@end
@implementation ThumbStripPlusSignButton
- (void)didMoveToSuperview {
[super didMoveToSuperview];
if (self.subviews.count > 0)
return;
UIImageView* plusSignImage = [[UIImageView alloc]
initWithImage:[UIImage imageNamed:@"grid_cell_plus_sign"]];
self.plusSignImage = plusSignImage;
plusSignImage.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:plusSignImage];
NSArray* constraints = @[
[plusSignImage.centerXAnchor
constraintEqualToAnchor:self.trailingAnchor
constant:-kPlusSignImageTrailingCenterDistance],
[plusSignImage.centerYAnchor
constraintEqualToAnchor:self.topAnchor
constant:kPlusSignImageYCenterConstant],
];
[NSLayoutConstraint activateConstraints:constraints];
}
#pragma mark - UIView
- (void)layoutSubviews {
[super layoutSubviews];
if (self.gradient)
[self.gradient removeFromSuperlayer];
CAGradientLayer* gradient = [CAGradientLayer layer];
self.gradient = gradient;
gradient.frame = self.bounds;
gradient.colors =
@[ (id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor ];
gradient.startPoint = CGPointMake(0.0, 0.5);
gradient.endPoint = CGPointMake(1.0, 0.5);
gradient.locations = @[ @0, @0.5 ];
[self.layer insertSublayer:gradient atIndex:0];
}
@end
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