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. 4 5#import <QuartzCore/QuartzCore.h> 6 7#include "content/browser/web_contents/web_contents_view_overscroll_animator_slider_mac.h" 8 9#include "content/browser/web_contents/web_contents_impl.h" 10#include "content/public/browser/web_contents_observer.h" 11 12namespace { 13// The minimum possible progress of an overscroll animation. 14CGFloat kMinProgress = 0; 15// The maximum possible progress of an overscroll animation. 16CGFloat 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; 20CGFloat kMaxAnimationDuration = 0.2; 21} // namespace 22 23// OverscrollAnimatorSliderView Private Category ------------------------------- 24 25@interface OverscrollAnimatorSliderView () 26// Callback from WebContentsPaintObserver. 27- (void)webContentsFinishedNonEmptyPaint; 28 29// Resets overscroll animation state. 30- (void)reset; 31 32// Given a |progress| from 0 to 2, the expected frame origin of the -movingView. 33- (NSPoint)frameOriginWithProgress:(CGFloat)progress; 34 35// The NSView that is moving during the overscroll animation. 36- (NSView*)movingView; 37 38// The expected duration of an animation from progress_ to |progress| 39- (CGFloat)animationDurationForProgress:(CGFloat)progress; 40 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; 46@end 47 48// Helper Class (ResizingView) ------------------------------------------------- 49 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 54// consistent. 55// http://crbug.com/264207 56@interface ResizingView : NSView 57@end 58 59@implementation ResizingView 60- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize { 61 for (NSView* subview in self.subviews) 62 [subview setFrame:self.bounds]; 63} 64@end 65 66// Helper Class (WebContentsPaintObserver) ------------------------------------- 67 68namespace overscroll_animator { 69class WebContentsPaintObserver : public content::WebContentsObserver { 70 public: 71 WebContentsPaintObserver(content::WebContents* web_contents, 72 OverscrollAnimatorSliderView* slider_view) 73 : WebContentsObserver(web_contents), slider_view_(slider_view) {} 74 75 virtual void DidFirstVisuallyNonEmptyPaint() OVERRIDE { 76 [slider_view_ webContentsFinishedNonEmptyPaint]; 77 } 78 79 private: 80 OverscrollAnimatorSliderView* slider_view_; // Weak reference. 81}; 82} // namespace overscroll_animator 83 84// OverscrollAnimatorSliderView Implementation --------------------------------- 85 86@implementation OverscrollAnimatorSliderView 87 88- (instancetype)initWithFrame:(NSRect)frame { 89 self = [super initWithFrame:frame]; 90 if (self) { 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_]; 106 107 [self reset]; 108 } 109 return self; 110} 111 112- (void)webContentsFinishedNonEmptyPaint { 113 observer_.reset(); 114 [self reset]; 115} 116 117- (void)reset { 118 DCHECK(!animating_); 119 inOverscroll_ = NO; 120 progress_ = kMinProgress; 121 122 [CATransaction begin]; 123 [CATransaction setDisableActions:YES]; 124 bottomView_.get().hidden = YES; 125 middleView_.get().hidden = NO; 126 topView_.get().hidden = YES; 127 128 [bottomView_ setFrameOrigin:NSMakePoint(0, 0)]; 129 [middleView_ setFrameOrigin:NSMakePoint(0, 0)]; 130 [topView_ setFrameOrigin:NSMakePoint(0, 0)]; 131 [CATransaction commit]; 132} 133 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); 138} 139 140- (NSView*)movingView { 141 if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS) 142 return middleView_; 143 return topView_; 144} 145 146- (CGFloat)animationDurationForProgress:(CGFloat)progress { 147 CGFloat progressPercentage = 148 fabs(progress_ - progress) / (kMaxProgress - kMinProgress); 149 return progressPercentage * kMaxAnimationDuration; 150} 151 152- (void)scrollWheel:(NSEvent*)event { 153 NSView* latestRenderWidgetHostView = [[middleView_ subviews] lastObject]; 154 [latestRenderWidgetHostView scrollWheel:event]; 155} 156 157// WebContentsOverscrollAnimator Implementation -------------------------------- 158 159- (BOOL)needsNavigationSnapshot { 160 return YES; 161} 162 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_) 168 return; 169 170 inOverscroll_ = YES; 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]; 176 } else { 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]; 181 } 182 183 [self updateOverscrollProgress:kMinProgress]; 184} 185 186- (void)addRenderWidgetHostNativeView:(NSView*)view { 187 [middleView_ addSubview:view]; 188} 189 190- (void)updateOverscrollProgress:(CGFloat)progress { 191 if (animating_) 192 return; 193 DCHECK_LE(progress, kMaxProgress); 194 DCHECK_GE(progress, kMinProgress); 195 progress_ = progress; 196 [[self movingView] setFrameOrigin:[self frameOriginWithProgress:progress]]; 197} 198 199- (void)completeOverscroll:(content::WebContentsImpl*)webContents { 200 if (animating_ || !inOverscroll_) 201 return; 202 203 animating_ = YES; 204 205 NSView* view = [self movingView]; 206 [NSAnimationContext beginGrouping]; 207 [NSAnimationContext currentContext].duration = 208 [self animationDurationForProgress:kMaxProgress]; 209 [[NSAnimationContext currentContext] setCompletionHandler:^{ 210 animating_ = NO; 211 212 // Animation is complete. Now perform page load. 213 if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS) 214 webContents->GetController().GoBack(); 215 else 216 webContents->GetController().GoForward(); 217 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)]; 222 observer_.reset( 223 new overscroll_animator::WebContentsPaintObserver(webContents, self)); 224 }]; 225 226 // Animate the moving view to its final position. 227 [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMaxProgress]]; 228 229 [NSAnimationContext endGrouping]; 230} 231 232- (void)cancelOverscroll { 233 if (animating_) 234 return; 235 236 if (!inOverscroll_) { 237 [self reset]; 238 return; 239 } 240 241 animating_ = YES; 242 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. 249 animating_ = NO; 250 [self reset]; 251 }]; 252 253 // Animate the moving view to its initial position. 254 [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMinProgress]]; 255 256 [NSAnimationContext endGrouping]; 257} 258 259@end 260