| /* |
| * Copyright (C) 2013, 2014 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #import "config.h" |
| #import "ViewGestureController.h" |
| |
| #if PLATFORM(IOS) |
| |
| #import "WebPageGroup.h" |
| #import "ViewGestureControllerMessages.h" |
| #import "ViewGestureGeometryCollectorMessages.h" |
| #import "ViewSnapshotStore.h" |
| #import "WebBackForwardList.h" |
| #import "WebPageMessages.h" |
| #import "WebPageProxy.h" |
| #import "WebProcessProxy.h" |
| #import <WebCore/IOSurface.h> |
| #import <QuartzCore/QuartzCorePrivate.h> |
| #import <UIKit/UIScreenEdgePanGestureRecognizer.h> |
| #import <UIKit/UIViewControllerTransitioning_Private.h> |
| #import <UIKit/UIWebTouchEventsGestureRecognizer.h> |
| #import <UIKit/_UINavigationInteractiveTransition.h> |
| #import <UIKit/_UINavigationParallaxTransition.h> |
| |
| using namespace WebCore; |
| |
| @interface WKSwipeTransitionController : NSObject <_UINavigationInteractiveTransitionBaseDelegate> |
| - (instancetype)initWithViewGestureController:(WebKit::ViewGestureController*)gestureController gestureRecognizerView:(UIView *)gestureRecognizerView; |
| @end |
| |
| @interface _UIViewControllerTransitionContext (WKDetails) |
| @property (nonatomic, copy, setter=_setInteractiveUpdateHandler:) void (^_interactiveUpdateHandler)(BOOL interactionIsOver, CGFloat percentComplete, BOOL transitionCompleted, _UIViewControllerTransitionContext *); |
| @end |
| |
| @implementation WKSwipeTransitionController |
| { |
| WebKit::ViewGestureController *_gestureController; |
| RetainPtr<_UINavigationInteractiveTransitionBase> _backTransitionController; |
| RetainPtr<_UINavigationInteractiveTransitionBase> _forwardTransitionController; |
| } |
| |
| static const float swipeSnapshotRemovalRenderTreeSizeTargetFraction = 0.5; |
| static const std::chrono::seconds swipeSnapshotRemovalWatchdogDuration = 3_s; |
| |
| - (instancetype)initWithViewGestureController:(WebKit::ViewGestureController*)gestureController gestureRecognizerView:(UIView *)gestureRecognizerView |
| { |
| self = [super init]; |
| if (self) { |
| _gestureController = gestureController; |
| |
| _backTransitionController = adoptNS([_UINavigationInteractiveTransitionBase alloc]); |
| _backTransitionController = [_backTransitionController initWithGestureRecognizerView:gestureRecognizerView animator:nil delegate:self]; |
| |
| _forwardTransitionController = adoptNS([_UINavigationInteractiveTransitionBase alloc]); |
| _forwardTransitionController = [_forwardTransitionController initWithGestureRecognizerView:gestureRecognizerView animator:nil delegate:self]; |
| [_forwardTransitionController setShouldReverseTranslation:YES]; |
| } |
| return self; |
| } |
| |
| - (WebKit::ViewGestureController::SwipeDirection)directionForTransition:(_UINavigationInteractiveTransitionBase *)transition |
| { |
| return transition == _backTransitionController ? WebKit::ViewGestureController::SwipeDirection::Left : WebKit::ViewGestureController::SwipeDirection::Right; |
| } |
| |
| - (void)startInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition |
| { |
| _gestureController->beginSwipeGesture(transition, [self directionForTransition:transition]); |
| } |
| |
| - (BOOL)shouldBeginInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition |
| { |
| return _gestureController->canSwipeInDirection([self directionForTransition:transition]); |
| } |
| |
| - (BOOL)interactiveTransition:(_UINavigationInteractiveTransitionBase *)transition gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer |
| { |
| return [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]; |
| } |
| |
| - (BOOL)interactiveTransition:(_UINavigationInteractiveTransitionBase *)transition gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch |
| { |
| return YES; |
| } |
| |
| - (UIPanGestureRecognizer *)gestureRecognizerForInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition WithTarget:(id)target action:(SEL)action |
| { |
| UIScreenEdgePanGestureRecognizer *recognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:target action:action]; |
| switch ([self directionForTransition:transition]) { |
| case WebKit::ViewGestureController::SwipeDirection::Left: |
| [recognizer setEdges:UIRectEdgeLeft]; |
| break; |
| case WebKit::ViewGestureController::SwipeDirection::Right: |
| [recognizer setEdges:UIRectEdgeRight]; |
| break; |
| } |
| return [recognizer autorelease]; |
| } |
| |
| @end |
| |
| namespace WebKit { |
| |
| ViewGestureController::ViewGestureController(WebPageProxy& webPageProxy) |
| : m_webPageProxy(webPageProxy) |
| , m_activeGestureType(ViewGestureType::None) |
| , m_swipeWatchdogTimer(this, &ViewGestureController::swipeSnapshotWatchdogTimerFired) |
| , m_snapshotRemovalTargetRenderTreeSize(0) |
| , m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit(false) |
| { |
| } |
| |
| ViewGestureController::~ViewGestureController() |
| { |
| } |
| |
| void ViewGestureController::installSwipeHandler(UIView *gestureRecognizerView, UIView *swipingView) |
| { |
| ASSERT(!m_swipeInteractiveTransitionDelegate); |
| m_swipeInteractiveTransitionDelegate = adoptNS([[WKSwipeTransitionController alloc] initWithViewGestureController:this gestureRecognizerView:gestureRecognizerView]); |
| m_liveSwipeView = swipingView; |
| } |
| |
| void ViewGestureController::beginSwipeGesture(_UINavigationInteractiveTransitionBase *transition, SwipeDirection direction) |
| { |
| if (m_activeGestureType != ViewGestureType::None) |
| return; |
| |
| ViewSnapshotStore::shared().recordSnapshot(m_webPageProxy); |
| |
| WebKit::WebBackForwardListItem* targetItem = direction == SwipeDirection::Left ? m_webPageProxy.backForwardList().backItem() : m_webPageProxy.backForwardList().forwardItem(); |
| |
| CGRect liveSwipeViewFrame = [m_liveSwipeView frame]; |
| |
| RetainPtr<UIViewController> snapshotViewController = adoptNS([[UIViewController alloc] init]); |
| m_snapshotView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]); |
| |
| RetainPtr<UIColor> backgroundColor = [UIColor whiteColor]; |
| ViewSnapshot snapshot; |
| if (ViewSnapshotStore::shared().getSnapshot(targetItem, snapshot)) { |
| float deviceScaleFactor = m_webPageProxy.deviceScaleFactor(); |
| FloatSize swipeLayerSizeInDeviceCoordinates(liveSwipeViewFrame.size); |
| swipeLayerSizeInDeviceCoordinates.scale(deviceScaleFactor); |
| if (snapshot.hasImage() && snapshot.size == swipeLayerSizeInDeviceCoordinates && deviceScaleFactor == snapshot.deviceScaleFactor) |
| [m_snapshotView layer].contents = snapshot.asLayerContents(); |
| Color coreColor = snapshot.backgroundColor; |
| if (coreColor.isValid()) |
| backgroundColor = adoptNS([[UIColor alloc] initWithCGColor:cachedCGColor(coreColor, ColorSpaceDeviceRGB)]); |
| } |
| |
| [m_snapshotView setBackgroundColor:backgroundColor.get()]; |
| [m_snapshotView layer].contentsGravity = kCAGravityTopLeft; |
| [m_snapshotView layer].contentsScale = m_liveSwipeView.window.screen.scale; |
| [snapshotViewController setView:m_snapshotView.get()]; |
| |
| m_transitionContainerView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]); |
| m_liveSwipeViewClippingView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]); |
| [m_liveSwipeViewClippingView setClipsToBounds:YES]; |
| |
| [m_liveSwipeView.superview insertSubview:m_transitionContainerView.get() belowSubview:m_liveSwipeView]; |
| [m_liveSwipeViewClippingView addSubview:m_liveSwipeView]; |
| [m_transitionContainerView addSubview:m_liveSwipeViewClippingView.get()]; |
| |
| RetainPtr<UIViewController> targettedViewController = adoptNS([[UIViewController alloc] init]); |
| [targettedViewController setView:m_liveSwipeViewClippingView.get()]; |
| |
| UINavigationControllerOperation transitionOperation = direction == SwipeDirection::Left ? UINavigationControllerOperationPop : UINavigationControllerOperationPush; |
| RetainPtr<_UINavigationParallaxTransition> animationController = adoptNS([[_UINavigationParallaxTransition alloc] initWithCurrentOperation:transitionOperation]); |
| |
| RetainPtr<_UIViewControllerOneToOneTransitionContext> transitionContext = adoptNS([[_UIViewControllerOneToOneTransitionContext alloc] init]); |
| [transitionContext _setFromViewController:targettedViewController.get()]; |
| [transitionContext _setToViewController:snapshotViewController.get()]; |
| [transitionContext _setContainerView:m_transitionContainerView.get()]; |
| [transitionContext _setFromStartFrame:liveSwipeViewFrame]; |
| [transitionContext _setToEndFrame:liveSwipeViewFrame]; |
| [transitionContext _setToStartFrame:CGRectZero]; |
| [transitionContext _setFromEndFrame:CGRectZero]; |
| [transitionContext _setAnimator:animationController.get()]; |
| [transitionContext _setInteractor:transition]; |
| [transitionContext _setTransitionIsInFlight:YES]; |
| [transitionContext _setCompletionHandler:^(_UIViewControllerTransitionContext *context, BOOL didComplete) { endSwipeGesture(targetItem, context, !didComplete); }]; |
| [transitionContext _setInteractiveUpdateHandler:^(BOOL, CGFloat, BOOL, _UIViewControllerTransitionContext *) { }]; |
| |
| [transition setAnimationController:animationController.get()]; |
| [transition startInteractiveTransition:transitionContext.get()]; |
| |
| m_activeGestureType = ViewGestureType::Swipe; |
| } |
| |
| bool ViewGestureController::canSwipeInDirection(SwipeDirection direction) |
| { |
| if (direction == SwipeDirection::Left) |
| return !!m_webPageProxy.backForwardList().backItem(); |
| return !!m_webPageProxy.backForwardList().forwardItem(); |
| } |
| |
| void ViewGestureController::endSwipeGesture(WebBackForwardListItem* targetItem, _UIViewControllerTransitionContext *context, bool cancelled) |
| { |
| [context _setTransitionIsInFlight:NO]; |
| [context _setInteractor:nil]; |
| [context _setAnimator:nil]; |
| |
| [[m_transitionContainerView superview] insertSubview:m_snapshotView.get() aboveSubview:m_transitionContainerView.get()]; |
| [[m_transitionContainerView superview] insertSubview:m_liveSwipeView aboveSubview:m_transitionContainerView.get()]; |
| [m_liveSwipeViewClippingView removeFromSuperview]; |
| m_liveSwipeViewClippingView = nullptr; |
| [m_transitionContainerView removeFromSuperview]; |
| m_transitionContainerView = nullptr; |
| |
| if (cancelled) { |
| removeSwipeSnapshot(); |
| return; |
| } |
| |
| ViewSnapshot snapshot; |
| m_snapshotRemovalTargetRenderTreeSize = 0; |
| if (ViewSnapshotStore::shared().getSnapshot(targetItem, snapshot)) |
| m_snapshotRemovalTargetRenderTreeSize = snapshot.renderTreeSize * swipeSnapshotRemovalRenderTreeSizeTargetFraction; |
| m_snapshotRemovalTargetTransactionID = m_webPageProxy.drawingArea()->lastVisibleTransactionID() + 1; |
| |
| // We don't want to replace the current back-forward item's snapshot |
| // like we normally would when going back or forward, because we are |
| // displaying the destination item's snapshot. |
| ViewSnapshotStore::shared().disableSnapshotting(); |
| m_webPageProxy.goToBackForwardItem(targetItem); |
| ViewSnapshotStore::shared().enableSnapshotting(); |
| |
| m_swipeWatchdogTimer.startOneShot(swipeSnapshotRemovalWatchdogDuration.count()); |
| |
| m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit = true; |
| } |
| |
| void ViewGestureController::setRenderTreeSize(uint64_t renderTreeSize) |
| { |
| if (m_activeGestureType != ViewGestureType::Swipe) |
| return; |
| |
| if (!m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit) |
| return; |
| |
| // Don't remove the swipe snapshot until we get a drawing area transaction more recent than the navigation, |
| // and we hit the render tree size threshold. This avoids potentially removing the snapshot early, |
| // when receiving commits from the previous (pre-navigation) page. |
| if ((!m_snapshotRemovalTargetRenderTreeSize || renderTreeSize > m_snapshotRemovalTargetRenderTreeSize) && m_webPageProxy.drawingArea()->lastVisibleTransactionID() >= m_snapshotRemovalTargetTransactionID) |
| removeSwipeSnapshot(); |
| } |
| |
| void ViewGestureController::swipeSnapshotWatchdogTimerFired(Timer<ViewGestureController>*) |
| { |
| removeSwipeSnapshot(); |
| } |
| |
| void ViewGestureController::removeSwipeSnapshot() |
| { |
| m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit = false; |
| |
| m_swipeWatchdogTimer.stop(); |
| |
| if (m_activeGestureType != ViewGestureType::Swipe) |
| return; |
| |
| #if USE(IOSURFACE) |
| if (m_currentSwipeSnapshotSurface) |
| m_currentSwipeSnapshotSurface->setIsVolatile(true); |
| m_currentSwipeSnapshotSurface = nullptr; |
| #endif |
| |
| [m_snapshotView removeFromSuperview]; |
| m_snapshotView = nullptr; |
| |
| m_snapshotRemovalTargetRenderTreeSize = 0; |
| m_activeGestureType = ViewGestureType::None; |
| } |
| |
| } // namespace WebKit |
| |
| #endif // PLATFORM(IOS) |