blob: dcbfb82f6cf242bedde37aa5b9f94c1cfc189581 [file] [log] [blame]
// 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