| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/app/change_profile_animator.h" |
| |
| #import <objc/runtime.h> |
| |
| #import "base/functional/bind.h" |
| #import "base/functional/callback.h" |
| #import "base/task/sequenced_task_runner.h" |
| #import "ios/chrome/app/profile/profile_state.h" |
| #import "ios/chrome/app/profile/profile_state_observer.h" |
| #import "ios/chrome/browser/shared/coordinator/scene/scene_state.h" |
| #import "ios/chrome/browser/shared/coordinator/scene/scene_state_observer.h" |
| #import "ios/chrome/browser/shared/ui/chrome_overlay_window/chrome_overlay_window.h" |
| |
| @interface ChangeProfileAnimation : NSObject |
| |
| - (instancetype)init NS_UNAVAILABLE; |
| - (instancetype)initWithWindow:(ChromeOverlayWindow*)window |
| NS_DESIGNATED_INITIALIZER; |
| |
| // Captures a snapshot of the view presented by the window, install it as |
| // an overlay, and start an animation to blur that view during `duration`. |
| - (void)blurWithDuration:(base::TimeDelta)duration; |
| |
| // Removes the snapshot overlay, and remove the blur effect in `duration`. |
| // If called while the blur animation is in progress, the unblur will wait |
| // for the blur animation to complete before starting. |
| - (void)unblurWithDuration:(base::TimeDelta)duration; |
| |
| @end |
| |
| namespace { |
| |
| // Duration for the fade-in and fade-out of the change profile animation. |
| constexpr base::TimeDelta kAnimationDuration = base::Milliseconds(250); |
| |
| // Returns a callback that starts the unblur animation on `animator` with |
| // a `duration`, or does nothing if `animator` is nil. |
| void UnblurWithDuration(ChangeProfileAnimation* animator, |
| base::TimeDelta duration) { |
| [animator unblurWithDuration:kAnimationDuration]; |
| } |
| |
| // Invokes `continuation` if `weak_scene_state` is not nil and the UI is |
| // enabled (to avoid crashing if the `SceneState` is disconnected while |
| // the callback was pending). Then stops the animation using `animator` |
| // once the continuation completes. |
| void InvokeChangeProfileContinuation(ChangeProfileContinuation continuation, |
| __weak SceneState* weak_scene_state, |
| base::OnceClosure closure) { |
| if (SceneState* strong_scene_state = weak_scene_state) { |
| if (strong_scene_state.UIEnabled) { |
| std::move(continuation).Run(strong_scene_state, std::move(closure)); |
| } |
| } |
| } |
| |
| } // namespace |
| |
| @implementation ChangeProfileAnimation { |
| // The window on which the animations should be played. |
| __weak ChromeOverlayWindow* _window; |
| |
| // Visual effect view used to animate the blur and unblur animations. |
| UIVisualEffectView* _effectView; |
| |
| // Snapshot of the old UI captured when the blur animation is started. |
| UIView* _snapshotView; |
| |
| // Records whether the blur animation is in progress. Used to delay |
| // the unblur animation if the profile initialisation was faster than |
| // the blur animation. |
| BOOL _blurInProgress; |
| |
| // Store the duration passed if -unblurWithDuration: was called while |
| // the blur animation was still in progress. If it has a value when |
| // the blur animation completes, the unblur will start automatically |
| // with that duration. |
| std::optional<base::TimeDelta> _unblurDuration; |
| } |
| |
| - (instancetype)initWithWindow:(ChromeOverlayWindow*)window { |
| if ((self = [super init])) { |
| _window = window; |
| } |
| return self; |
| } |
| |
| - (void)blurWithDuration:(base::TimeDelta)duration { |
| UIView* view = _window.rootViewController.view; |
| if (!view) { |
| return; |
| } |
| |
| _effectView = [[UIVisualEffectView alloc] initWithEffect:nil]; |
| _snapshotView = [view snapshotViewAfterScreenUpdates:NO]; |
| |
| if (!_snapshotView) { |
| _snapshotView = [[UIView alloc] initWithFrame:view.frame]; |
| _snapshotView.backgroundColor = [UIColor whiteColor]; |
| } |
| |
| // Install the snapshot and the effect view as overlays above the UIWindow. |
| // The effect initially does nothing, but it is possible to animate it by |
| // setting the -effect property in an animation block. |
| [_window activateOverlay:_snapshotView withLevel:UIWindowLevelNormal]; |
| [_window activateOverlay:_effectView withLevel:(UIWindowLevelNormal + 1.0)]; |
| |
| _blurInProgress = YES; |
| |
| // Use `self` to allow the block to retain the object until the animation |
| // completes. This is required because the ChangeProfileAnimator drops its |
| // reference after starting the fade out. |
| [UIView animateWithDuration:duration.InSecondsF() |
| animations:^{ |
| [self blurAnimations]; |
| } |
| completion:^(BOOL complete) { |
| if (complete) { |
| [self blurComplete]; |
| } |
| }]; |
| } |
| |
| - (void)unblurWithDuration:(base::TimeDelta)duration { |
| if (_blurInProgress) { |
| _unblurDuration = duration; |
| return; |
| } |
| |
| if (!_snapshotView) { |
| return; |
| } |
| |
| [_window deactivateOverlay:_snapshotView]; |
| _snapshotView = nil; |
| |
| // Use `self` to allow the block to retain the object until the animation |
| // completes. This is required because the ChangeProfileAnimator drops its |
| // reference after starting the fade out. |
| [UIView animateWithDuration:duration.InSecondsF() |
| animations:^{ |
| [self unblurAnimations]; |
| } |
| completion:^(BOOL complete) { |
| if (complete) { |
| [self unblurComplete]; |
| } |
| }]; |
| } |
| |
| // Performs the view changes to animate as part of the blur animation. |
| - (void)blurAnimations { |
| _effectView.effect = |
| [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial]; |
| } |
| |
| // Invoked when the blur animation is complete. Will invoke the unblur |
| // animation if it was requested while the blur animation was still in |
| // progress. |
| - (void)blurComplete { |
| if (!_blurInProgress) { |
| // Return early if this is called multiple times. |
| return; |
| } |
| |
| _blurInProgress = NO; |
| if (_unblurDuration.has_value()) { |
| base::TimeDelta duration = *_unblurDuration; |
| _unblurDuration = std::nullopt; |
| |
| [self unblurWithDuration:duration]; |
| return; |
| } |
| } |
| |
| // Performs the view changes to animate as part of the unblur animation. |
| - (void)unblurAnimations { |
| _effectView.effect = nil; |
| } |
| |
| // Invoked when the unblur animation is complete. Should remove all the |
| // view used for the animations. |
| - (void)unblurComplete { |
| if (!_effectView) { |
| // Return early if this is called multiple times. |
| return; |
| } |
| |
| [_window deactivateOverlay:_effectView]; |
| _effectView = nil; |
| } |
| |
| @end |
| |
| @interface ChangeProfileAnimator () <ProfileStateObserver, |
| SceneStateAnimator, |
| SceneStateObserver> |
| @end |
| |
| @implementation ChangeProfileAnimator { |
| ChangeProfileAnimation* _animation; |
| __weak SceneState* _sceneState; |
| ProfileInitStage _minimumInitStage; |
| ChangeProfileContinuation _continuation; |
| BOOL _cancelledAnimation; |
| } |
| |
| - (instancetype)initWithWindow:(ChromeOverlayWindow*)window { |
| if ((self = [super init])) { |
| if (window) { |
| _animation = [[ChangeProfileAnimation alloc] initWithWindow:window]; |
| } |
| } |
| return self; |
| } |
| |
| - (void)startAnimation { |
| [_animation blurWithDuration:kAnimationDuration]; |
| } |
| |
| - (void)waitForSceneState:(SceneState*)sceneState |
| toReachInitStage:(ProfileInitStage)initStage |
| continuation:(ChangeProfileContinuation)continuation { |
| DCHECK(continuation); |
| DCHECK(sceneState.profileState); |
| DCHECK_GE(initStage, ProfileInitStage::kUIReady); |
| |
| _sceneState = sceneState; |
| _continuation = std::move(continuation); |
| _minimumInitStage = initStage; |
| |
| // Attach self as an associated object of the SceneState. This ensures |
| // that the ChangeProfileAnimator will live as long as the SceneState. |
| objc_setAssociatedObject(_sceneState, [self associationKey], self, |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| |
| _sceneState.animator = self; |
| |
| // Observe both the SceneState and the ProfileState to detect when the |
| // profile and the UI initialisations are complete. ProfileState calls |
| // -profileState:didTransitionToInitStage:fromInitStage: when adding |
| // an observer, so there is no need to check the initState here. |
| [_sceneState addObserver:self]; |
| [_sceneState.profileState addObserver:self]; |
| } |
| |
| #pragma mark ProfileStateObserver |
| |
| - (void)profileState:(ProfileState*)profileState |
| didTransitionToInitStage:(ProfileInitStage)nextInitStage |
| fromInitStage:(ProfileInitStage)fromInitStage { |
| [self initialisationProgressed]; |
| } |
| |
| #pragma mark SceneStateObserver |
| |
| - (void)sceneStateDidEnableUI:(SceneState*)sceneState { |
| [self initialisationProgressed]; |
| } |
| |
| #pragma mark SceneStateAnimator |
| |
| - (void)cancelAnimation { |
| if (!_cancelledAnimation) { |
| _cancelledAnimation = YES; |
| [_animation unblurWithDuration:kAnimationDuration]; |
| } |
| } |
| |
| - (void)restartAnimation { |
| if (_cancelledAnimation) { |
| _cancelledAnimation = NO; |
| [_animation blurWithDuration:kAnimationDuration]; |
| } |
| } |
| |
| #pragma mark Private methods |
| |
| // Called when the initialisation progressed (i.e. the state of any of the |
| // observed object changed). |
| - (void)initialisationProgressed { |
| if (!_sceneState.UIEnabled) { |
| return; |
| } |
| |
| ProfileState* profileState = _sceneState.profileState; |
| if (profileState.initStage < _minimumInitStage) { |
| return; |
| } |
| |
| // Ensure that the completion is always invoked asynchronously, even if |
| // the profile was already in the expected stage and that the animation |
| // to unblur the view starts when the continuation is complete. |
| // |
| // The callback does not strongly retain the SceneState since the ivar |
| // is declared as __weak SceneState* and base::BindOnce(...) correctly |
| // use a weak pointer for its storage. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&InvokeChangeProfileContinuation, |
| std::move(_continuation), _sceneState, |
| base::BindOnce(&UnblurWithDuration, _animation, |
| kAnimationDuration))); |
| |
| // Stop observing the ProfileState and the SceneState. |
| [profileState removeObserver:self]; |
| [_sceneState removeObserver:self]; |
| |
| _sceneState.animator = nil; |
| |
| // Uninstall self as an associated object for the SceneState, as the wait |
| // is complete and the object not needed anymore. |
| objc_setAssociatedObject(_sceneState, [self associationKey], nil, |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| } |
| |
| // Returns a unique pointer that can be used to attach the current instance |
| // to another object as an Objective-C associated object. This pointer has |
| // to be different for each instance of ChangeProfileAnimator. |
| - (void*)associationKey { |
| return &_minimumInitStage; |
| } |
| |
| @end |