Commit 602d2720 authored by Mark Cogan's avatar Mark Cogan Committed by Commit Bot

[iOS] Merge tab grid transitions with tab views.

This CL updates the tab grid transitions to cleanly match the presented tab views.

The basic abstraction for this transition is the GridToTabTransitionView protocol which defines a set of 'cell' and 'tab' views, and methods to lay them out. The general idea is that a view conforming to this protocol can either be in 'cell' mode (where it exactly matches a tab grid cell), or in 'tab' mode (where it exactly matches an open tab).

Most of the work is done by the GridTransitionCell subclass of GridCell, which positions and scales the "tab" and "cell" views it is supplied with so they animate smoothly. All of this positioning/scaling logic is internal to that class and nothing else depends on it.

Providing the transition cell with the tab views is handled in the transition animator, mostly leveraging the content area layout guide. This doesn't require any direct integration with the tab view (that is, the BVC) beyond that layout guide.

Approaches I abandoned:

- letting -layoutSubviews position the tab/grid views. The problem with this approach (which is much cleaner from an API perspective) is that the timing of the layout passes is unpredictable, it it resulted in changes being made that weren't fully animated (for example, subviews would be scaled and positioned without animation, and then the superview would scale with an animation).

- using layout constraints for GridTransitionCell's tab views. This is probably the correct thing to do long-term; mostly this was a decision made under time constraints, but since ultimately animating constraint changes depends on -layoutSubviews being called at the right times, I did have some concerns given the difficulties described above. It's unfortunate that grid_cell.mm is mixing constraint- and frame-based layout, but it is confined to that single file.

Bug: 851460, 850507, 862343
Cq-Include-Trybots: luci.chromium.try:ios-simulator-full-configs;master.tryserver.chromium.mac:ios-simulator-cronet
Change-Id: If6d55f14aa05e705143547b61773fdb34796ad2d
Reviewed-on: https://chromium-review.googlesource.com/1136647
Commit-Queue: Mark Cogan <marq@chromium.org>
Reviewed-by: default avatarGauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/master@{#575590}
parent 0922d466
...@@ -15,7 +15,33 @@ ...@@ -15,7 +15,33 @@
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
namespace {
// Frame-based layout utilities for GridTransitionCell.
// Scales the size of |view|'s frame by |factor| in both height and width. This
// scaling is done by changing the frame size without changing its origin,
// unlike a scale transform which scales around the view's center.
void ScaleView(UIView* view, CGFloat factor) {
if (!view)
return;
CGRect frame = view.frame;
frame.size.width *= factor;
frame.size.height *= factor;
view.frame = frame;
}
// Positions |view| by setting its frame's origin to |point|.
void PositionView(UIView* view, CGPoint point) {
if (!view)
return;
CGRect frame = view.frame;
frame.origin = point;
view.frame = frame;
}
} // namespace
@interface GridCell () @interface GridCell ()
// Header height of the cell.
@property(nonatomic, strong) NSLayoutConstraint* topBarHeight;
// Visual components of the cell. // Visual components of the cell.
@property(nonatomic, weak) UIView* topBar; @property(nonatomic, weak) UIView* topBar;
@property(nonatomic, weak) UIImageView* iconView; @property(nonatomic, weak) UIImageView* iconView;
...@@ -36,8 +62,9 @@ ...@@ -36,8 +62,9 @@
@synthesize icon = _icon; @synthesize icon = _icon;
@synthesize snapshot = _snapshot; @synthesize snapshot = _snapshot;
@synthesize title = _title; @synthesize title = _title;
@synthesize topBar = _topBar;
// Private properties. // Private properties.
@synthesize topBarHeight = _topBarHeight;
@synthesize topBar = _topBar;
@synthesize iconView = _iconView; @synthesize iconView = _iconView;
@synthesize snapshotView = _snapshotView; @synthesize snapshotView = _snapshotView;
@synthesize titleLabel = _titleLabel; @synthesize titleLabel = _titleLabel;
...@@ -74,12 +101,15 @@ ...@@ -74,12 +101,15 @@
_snapshotView = snapshotView; _snapshotView = snapshotView;
_closeTapTargetButton = closeTapTargetButton; _closeTapTargetButton = closeTapTargetButton;
_topBarHeight =
[topBar.heightAnchor constraintEqualToConstant:kGridCellHeaderHeight];
NSArray* constraints = @[ NSArray* constraints = @[
[topBar.topAnchor constraintEqualToAnchor:contentView.topAnchor], [topBar.topAnchor constraintEqualToAnchor:contentView.topAnchor],
[topBar.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor], [topBar.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor],
[topBar.trailingAnchor [topBar.trailingAnchor
constraintEqualToAnchor:contentView.trailingAnchor], constraintEqualToAnchor:contentView.trailingAnchor],
[topBar.heightAnchor constraintEqualToConstant:kGridCellHeaderHeight], _topBarHeight,
[snapshotView.topAnchor constraintEqualToAnchor:topBar.bottomAnchor], [snapshotView.topAnchor constraintEqualToAnchor:topBar.bottomAnchor],
[snapshotView.leadingAnchor [snapshotView.leadingAnchor
constraintEqualToAnchor:contentView.leadingAnchor], constraintEqualToAnchor:contentView.leadingAnchor],
...@@ -303,7 +333,15 @@ ...@@ -303,7 +333,15 @@
@end @end
@implementation GridTransitionCell @implementation GridTransitionCell {
// Previous tab view width, used to scale the tab views.
CGFloat _previousTabViewWidth;
}
// Synthesis of GridToTabTransitionView properties.
@synthesize topTabView = _topTabView;
@synthesize mainTabView = _mainTabView;
@synthesize bottomTabView = _bottomTabView;
+ (instancetype)transitionCellFromCell:(GridCell*)cell { + (instancetype)transitionCellFromCell:(GridCell*)cell {
GridTransitionCell* proxy = [[self alloc] initWithFrame:cell.bounds]; GridTransitionCell* proxy = [[self alloc] initWithFrame:cell.bounds];
...@@ -314,11 +352,10 @@ ...@@ -314,11 +352,10 @@
proxy.title = cell.title; proxy.title = cell.title;
return proxy; return proxy;
} }
#pragma mark - GridToTabTransitionView properties.
#pragma mark - GridToTabTransitionView
- (void)setTopCellView:(UIView*)topCellView { - (void)setTopCellView:(UIView*)topCellView {
// The top cell is the top bar and can't be changed. // The top cell view is |topBar| and can't be changed.
NOTREACHED(); NOTREACHED();
} }
...@@ -326,6 +363,37 @@ ...@@ -326,6 +363,37 @@
return self.topBar; return self.topBar;
} }
- (void)setTopTabView:(UIView*)topTabView {
DCHECK(!_topTabView) << "topTabView should only be set once.";
if (!topTabView.superview)
[self.contentView addSubview:topTabView];
_topTabView = topTabView;
}
- (void)setMainCellView:(UIView*)mainCellView {
// The main cell view is the snapshot view and can't be changed.
NOTREACHED();
}
- (UIView*)mainCellView {
return self.snapshotView;
}
- (void)setMainTabView:(UIView*)mainTabView {
DCHECK(!_mainTabView) << "mainTabView should only be set once.";
if (!mainTabView.superview)
[self.contentView addSubview:mainTabView];
_previousTabViewWidth = mainTabView.frame.size.width;
_mainTabView = mainTabView;
}
- (void)setBottomTabView:(UIView*)bottomTabView {
DCHECK(!_bottomTabView) << "bottomTabView should only be set once.";
if (!bottomTabView.superview)
[self.contentView addSubview:bottomTabView];
_bottomTabView = bottomTabView;
}
- (CGFloat)cornerRadius { - (CGFloat)cornerRadius {
return self.contentView.layer.cornerRadius; return self.contentView.layer.cornerRadius;
} }
...@@ -334,4 +402,58 @@ ...@@ -334,4 +402,58 @@
self.contentView.layer.cornerRadius = radius; self.contentView.layer.cornerRadius = radius;
} }
#pragma mark - GridToTabTransitionView methods
- (void)positionTabViews {
[self scaleTabViews];
self.topBarHeight.constant = self.topTabView.frame.size.height;
[self setNeedsUpdateConstraints];
[self layoutIfNeeded];
PositionView(self.topTabView, CGPointMake(0, 0));
// Position the main view so it's top-aligned with the main cell view.
PositionView(self.mainTabView, self.mainCellView.frame.origin);
if (!self.bottomTabView)
return;
// Position the bottom tab view at the bottom.
CGFloat yPosition = CGRectGetMaxY(self.contentView.bounds) -
self.bottomTabView.frame.size.height;
PositionView(self.bottomTabView, CGPointMake(0, yPosition));
}
- (void)positionCellViews {
[self scaleTabViews];
self.topBarHeight.constant = kGridCellHeaderHeight;
[self setNeedsUpdateConstraints];
[self layoutIfNeeded];
CGFloat yOffset = kGridCellHeaderHeight - self.topTabView.frame.size.height;
PositionView(self.topTabView, CGPointMake(0, yOffset));
// Position the main view so it's top-aligned with the main cell view.
PositionView(self.mainTabView, self.mainCellView.frame.origin);
if (!self.bottomTabView)
return;
if (self.bottomTabView.frame.origin.y > 0) {
// Position the bottom tab so it's equivalently located.
CGFloat scale = self.bounds.size.width / _previousTabViewWidth;
PositionView(self.bottomTabView,
CGPointMake(0, self.bottomTabView.frame.origin.y * scale));
} else {
// Position the bottom tab view below the main content view.
CGFloat yOffset = CGRectGetMaxY(self.mainCellView.frame);
PositionView(self.bottomTabView, CGPointMake(0, yOffset));
}
}
#pragma mark - Private helper methods
// Scales the tab views relative to the current width of the cell.
- (void)scaleTabViews {
CGFloat scale = self.bounds.size.width / _previousTabViewWidth;
ScaleView(self.topTabView, scale);
ScaleView(self.mainTabView, scale);
ScaleView(self.bottomTabView, scale);
_previousTabViewWidth = self.mainTabView.frame.size.width;
}
@end @end
...@@ -137,6 +137,10 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) { ...@@ -137,6 +137,10 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
[self setupTopToolbar]; [self setupTopToolbar];
[self setupBottomToolbar]; [self setupBottomToolbar];
[self setupFloatingButton]; [self setupFloatingButton];
// Hide the toolbars and the floating button, so they can fade in the first
// time there's a transition into this view controller.
[self hideToolbars];
} }
- (void)viewWillAppear:(BOOL)animated { - (void)viewWillAppear:(BOOL)animated {
...@@ -702,18 +706,20 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) { ...@@ -702,18 +706,20 @@ NSUInteger GetPageIndexFromPage(TabGridPage page) {
kTabGridCloseAllButtonIdentifier; kTabGridCloseAllButtonIdentifier;
} }
// Shows (by setting the alpha to 1.0) the two toolbar views. Suitable for use // Shows (by setting the alpha to 1.0) the two toolbar views and the floating
// in animations. // button. Suitable for use in animations.
- (void)showToolbars { - (void)showToolbars {
self.topToolbar.alpha = 1.0; self.topToolbar.alpha = 1.0;
self.bottomToolbar.alpha = 1.0; self.bottomToolbar.alpha = 1.0;
self.floatingButton.alpha = 1.0;
} }
// Hides (by setting the alpha to 0.0) the two toolbar views. Suitable for use // Hides (by setting the alpha to 0.0) the two toolbar views and the floating
// in animations. // button. Suitable for use in animations.
- (void)hideToolbars { - (void)hideToolbars {
self.topToolbar.alpha = 0.0; self.topToolbar.alpha = 0.0;
self.bottomToolbar.alpha = 0.0; self.bottomToolbar.alpha = 0.0;
self.floatingButton.alpha = 0.0;
} }
// Translates the toolbar views offscreen and then animates them back in using // Translates the toolbar views offscreen and then animates them back in using
......
...@@ -7,18 +7,36 @@ ...@@ -7,18 +7,36 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
// A protocol to be adopted by a view that will be provided for the transition // An collection of properties and methods a view must support in order to be
// animation. This view will need to be animated between 'cell' and 'tab' // used to animate the transition between a grid cell and a browser tab.
// states, and will need to have a number of properties available for that
// purpose.
@protocol GridToTabTransitionView @protocol GridToTabTransitionView
// The subview at the top of the view in 'cell' state. // The subview at the top of the view in 'cell' state.
@property(nonatomic, strong) UIView* topCellView; @property(nonatomic, strong) UIView* topCellView;
// The subview at the top of the view in 'tab' state.
@property(nonatomic, strong) UIView* topTabView;
// The subview containing the main content in 'cell' state.
@property(nonatomic, strong) UIView* mainCellView;
// The subview containing the main content in 'tab' state.
@property(nonatomic, strong) UIView* mainTabView;
// The subview at the bottom of the view in 'tab' state.
@property(nonatomic, strong) UIView* bottomTabView;
// The corner radius of the view. // The corner radius of the view.
@property(nonatomic) CGFloat cornerRadius; @property(nonatomic) CGFloat cornerRadius;
// Tells the view to scale and position its subviews for the "tab" layout. This
// must be able to be called inside an animation block.
- (void)positionTabViews;
// Tells the view to scale and position its subviews for the "cell" layout. This
// must be able to be called inside an animation block.
- (void)positionCellViews;
@end @end
#endif // IOS_CHROME_BROWSER_UI_TAB_GRID_TRANSITIONS_GRID_TO_TAB_TRANSITION_VIEW_H_ #endif // IOS_CHROME_BROWSER_UI_TAB_GRID_TRANSITIONS_GRID_TO_TAB_TRANSITION_VIEW_H_
...@@ -85,8 +85,12 @@ ...@@ -85,8 +85,12 @@
CGRect finalRect = CGRect finalRect =
[NamedGuide guideWithName:kContentAreaGuide view:viewWithNamedGuides] [NamedGuide guideWithName:kContentAreaGuide view:viewWithNamedGuides]
.layoutFrame; .layoutFrame;
layout.expandedRect =
[proxyContainer convertRect:finalRect fromView:viewWithNamedGuides]; [layout.activeItem populateWithSnapshotsFromView:viewWithNamedGuides
middleRect:finalRect];
layout.expandedRect = [proxyContainer convertRect:viewWithNamedGuides.frame
fromView:presentedView];
NSTimeInterval duration = [self transitionDuration:transitionContext]; NSTimeInterval duration = [self transitionDuration:transitionContext];
// Create the animation view and insert it. // Create the animation view and insert it.
......
...@@ -76,6 +76,11 @@ ...@@ -76,6 +76,11 @@
center:(CGPoint)center center:(CGPoint)center
size:(CGSize)size; size:(CGSize)size;
// Populate the |cell| view of the reciever by extracting snapshots from |view|,
// using |rect| to define (in |view|'s coordinates) the main tab view, with any
// space above and below |rect| being the top and bottom tab views.
- (void)populateWithSnapshotsFromView:(UIView*)view middleRect:(CGRect)rect;
@end @end
#endif // IOS_CHROME_BROWSER_UI_TAB_GRID_TRANSITIONS_GRID_TRANSITION_LAYOUT_H_ #endif // IOS_CHROME_BROWSER_UI_TAB_GRID_TRANSITIONS_GRID_TRANSITION_LAYOUT_H_
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
#import "ios/chrome/browser/ui/tab_grid/transitions/grid_transition_layout.h" #import "ios/chrome/browser/ui/tab_grid/transitions/grid_transition_layout.h"
#import "ios/chrome/browser/ui/tab_grid/transitions/grid_to_tab_transition_view.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
...@@ -71,4 +73,32 @@ ...@@ -71,4 +73,32 @@
return item; return item;
} }
- (void)populateWithSnapshotsFromView:(UIView*)view middleRect:(CGRect)rect {
self.cell.mainTabView = [view resizableSnapshotViewFromRect:rect
afterScreenUpdates:YES
withCapInsets:UIEdgeInsetsZero];
CGSize viewSize = view.bounds.size;
if (rect.origin.y > 0) {
// |rect| starts below the top of |view|, so section off the top part of
// |view|.
CGRect topRect = CGRectMake(0, 0, viewSize.width, rect.origin.y);
self.cell.topTabView =
[view resizableSnapshotViewFromRect:topRect
afterScreenUpdates:YES
withCapInsets:UIEdgeInsetsZero];
}
CGFloat middleRectBottom = CGRectGetMaxY(rect);
if (middleRectBottom < viewSize.height) {
// |rect| ends above the bottom of |view|, so section off the bottom part of
// |view|.
CGFloat bottomHeight = viewSize.height - middleRectBottom;
CGRect bottomRect =
CGRectMake(0, middleRectBottom, viewSize.width, bottomHeight);
self.cell.bottomTabView =
[view resizableSnapshotViewFromRect:bottomRect
afterScreenUpdates:YES
withCapInsets:UIEdgeInsetsZero];
}
}
@end @end
...@@ -87,8 +87,12 @@ ...@@ -87,8 +87,12 @@
CGRect initialRect = CGRect initialRect =
[NamedGuide guideWithName:kContentAreaGuide view:viewWithNamedGuides] [NamedGuide guideWithName:kContentAreaGuide view:viewWithNamedGuides]
.layoutFrame; .layoutFrame;
layout.expandedRect =
[proxyContainer convertRect:initialRect fromView:viewWithNamedGuides]; [layout.activeItem populateWithSnapshotsFromView:viewWithNamedGuides
middleRect:initialRect];
layout.expandedRect = [proxyContainer convertRect:viewWithNamedGuides.frame
fromView:dismissingView];
NSTimeInterval duration = [self transitionDuration:transitionContext]; NSTimeInterval duration = [self transitionDuration:transitionContext];
// Create the animation view and insert it. // Create the animation view and insert it.
......
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