blob: b614fb13dea0fdc0f46a06b62bb0590d29665251 [file] [log] [blame]
// Copyright 2014 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 <QuartzCore/QuartzCore.h>
#include "content/browser/web_contents/web_contents_view_overscroll_animator_slider_mac.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/web_contents_observer.h"
namespace {
// The minimum possible progress of an overscroll animation.
CGFloat kMinProgress = 0;
// The maximum possible progress of an overscroll animation.
CGFloat kMaxProgress = 2.0;
// The maximum duration of the completion or cancellation animations. The
// effective maximum is half of this value, since the longest animation is from
// progress = 1.0 to progress = 2.0;
CGFloat kMaxAnimationDuration = 0.2;
} // namespace
// OverscrollAnimatorSliderView Private Category -------------------------------
@interface OverscrollAnimatorSliderView ()
// Callback from WebContentsPaintObserver.
- (void)webContentsFinishedNonEmptyPaint;
// Resets overscroll animation state.
- (void)reset;
// Given a |progress| from 0 to 2, the expected frame origin of the -movingView.
- (NSPoint)frameOriginWithProgress:(CGFloat)progress;
// The NSView that is moving during the overscroll animation.
- (NSView*)movingView;
// The expected duration of an animation from progress_ to |progress|
- (CGFloat)animationDurationForProgress:(CGFloat)progress;
// NSView override. During an overscroll animation, the cursor may no longer
// rest on the RenderWidgetHost's NativeView, which prevents wheel events from
// reaching the NativeView. The overscroll animation is driven by wheel events
// so they must be explicitly forwarded to the NativeView.
- (void)scrollWheel:(NSEvent*)event;
@end
// Helper Class (ResizingView) -------------------------------------------------
// This NSView subclass is intended to be the RenderWidgetHost's NativeView's
// parent NSView. It is possible for the RenderWidgetHost's NativeView's size to
// become out of sync with its parent NSView. The override of
// -resizeSubviewsWithOldSize: ensures that the sizes will eventually become
// consistent.
// http://crbug.com/264207
@interface ResizingView : NSView
@end
@implementation ResizingView
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
for (NSView* subview in self.subviews)
[subview setFrame:self.bounds];
}
@end
// Helper Class (WebContentsPaintObserver) -------------------------------------
namespace overscroll_animator {
class WebContentsPaintObserver : public content::WebContentsObserver {
public:
WebContentsPaintObserver(content::WebContents* web_contents,
OverscrollAnimatorSliderView* slider_view)
: WebContentsObserver(web_contents), slider_view_(slider_view) {}
void DidFirstVisuallyNonEmptyPaint() override {
[slider_view_ webContentsFinishedNonEmptyPaint];
}
private:
OverscrollAnimatorSliderView* slider_view_; // Weak reference.
};
} // namespace overscroll_animator
// OverscrollAnimatorSliderView Implementation ---------------------------------
@implementation OverscrollAnimatorSliderView
- (instancetype)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
bottomView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
bottomView_.get().imageScaling = NSImageScaleNone;
bottomView_.get().autoresizingMask =
NSViewWidthSizable | NSViewHeightSizable;
bottomView_.get().imageAlignment = NSImageAlignTop;
[self addSubview:bottomView_];
middleView_.reset([[ResizingView alloc] initWithFrame:self.bounds]);
middleView_.get().autoresizingMask =
NSViewWidthSizable | NSViewHeightSizable;
[self addSubview:middleView_];
topView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
topView_.get().autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
topView_.get().imageScaling = NSImageScaleNone;
topView_.get().imageAlignment = NSImageAlignTop;
[self addSubview:topView_];
[self reset];
}
return self;
}
- (void)webContentsFinishedNonEmptyPaint {
observer_.reset();
[self reset];
}
- (void)reset {
DCHECK(!animating_);
inOverscroll_ = NO;
progress_ = kMinProgress;
[CATransaction begin];
[CATransaction setDisableActions:YES];
bottomView_.get().hidden = YES;
middleView_.get().hidden = NO;
topView_.get().hidden = YES;
[bottomView_ setFrameOrigin:NSMakePoint(0, 0)];
[middleView_ setFrameOrigin:NSMakePoint(0, 0)];
[topView_ setFrameOrigin:NSMakePoint(0, 0)];
[CATransaction commit];
}
- (NSPoint)frameOriginWithProgress:(CGFloat)progress {
if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
return NSMakePoint(progress / kMaxProgress * self.bounds.size.width, 0);
return NSMakePoint((1 - progress / kMaxProgress) * self.bounds.size.width, 0);
}
- (NSView*)movingView {
if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
return middleView_;
return topView_;
}
- (CGFloat)animationDurationForProgress:(CGFloat)progress {
CGFloat progressPercentage =
fabs(progress_ - progress) / (kMaxProgress - kMinProgress);
return progressPercentage * kMaxAnimationDuration;
}
- (void)scrollWheel:(NSEvent*)event {
NSView* latestRenderWidgetHostView = [[middleView_ subviews] lastObject];
[latestRenderWidgetHostView scrollWheel:event];
}
// WebContentsOverscrollAnimator Implementation --------------------------------
- (BOOL)needsNavigationSnapshot {
return YES;
}
- (void)beginOverscrollInDirection:
(content::OverscrollAnimatorDirection)direction
navigationSnapshot:(NSImage*)snapshot {
// TODO(erikchen): If snapshot is nil, need a placeholder.
if (animating_ || inOverscroll_)
return;
inOverscroll_ = YES;
direction_ = direction;
if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS) {
// The middleView_ will slide to the right, revealing bottomView_.
bottomView_.get().hidden = NO;
[bottomView_ setImage:snapshot];
} else {
// The topView_ will slide in from the right, concealing middleView_.
topView_.get().hidden = NO;
[topView_ setFrameOrigin:NSMakePoint(self.bounds.size.width, 0)];
[topView_ setImage:snapshot];
}
[self updateOverscrollProgress:kMinProgress];
}
- (void)addRenderWidgetHostNativeView:(NSView*)view {
[middleView_ addSubview:view];
}
- (void)updateOverscrollProgress:(CGFloat)progress {
if (animating_)
return;
DCHECK_LE(progress, kMaxProgress);
DCHECK_GE(progress, kMinProgress);
progress_ = progress;
[[self movingView] setFrameOrigin:[self frameOriginWithProgress:progress]];
}
- (void)completeOverscroll:(content::WebContentsImpl*)webContents {
if (animating_ || !inOverscroll_)
return;
animating_ = YES;
NSView* view = [self movingView];
[NSAnimationContext beginGrouping];
[NSAnimationContext currentContext].duration =
[self animationDurationForProgress:kMaxProgress];
[[NSAnimationContext currentContext] setCompletionHandler:^{
animating_ = NO;
// Animation is complete. Now perform page load.
if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
webContents->GetController().GoBack();
else
webContents->GetController().GoForward();
// Reset the position of the middleView_, but wait for the page to paint
// before showing it.
middleView_.get().hidden = YES;
[middleView_ setFrameOrigin:NSMakePoint(0, 0)];
observer_.reset(
new overscroll_animator::WebContentsPaintObserver(webContents, self));
}];
// Animate the moving view to its final position.
[[view animator] setFrameOrigin:[self frameOriginWithProgress:kMaxProgress]];
[NSAnimationContext endGrouping];
}
- (void)cancelOverscroll {
if (animating_)
return;
if (!inOverscroll_) {
[self reset];
return;
}
animating_ = YES;
NSView* view = [self movingView];
[NSAnimationContext beginGrouping];
[NSAnimationContext currentContext].duration =
[self animationDurationForProgress:kMinProgress];
[[NSAnimationContext currentContext] setCompletionHandler:^{
// Animation is complete. Reset the state.
animating_ = NO;
[self reset];
}];
// Animate the moving view to its initial position.
[[view animator] setFrameOrigin:[self frameOriginWithProgress:kMinProgress]];
[NSAnimationContext endGrouping];
}
@end