1/* 2 * Copyright (C) 2013, 2014 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26#import "config.h" 27#import "WKScrollView.h" 28 29#if PLATFORM(IOS) 30 31#import "WKWebViewInternal.h" 32#import <CoreGraphics/CGFloat.h> 33 34@interface UIScrollView (UIScrollViewInternalHack) 35- (CGFloat)_rubberBandOffsetForOffset:(CGFloat)newOffset maxOffset:(CGFloat)maxOffset minOffset:(CGFloat)minOffset range:(CGFloat)range outside:(BOOL *)outside; 36@end 37 38@interface WKScrollViewDelegateForwarder : NSObject <UIScrollViewDelegate> 39 40- (instancetype)initWithInternalDelegate:(WKWebView *)internalDelegate externalDelegate:(id <UIScrollViewDelegate>)externalDelegate; 41 42@end 43 44@implementation WKScrollViewDelegateForwarder { 45 WKWebView *_internalDelegate; 46 id <UIScrollViewDelegate> _externalDelegate; 47} 48 49- (instancetype)initWithInternalDelegate:(WKWebView <UIScrollViewDelegate> *)internalDelegate externalDelegate:(id <UIScrollViewDelegate>)externalDelegate 50{ 51 self = [super init]; 52 if (!self) 53 return nil; 54 _internalDelegate = internalDelegate; 55 _externalDelegate = externalDelegate; 56 return self; 57} 58 59- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 60{ 61 NSMethodSignature *signature = [super methodSignatureForSelector:aSelector]; 62 if (!signature) 63 signature = [(NSObject *)_internalDelegate methodSignatureForSelector:aSelector]; 64 if (!signature) 65 signature = [(NSObject *)_externalDelegate methodSignatureForSelector:aSelector]; 66 return signature; 67} 68 69- (BOOL)respondsToSelector:(SEL)aSelector 70{ 71 return [super respondsToSelector:aSelector] || [_internalDelegate respondsToSelector:aSelector] || [_externalDelegate respondsToSelector:aSelector]; 72} 73 74- (void)forwardInvocation:(NSInvocation *)anInvocation 75{ 76 SEL aSelector = [anInvocation selector]; 77 BOOL internalDelegateWillRespond = [_internalDelegate respondsToSelector:aSelector]; 78 BOOL externalDelegateWillRespond = [_externalDelegate respondsToSelector:aSelector]; 79 80 if (internalDelegateWillRespond && externalDelegateWillRespond) 81 [_internalDelegate _willInvokeUIScrollViewDelegateCallback]; 82 83 if (internalDelegateWillRespond) 84 [anInvocation invokeWithTarget:_internalDelegate]; 85 if (externalDelegateWillRespond) 86 [anInvocation invokeWithTarget:_externalDelegate]; 87 88 if (internalDelegateWillRespond && externalDelegateWillRespond) 89 [_internalDelegate _didInvokeUIScrollViewDelegateCallback]; 90 91 if (!internalDelegateWillRespond && !externalDelegateWillRespond) 92 [super forwardInvocation:anInvocation]; 93} 94 95- (id)forwardingTargetForSelector:(SEL)aSelector 96{ 97 BOOL internalDelegateWillRespond = [_internalDelegate respondsToSelector:aSelector]; 98 BOOL externalDelegateWillRespond = [_externalDelegate respondsToSelector:aSelector]; 99 100 if (internalDelegateWillRespond && !externalDelegateWillRespond) 101 return _internalDelegate; 102 if (externalDelegateWillRespond && !internalDelegateWillRespond) 103 return _externalDelegate; 104 return nil; 105} 106 107@end 108 109@implementation WKScrollView { 110 id <UIScrollViewDelegate> _externalDelegate; 111 WKScrollViewDelegateForwarder *_delegateForwarder; 112} 113 114- (void)setInternalDelegate:(WKWebView <UIScrollViewDelegate> *)internalDelegate 115{ 116 if (internalDelegate == _internalDelegate) 117 return; 118 _internalDelegate = internalDelegate; 119 [self _updateDelegate]; 120} 121 122- (void)setDelegate:(id <UIScrollViewDelegate>)delegate 123{ 124 if (_externalDelegate == delegate) 125 return; 126 _externalDelegate = delegate; 127 [self _updateDelegate]; 128} 129 130- (id <UIScrollViewDelegate>)delegate 131{ 132 return _externalDelegate; 133} 134 135- (void)_updateDelegate 136{ 137 WKScrollViewDelegateForwarder *oldForwarder = _delegateForwarder; 138 _delegateForwarder = nil; 139 if (!_externalDelegate) 140 [super setDelegate:_internalDelegate]; 141 else if (!_internalDelegate) 142 [super setDelegate:_externalDelegate]; 143 else { 144 _delegateForwarder = [[WKScrollViewDelegateForwarder alloc] initWithInternalDelegate:_internalDelegate externalDelegate:_externalDelegate]; 145 [super setDelegate:_delegateForwarder]; 146 } 147 [oldForwarder release]; 148} 149 150- (void)dealloc 151{ 152 [_delegateForwarder release]; 153 [super dealloc]; 154} 155 156static inline bool valuesAreWithinOnePixel(CGFloat a, CGFloat b) 157{ 158 return CGFAbs(a - b) < 1; 159} 160 161- (CGFloat)_rubberBandOffsetForOffset:(CGFloat)newOffset maxOffset:(CGFloat)maxOffset minOffset:(CGFloat)minOffset range:(CGFloat)range outside:(BOOL *)outside 162{ 163 UIEdgeInsets contentInsets = self.contentInset; 164 CGSize contentSize = self.contentSize; 165 CGRect bounds = self.bounds; 166 167 CGFloat minimalHorizontalRange = bounds.size.width - contentInsets.left - contentInsets.right; 168 if (contentSize.width < minimalHorizontalRange) { 169 if (valuesAreWithinOnePixel(minOffset, -contentInsets.left) 170 && valuesAreWithinOnePixel(maxOffset, contentSize.width + contentInsets.right - bounds.size.width) 171 && valuesAreWithinOnePixel(range, bounds.size.width)) { 172 173 CGFloat emptyHorizontalMargin = (minimalHorizontalRange - contentSize.width) / 2; 174 minOffset -= emptyHorizontalMargin; 175 maxOffset = minOffset; 176 } 177 } 178 179 CGFloat minimalVerticalRange = bounds.size.height - contentInsets.top - contentInsets.bottom; 180 if (contentSize.height < minimalVerticalRange) { 181 if (valuesAreWithinOnePixel(minOffset, -contentInsets.top) 182 && valuesAreWithinOnePixel(maxOffset, contentSize.height + contentInsets.bottom - bounds.size.height) 183 && valuesAreWithinOnePixel(range, bounds.size.height)) { 184 185 CGFloat emptyVerticalMargin = (minimalVerticalRange - contentSize.height) / 2; 186 minOffset -= emptyVerticalMargin; 187 maxOffset = minOffset; 188 } 189 } 190 191 return [super _rubberBandOffsetForOffset:newOffset maxOffset:maxOffset minOffset:minOffset range:range outside:outside]; 192} 193 194- (void)setContentInset:(UIEdgeInsets)contentInset 195{ 196 [super setContentInset:contentInset]; 197 198 [_internalDelegate _updateVisibleContentRects]; 199} 200 201// Fetch top/left rubberband amounts (as negative values). 202- (CGSize)_currentTopLeftRubberbandAmount 203{ 204 UIEdgeInsets edgeInsets = [self contentInset]; 205 206 CGSize rubberbandAmount = CGSizeZero; 207 208 CGPoint contentOffset = [self contentOffset]; 209 if (contentOffset.x < -edgeInsets.left) 210 rubberbandAmount.width = std::min<CGFloat>(contentOffset.x + -edgeInsets.left, 0); 211 212 if (contentOffset.y < -edgeInsets.top) 213 rubberbandAmount.height = std::min<CGFloat>(contentOffset.y + edgeInsets.top, 0); 214 215 return rubberbandAmount; 216} 217 218- (void)_restoreContentOffsetWithRubberbandAmount:(CGSize)rubberbandAmount 219{ 220 UIEdgeInsets edgeInsets = [self contentInset]; 221 CGPoint adjustedOffset = [self contentOffset]; 222 223 if (rubberbandAmount.width < 0) 224 adjustedOffset.x = -edgeInsets.left + rubberbandAmount.width; 225 226 if (rubberbandAmount.height < 0) 227 adjustedOffset.y = -edgeInsets.top + rubberbandAmount.height; 228 229 [self setContentOffset:adjustedOffset]; 230} 231 232- (void)_setContentSizePreservingContentOffsetDuringRubberband:(CGSize)contentSize 233{ 234 CGSize currentContentSize = [self contentSize]; 235 236 if (CGSizeEqualToSize(currentContentSize, CGSizeZero) || CGSizeEqualToSize(currentContentSize, contentSize) || self.zoomScale < self.minimumZoomScale) { 237 [self setContentSize:contentSize]; 238 return; 239 } 240 241 CGSize rubberbandAmount = [self _currentTopLeftRubberbandAmount]; 242 243 [self setContentSize:contentSize]; 244 245 if (!CGSizeEqualToSize(rubberbandAmount, CGSizeZero)) 246 [self _restoreContentOffsetWithRubberbandAmount:rubberbandAmount]; 247} 248 249@end 250 251#endif // PLATFORM(IOS) 252