1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import <QuartzCore/QuartzCore.h>
7 #include "content/browser/web_contents/web_contents_view_overscroll_animator_slider_mac.h"
9 #include "content/browser/web_contents/web_contents_impl.h"
10 #include "content/public/browser/web_contents_observer.h"
13 // The minimum possible progress of an overscroll animation.
14 CGFloat kMinProgress = 0;
15 // The maximum possible progress of an overscroll animation.
16 CGFloat kMaxProgress = 2.0;
17 // The maximum duration of the completion or cancellation animations. The
18 // effective maximum is half of this value, since the longest animation is from
19 // progress = 1.0 to progress = 2.0;
20 CGFloat kMaxAnimationDuration = 0.2;
23 // OverscrollAnimatorSliderView Private Category -------------------------------
25 @interface OverscrollAnimatorSliderView ()
26 // Callback from WebContentsPaintObserver.
27 - (void)webContentsFinishedNonEmptyPaint;
29 // Resets overscroll animation state.
32 // Given a |progress| from 0 to 2, the expected frame origin of the -movingView.
33 - (NSPoint)frameOriginWithProgress:(CGFloat)progress;
35 // The NSView that is moving during the overscroll animation.
36 - (NSView*)movingView;
38 // The expected duration of an animation from progress_ to |progress|
39 - (CGFloat)animationDurationForProgress:(CGFloat)progress;
41 // NSView override. During an overscroll animation, the cursor may no longer
42 // rest on the RenderWidgetHost's NativeView, which prevents wheel events from
43 // reaching the NativeView. The overscroll animation is driven by wheel events
44 // so they must be explicitly forwarded to the NativeView.
45 - (void)scrollWheel:(NSEvent*)event;
48 // Helper Class (ResizingView) -------------------------------------------------
50 // This NSView subclass is intended to be the RenderWidgetHost's NativeView's
51 // parent NSView. It is possible for the RenderWidgetHost's NativeView's size to
52 // become out of sync with its parent NSView. The override of
53 // -resizeSubviewsWithOldSize: ensures that the sizes will eventually become
55 // http://crbug.com/264207
56 @interface ResizingView : NSView
59 @implementation ResizingView
60 - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
61 for (NSView* subview in self.subviews)
62 [subview setFrame:self.bounds];
66 // Helper Class (WebContentsPaintObserver) -------------------------------------
68 namespace overscroll_animator {
69 class WebContentsPaintObserver : public content::WebContentsObserver {
71 WebContentsPaintObserver(content::WebContents* web_contents,
72 OverscrollAnimatorSliderView* slider_view)
73 : WebContentsObserver(web_contents), slider_view_(slider_view) {}
75 virtual void DidFirstVisuallyNonEmptyPaint() override {
76 [slider_view_ webContentsFinishedNonEmptyPaint];
80 OverscrollAnimatorSliderView* slider_view_; // Weak reference.
82 } // namespace overscroll_animator
84 // OverscrollAnimatorSliderView Implementation ---------------------------------
86 @implementation OverscrollAnimatorSliderView
88 - (instancetype)initWithFrame:(NSRect)frame {
89 self = [super initWithFrame:frame];
91 bottomView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
92 bottomView_.get().imageScaling = NSImageScaleNone;
93 bottomView_.get().autoresizingMask =
94 NSViewWidthSizable | NSViewHeightSizable;
95 bottomView_.get().imageAlignment = NSImageAlignTop;
96 [self addSubview:bottomView_];
97 middleView_.reset([[ResizingView alloc] initWithFrame:self.bounds]);
98 middleView_.get().autoresizingMask =
99 NSViewWidthSizable | NSViewHeightSizable;
100 [self addSubview:middleView_];
101 topView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
102 topView_.get().autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
103 topView_.get().imageScaling = NSImageScaleNone;
104 topView_.get().imageAlignment = NSImageAlignTop;
105 [self addSubview:topView_];
112 - (void)webContentsFinishedNonEmptyPaint {
120 progress_ = kMinProgress;
122 [CATransaction begin];
123 [CATransaction setDisableActions:YES];
124 bottomView_.get().hidden = YES;
125 middleView_.get().hidden = NO;
126 topView_.get().hidden = YES;
128 [bottomView_ setFrameOrigin:NSMakePoint(0, 0)];
129 [middleView_ setFrameOrigin:NSMakePoint(0, 0)];
130 [topView_ setFrameOrigin:NSMakePoint(0, 0)];
131 [CATransaction commit];
134 - (NSPoint)frameOriginWithProgress:(CGFloat)progress {
135 if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
136 return NSMakePoint(progress / kMaxProgress * self.bounds.size.width, 0);
137 return NSMakePoint((1 - progress / kMaxProgress) * self.bounds.size.width, 0);
140 - (NSView*)movingView {
141 if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
146 - (CGFloat)animationDurationForProgress:(CGFloat)progress {
147 CGFloat progressPercentage =
148 fabs(progress_ - progress) / (kMaxProgress - kMinProgress);
149 return progressPercentage * kMaxAnimationDuration;
152 - (void)scrollWheel:(NSEvent*)event {
153 NSView* latestRenderWidgetHostView = [[middleView_ subviews] lastObject];
154 [latestRenderWidgetHostView scrollWheel:event];
157 // WebContentsOverscrollAnimator Implementation --------------------------------
159 - (BOOL)needsNavigationSnapshot {
163 - (void)beginOverscrollInDirection:
164 (content::OverscrollAnimatorDirection)direction
165 navigationSnapshot:(NSImage*)snapshot {
166 // TODO(erikchen): If snapshot is nil, need a placeholder.
167 if (animating_ || inOverscroll_)
171 direction_ = direction;
172 if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS) {
173 // The middleView_ will slide to the right, revealing bottomView_.
174 bottomView_.get().hidden = NO;
175 [bottomView_ setImage:snapshot];
177 // The topView_ will slide in from the right, concealing middleView_.
178 topView_.get().hidden = NO;
179 [topView_ setFrameOrigin:NSMakePoint(self.bounds.size.width, 0)];
180 [topView_ setImage:snapshot];
183 [self updateOverscrollProgress:kMinProgress];
186 - (void)addRenderWidgetHostNativeView:(NSView*)view {
187 [middleView_ addSubview:view];
190 - (void)updateOverscrollProgress:(CGFloat)progress {
193 DCHECK_LE(progress, kMaxProgress);
194 DCHECK_GE(progress, kMinProgress);
195 progress_ = progress;
196 [[self movingView] setFrameOrigin:[self frameOriginWithProgress:progress]];
199 - (void)completeOverscroll:(content::WebContentsImpl*)webContents {
200 if (animating_ || !inOverscroll_)
205 NSView* view = [self movingView];
206 [NSAnimationContext beginGrouping];
207 [NSAnimationContext currentContext].duration =
208 [self animationDurationForProgress:kMaxProgress];
209 [[NSAnimationContext currentContext] setCompletionHandler:^{
212 // Animation is complete. Now perform page load.
213 if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
214 webContents->GetController().GoBack();
216 webContents->GetController().GoForward();
218 // Reset the position of the middleView_, but wait for the page to paint
219 // before showing it.
220 middleView_.get().hidden = YES;
221 [middleView_ setFrameOrigin:NSMakePoint(0, 0)];
223 new overscroll_animator::WebContentsPaintObserver(webContents, self));
226 // Animate the moving view to its final position.
227 [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMaxProgress]];
229 [NSAnimationContext endGrouping];
232 - (void)cancelOverscroll {
236 if (!inOverscroll_) {
243 NSView* view = [self movingView];
244 [NSAnimationContext beginGrouping];
245 [NSAnimationContext currentContext].duration =
246 [self animationDurationForProgress:kMinProgress];
247 [[NSAnimationContext currentContext] setCompletionHandler:^{
248 // Animation is complete. Reset the state.
253 // Animate the moving view to its initial position.
254 [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMinProgress]];
256 [NSAnimationContext endGrouping];