0

[iOS][Thumb Strip] Open/close thumb strip when scrolling in web view

The new ThumbStripMediator handles observing the regular/incognito
WebStateLists to add the ViewRevealingVerticalPanHandler as a
scrollViewProxy observer. This allows it to get scroll delegate
callbacks from the active webstate.

This CL also slightly fixes the BVC thumb strip hide/reveal behavior to
correctly change the scroll view's content insets as well as the
content offset.

Bug: 1094335
Change-Id: I006a3e72a06b1e327ce0984cf84bc69c2e95fe7a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2563556
Reviewed-by: Gauthier Ambard <gambard@chromium.org>
Reviewed-by: Chris Lu <thegreenfrog@chromium.org>
Commit-Queue: Robbie Gibson <rkgibson@google.com>
Cr-Commit-Position: refs/heads/master@{#834183}
This commit is contained in:
Robbie Gibson
2020-12-07 13:43:52 +00:00
committed by Chromium LUCI CQ
parent 599ad0b15b
commit 609dba3217
12 changed files with 381 additions and 21 deletions

@ -2903,6 +2903,7 @@ NSString* const kBrowserViewControllerSnackbarCategory =
// view is taken, the snapshot will be a blank view. However, if the view's
// parent is hidden but the view itself is not, the snapshot will not be a
// blank view.
[self.tabStripSnapshot removeFromSuperview];
self.tabStripSnapshot = [self.tabStripView screenshotForAnimation];
self.tabStripSnapshot.translatesAutoresizingMaskIntoConstraints = NO;
self.tabStripSnapshot.transform =
@ -2923,12 +2924,11 @@ NSString* const kBrowserViewControllerSnackbarCategory =
// When the Fullscreen Provider is used, the web content extends up to the
// top of the BVC view. It has a visible background and blocks the thumb
// strip. Thus, when the view revealing process starts, the web content
// frame must be moved down. To prevent the actual web content from jumping,
// the content offset must be moved up by a corresponding amount.
// frame must be moved down and the content inset is decreased. To prevent
// the actual web content from jumping, the content offset must be moved up
// by a corresponding amount.
if (self.currentWebState && ![self isNTPActiveForCurrentWebState] &&
ios::GetChromeBrowserProvider()
->GetFullscreenProvider()
->IsInitialized()) {
fullscreen::features::ShouldUseSmoothScrolling()) {
CGFloat toolbarHeight = [self expandedTopToolbarHeight];
CGRect webStateViewFrame = UIEdgeInsetsInsetRect(
[self viewForWebState:self.currentWebState].frame,
@ -2940,6 +2940,12 @@ NSString* const kBrowserViewControllerSnackbarCategory =
CGPoint scrollOffset = scrollProxy.contentOffset;
scrollOffset.y += toolbarHeight;
scrollProxy.contentOffset = scrollOffset;
// TODO(crbug.com/1155536): Inform FullscreenController about these
// changes and allow it to calculate the correct overall contentInset.
UIEdgeInsets contentInset = scrollProxy.contentInset;
contentInset.top -= toolbarHeight;
scrollProxy.contentInset = contentInset;
}
}
}
@ -3000,6 +3006,10 @@ NSString* const kBrowserViewControllerSnackbarCategory =
CRWWebViewScrollViewProxy* scrollProxy =
self.currentWebState->GetWebViewProxy().scrollViewProxy;
UIEdgeInsets contentInset = scrollProxy.contentInset;
contentInset.top += toolbarHeight;
scrollProxy.contentInset = contentInset;
CGPoint scrollOffset = scrollProxy.contentOffset;
scrollOffset.y -= toolbarHeight;
scrollProxy.contentOffset = scrollOffset;

@ -8,11 +8,16 @@ source_set("gestures") {
sources = [
"layout_switcher.h",
"layout_switcher_provider.h",
"pan_handler_scroll_view.h",
"pan_handler_scroll_view.mm",
"view_revealing_animatee.h",
"view_revealing_vertical_pan_handler.h",
"view_revealing_vertical_pan_handler.mm",
]
deps = [ "//base" ]
deps = [
"//base",
"//ios/web/public/ui",
]
configs += [ "//build/config/compiler:enable_arc" ]
}

@ -0,0 +1,28 @@
// 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_GESTURES_PAN_HANDLER_SCROLL_VIEW_H_
#define IOS_CHROME_BROWSER_UI_GESTURES_PAN_HANDLER_SCROLL_VIEW_H_
#import <UIKit/UIKit.h>
@class CRWWebViewScrollViewProxy;
// This private class handles forwarding updates to these properties to an
// underlying |UIScrollView| or |CRWWebViewScrollViewProxy|.
@interface PanHandlerScrollView : NSObject
@property(nonatomic) CGPoint contentOffset;
@property(nonatomic, assign) UIEdgeInsets contentInset;
@property(nonatomic, readonly) UIPanGestureRecognizer* panGestureRecognizer;
@property(nonatomic, readonly, getter=isDecelerating) BOOL decelerating;
@property(nonatomic, readonly, getter=isDragging) BOOL dragging;
- (instancetype)initWithScrollView:(UIScrollView*)scrollView;
- (instancetype)initWithWebViewScrollViewProxy:
(CRWWebViewScrollViewProxy*)scrollViewProxy;
@end
#endif // IOS_CHROME_BROWSER_UI_GESTURES_PAN_HANDLER_SCROLL_VIEW_H_

@ -0,0 +1,72 @@
// 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/gestures/pan_handler_scroll_view.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface PanHandlerScrollView ()
@property(nonatomic, strong) UIScrollView* scrollView;
@property(nonatomic, strong) CRWWebViewScrollViewProxy* scrollViewProxy;
@end
@implementation PanHandlerScrollView
- (instancetype)initWithScrollView:(UIScrollView*)scrollView {
if (self = [super init]) {
_scrollView = scrollView;
}
return self;
}
- (instancetype)initWithWebViewScrollViewProxy:
(CRWWebViewScrollViewProxy*)scrollViewProxy {
if (self = [super init]) {
_scrollViewProxy = scrollViewProxy;
}
return self;
}
- (CGPoint)contentOffset {
return (self.scrollView) ? self.scrollView.contentOffset
: self.scrollViewProxy.contentOffset;
}
- (void)setContentOffset:(CGPoint)contentOffset {
self.scrollView.contentOffset = contentOffset;
self.scrollViewProxy.contentOffset = contentOffset;
}
- (UIEdgeInsets)contentInset {
return (self.scrollView) ? self.scrollView.contentInset
: self.scrollViewProxy.contentInset;
}
- (void)setContentInset:(UIEdgeInsets)contentInset {
self.scrollView.contentInset = contentInset;
self.scrollViewProxy.contentInset = contentInset;
}
- (UIPanGestureRecognizer*)panGestureRecognizer {
return (self.scrollView) ? self.scrollView.panGestureRecognizer
: self.scrollViewProxy.panGestureRecognizer;
}
- (BOOL)isDecelerating {
return (self.scrollView) ? self.scrollView.isDecelerating
: self.scrollViewProxy.isDecelerating;
}
- (BOOL)isDragging {
return (self.scrollView) ? self.scrollView.isDragging
: self.scrollViewProxy.isDragging;
}
@end

@ -9,6 +9,7 @@
#import "ios/chrome/browser/ui/gestures/layout_switcher_provider.h"
#import "ios/chrome/browser/ui/gestures/view_revealing_animatee.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
// Responsible for handling vertical pan gestures to reveal/hide a view behind
// another.
@ -21,7 +22,8 @@
// TODO(crbug.com/1123512): Add support for going straight from a Hidden state
// to a revealed state (and vice-versa) if the gesture's translation and
// velocity are enough to trigger such transition.
@interface ViewRevealingVerticalPanHandler : NSObject <UIScrollViewDelegate>
@interface ViewRevealingVerticalPanHandler
: NSObject <CRWWebViewScrollViewProxyObserver, UIScrollViewDelegate>
// |peekedHeight| is the height of the view when peeked (partially revealed).
// |revealedCoverHeight| is the height of the cover view that remains visible

@ -4,9 +4,11 @@
#import "ios/chrome/browser/ui/gestures/view_revealing_vertical_pan_handler.h"
#include "base/logging.h"
#import "base/notreached.h"
#include "base/numerics/ranges.h"
#import "ios/chrome/browser/ui/gestures/layout_switcher.h"
#import "ios/chrome/browser/ui/gestures/pan_handler_scroll_view.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
@ -366,27 +368,86 @@ const CGFloat kAnimationDuration = 0.25f;
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
PanHandlerScrollView* view =
[[PanHandlerScrollView alloc] initWithScrollView:scrollView];
[self panHandlerScrollViewWillBeginDragging:view];
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
PanHandlerScrollView* view =
[[PanHandlerScrollView alloc] initWithScrollView:scrollView];
[self panHandlerScrollViewDidScroll:view];
}
- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset {
PanHandlerScrollView* view =
[[PanHandlerScrollView alloc] initWithScrollView:scrollView];
[self panHandlerScrollViewWillEndDragging:view
withVelocity:velocity
targetContentOffset:targetContentOffset];
}
- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
willDecelerate:(BOOL)decelerate {
// No-op.
}
#pragma mark - CRWWebViewScrollViewProxyObserver
- (void)webViewScrollViewWillBeginDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
PanHandlerScrollView* view = [[PanHandlerScrollView alloc]
initWithWebViewScrollViewProxy:webViewScrollViewProxy];
[self panHandlerScrollViewWillBeginDragging:view];
}
- (void)webViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
PanHandlerScrollView* view = [[PanHandlerScrollView alloc]
initWithWebViewScrollViewProxy:webViewScrollViewProxy];
[self panHandlerScrollViewDidScroll:view];
}
- (void)webViewScrollViewWillEndDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset {
PanHandlerScrollView* view = [[PanHandlerScrollView alloc]
initWithWebViewScrollViewProxy:webViewScrollViewProxy];
[self panHandlerScrollViewWillEndDragging:view
withVelocity:velocity
targetContentOffset:targetContentOffset];
}
#pragma mark - UIScrollViewDelegate + CRWWebViewScrollViewProxyObserver
- (void)panHandlerScrollViewWillBeginDragging:
(PanHandlerScrollView*)scrollView {
switch (self.currentState) {
case ViewRevealState::Hidden:
case ViewRevealState::Hidden: {
// The transition out of hidden state can only start if the scroll view
// starts dragging from the top.
if (!self.animator.isRunning && scrollView.contentOffset.y != 0) {
CGFloat contentOffsetY =
scrollView.contentOffset.y + scrollView.contentInset.top;
if (contentOffsetY != 0) {
return;
}
break;
}
case ViewRevealState::Peeked:
break;
case ViewRevealState::Revealed:
// The scroll views should be covered in Revealed state, so should not
// be able to be scrolled.
NOTREACHED();
break;
}
[self panGestureBegan];
self.lastScrollOffset = scrollView.contentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
- (void)panHandlerScrollViewDidScroll:(PanHandlerScrollView*)scrollView {
// These delegate methods are approximating the pan gesture handling from
// above, so only change things if the user is actively scrolling.
if (!scrollView.isDragging) {
@ -404,15 +465,16 @@ const CGFloat kAnimationDuration = 0.25f;
if (self.animator.fractionComplete > 0 &&
self.animator.fractionComplete < 1) {
CGPoint currentScrollOffset = scrollView.contentOffset;
currentScrollOffset.y = std::max(self.lastScrollOffset.y, 0.0);
currentScrollOffset.y = self.lastScrollOffset.y;
scrollView.contentOffset = currentScrollOffset;
}
self.lastScrollOffset = scrollView.contentOffset;
}
- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset {
- (void)panHandlerScrollViewWillEndDragging:(PanHandlerScrollView*)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:
(inout CGPoint*)targetContentOffset {
if (self.currentState == ViewRevealState::Hidden &&
self.animator.state != UIViewAnimatingStateActive) {
return;
@ -429,9 +491,4 @@ const CGFloat kAnimationDuration = 0.25f;
[self panGestureEndedWithTranslation:translationY velocity:velocityY];
}
- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
willDecelerate:(BOOL)decelerate {
// No-op.
}
@end

@ -148,6 +148,7 @@
- (void)setIncognitoBrowser:(Browser*)incognitoBrowser {
DCHECK(self.incognitoTabsMediator);
self.incognitoTabsMediator.browser = incognitoBrowser;
self.thumbStripCoordinator.incognitoBrowser = incognitoBrowser;
}
- (void)stopChildCoordinatorsWithCompletion:(ProceduralBlock)completion {
@ -400,6 +401,8 @@
self.thumbStripCoordinator = [[ThumbStripCoordinator alloc]
initWithBaseViewController:baseViewController
browser:self.browser];
self.thumbStripCoordinator.regularBrowser = _regularBrowser;
self.thumbStripCoordinator.incognitoBrowser = _incognitoBrowser;
[self.thumbStripCoordinator start];
self.thumbStripCoordinator.panHandler.layoutSwitcherProvider =
baseViewController;

@ -9,12 +9,18 @@ source_set("thumb_strip") {
"thumb_strip_attacher.h",
"thumb_strip_coordinator.h",
"thumb_strip_coordinator.mm",
"thumb_strip_mediator.h",
"thumb_strip_mediator.mm",
]
deps = [
"//base",
"//ios/chrome/browser/main:public",
"//ios/chrome/browser/ui/coordinators:chrome_coordinators",
"//ios/chrome/browser/ui/gestures",
"//ios/chrome/browser/ui/tab_switcher/tab_grid/grid:grid_ui_constants",
"//ios/chrome/browser/web_state_list",
"//ios/web/public",
"//ios/web/public/ui",
]
configs += [ "//build/config/compiler:enable_arc" ]
}

@ -9,7 +9,6 @@
#import "ios/chrome/browser/ui/coordinators/chrome_coordinator.h"
@class ThumbStripCoordinator;
@class ViewRevealingVerticalPanHandler;
// Coordinator for the thumb strip, which is a 1-row horizontal display of tab
@ -19,6 +18,13 @@
// The thumb strip's pan gesture handler.
@property(nonatomic, strong) ViewRevealingVerticalPanHandler* panHandler;
// The regular browser used to observe scroll events to show/hide the thumb
// strip.
@property(nonatomic, assign) Browser* regularBrowser;
// The incognito browser used to observe scroll events to show/hide the thumb
// strip.
@property(nonatomic, assign) Browser* incognitoBrowser;
@end
#endif // IOS_CHROME_BROWSER_UI_THUMB_STRIP_THUMB_STRIP_COORDINATOR_H_

@ -4,8 +4,10 @@
#import "ios/chrome/browser/ui/thumb_strip/thumb_strip_coordinator.h"
#import "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/ui/gestures/view_revealing_vertical_pan_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_constants.h"
#import "ios/chrome/browser/ui/thumb_strip/thumb_strip_mediator.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
@ -19,6 +21,8 @@ const CGFloat kThumbStripHeight = 168.0f + 22.0f + 22.0f;
@interface ThumbStripCoordinator ()
@property(nonatomic, strong) ThumbStripMediator* mediator;
@end
@implementation ThumbStripCoordinator
@ -31,10 +35,27 @@ const CGFloat kThumbStripHeight = 168.0f + 22.0f + 22.0f;
initWithPeekedHeight:kThumbStripHeight
revealedCoverHeight:kBVCHeightTabGrid
baseViewHeight:baseViewHeight];
self.mediator = [[ThumbStripMediator alloc] init];
if (self.regularBrowser) {
self.mediator.regularWebStateList = self.regularBrowser->GetWebStateList();
}
if (self.incognitoBrowser) {
self.mediator.incognitoWebStateList =
self.incognitoBrowser->GetWebStateList();
}
self.mediator.webViewScrollViewObserver = self.panHandler;
}
- (void)stop {
self.panHandler = nil;
self.mediator = nil;
}
- (void)setIncognitoBrowser:(Browser*)incognitoBrowser {
_incognitoBrowser = incognitoBrowser;
self.mediator.incognitoWebStateList =
_incognitoBrowser ? _incognitoBrowser->GetWebStateList() : nullptr;
}
@end

@ -0,0 +1,29 @@
// 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_THUMB_STRIP_THUMB_STRIP_MEDIATOR_H_
#define IOS_CHROME_BROWSER_UI_THUMB_STRIP_THUMB_STRIP_MEDIATOR_H_
#include <UIKit/UIKit.h>
@protocol CRWWebViewScrollViewProxyObserver;
class WebStateList;
// Mediator for the thumb strip. Handles observing changes in the active web
// state.
@interface ThumbStripMediator : NSObject
// The regular web state list to observe.
@property(nonatomic, assign) WebStateList* regularWebStateList;
// The incognito web state list to observe.
@property(nonatomic, assign) WebStateList* incognitoWebStateList;
// The observer to register/deregister as CRWWebViewScrollViewProxyObserver for
// the active webstates in the given WebStateLists.
@property(nonatomic, weak) id<CRWWebViewScrollViewProxyObserver>
webViewScrollViewObserver;
@end
#endif // IOS_CHROME_BROWSER_UI_THUMB_STRIP_THUMB_STRIP_MEDIATOR_H_

@ -0,0 +1,121 @@
// 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/thumb_strip/thumb_strip_mediator.h"
#include "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/chrome/browser/web_state_list/web_state_list_observer_bridge.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface ThumbStripMediator () <WebStateListObserving> {
std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
}
@end
@implementation ThumbStripMediator
- (instancetype)init {
if (self = [super init]) {
_webStateListObserver = std::make_unique<WebStateListObserverBridge>(self);
}
return self;
}
- (void)dealloc {
if (_regularWebStateList) {
_regularWebStateList->RemoveObserver(_webStateListObserver.get());
}
if (_incognitoWebStateList) {
_incognitoWebStateList->RemoveObserver(_webStateListObserver.get());
}
}
- (void)setRegularWebStateList:(WebStateList*)regularWebStateList {
if (_regularWebStateList) {
_regularWebStateList->RemoveObserver(_webStateListObserver.get());
[self removeObserverFromWebState:_regularWebStateList->GetActiveWebState()];
}
_regularWebStateList = regularWebStateList;
if (_regularWebStateList) {
_regularWebStateList->AddObserver(_webStateListObserver.get());
[self addObserverToWebState:_regularWebStateList->GetActiveWebState()];
}
}
- (void)setIncognitoWebStateList:(WebStateList*)incognitoWebStateList {
if (_incognitoWebStateList) {
_incognitoWebStateList->RemoveObserver(_webStateListObserver.get());
[self
removeObserverFromWebState:_incognitoWebStateList->GetActiveWebState()];
}
_incognitoWebStateList = incognitoWebStateList;
if (_incognitoWebStateList) {
_incognitoWebStateList->AddObserver(_webStateListObserver.get());
[self addObserverToWebState:_incognitoWebStateList->GetActiveWebState()];
}
}
- (void)setWebViewScrollViewObserver:
(id<CRWWebViewScrollViewProxyObserver>)observer {
if (self.incognitoWebStateList) {
[self removeObserverFromWebState:self.incognitoWebStateList
->GetActiveWebState()];
}
if (self.regularWebStateList) {
[self removeObserverFromWebState:self.regularWebStateList
->GetActiveWebState()];
}
_webViewScrollViewObserver = observer;
if (self.incognitoWebStateList) {
[self
addObserverToWebState:self.incognitoWebStateList->GetActiveWebState()];
}
if (self.regularWebStateList) {
[self addObserverToWebState:self.regularWebStateList->GetActiveWebState()];
}
}
#pragma mark - Privates
// Remove |self.webViewScrollViewObserver| from the given |webState|. |webState|
// can be nullptr.
- (void)removeObserverFromWebState:(web::WebState*)webState {
if (webState && self.webViewScrollViewObserver) {
[webState->GetWebViewProxy().scrollViewProxy
removeObserver:self.webViewScrollViewObserver];
}
}
// Add |self.webViewScrollViewObserver| to the given |webState|. |webState| can
// be nullptr.
- (void)addObserverToWebState:(web::WebState*)webState {
if (webState && self.webViewScrollViewObserver) {
[webState->GetWebViewProxy().scrollViewProxy
addObserver:self.webViewScrollViewObserver];
}
}
#pragma mark - WebStateListObserving
- (void)webStateList:(WebStateList*)webStateList
didChangeActiveWebState:(web::WebState*)newWebState
oldWebState:(web::WebState*)oldWebState
atIndex:(int)atIndex
reason:(ActiveWebStateChangeReason)reason {
[self removeObserverFromWebState:oldWebState];
[self addObserverToWebState:newWebState];
}
@end