| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/layout/animating_layout_manager.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/stl_util.h" |
| #include "ui/gfx/animation/animation_container.h" |
| #include "ui/gfx/animation/slide_animation.h" |
| #include "ui/views/animation/animation_delegate_views.h" |
| #include "ui/views/layout/normalized_geometry.h" |
| #include "ui/views/view.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Returns the ChildLayout data for the child view in the proposed layout, or |
| // nullptr if not found. |
| const ChildLayout* FindChildViewInLayout(const ProposedLayout& layout, |
| const View* view) { |
| if (!view) |
| return nullptr; |
| |
| // The number of children should be small enough that this is more efficient |
| // than caching a lookup set. |
| for (auto& child_layout : layout.child_layouts) { |
| if (child_layout.child_view == view) |
| return &child_layout; |
| } |
| return nullptr; |
| } |
| |
| ChildLayout* FindChildViewInLayout(ProposedLayout* layout, const View* view) { |
| return const_cast<ChildLayout*>(FindChildViewInLayout(*layout, view)); |
| } |
| |
| // Describes the type of fade, used by LayoutFadeInfo (see below). |
| enum class LayoutFadeType { |
| // This view is fading in as part of the current animation. |
| kFadingIn, |
| // This view is fading out as part of the current animation. |
| kFadingOut, |
| // This view was fading as part of a previous animation that was interrupted |
| // and redirected. No child views in the current animation should base their |
| // position off of it. |
| kContinuingFade |
| }; |
| |
| } // namespace |
| |
| // Holds data about a view that is fading in or out as part of an animation. |
| struct AnimatingLayoutManager::LayoutFadeInfo { |
| // How the child view is fading. |
| LayoutFadeType fade_type; |
| // The child view which is fading. |
| View* child_view = nullptr; |
| // The view previous (leading side) to the fading view which is in both the |
| // starting and target layout, or null if none. |
| View* prev_view = nullptr; |
| // The view next (trailing side) to the fading view which is in both the |
| // starting and target layout, or null if none. |
| View* next_view = nullptr; |
| // The full-size bounds, normalized to the orientation of the layout manaer, |
| // that |child_view| starts with, if fading out, or ends with, if fading in. |
| NormalizedRect reference_bounds; |
| // The offset from the end of |prev_view| and the start of |next_view|. Insets |
| // may be negative if the views overlap. |
| Inset1D offsets; |
| }; |
| |
| // Manages the animation and various callbacks from the animation system that |
| // are required to update the layout during animations. |
| class AnimatingLayoutManager::AnimationDelegate |
| : public AnimationDelegateViews { |
| public: |
| explicit AnimationDelegate(AnimatingLayoutManager* layout_manager); |
| ~AnimationDelegate() override = default; |
| |
| // Returns true after the host view is added to a widget or animation has been |
| // enabled by a unit test. |
| // |
| // Before that, animation is not possible, so all changes to the host view |
| // should result in the host view's layout being snapped directly to the |
| // target layout. |
| bool ready_to_animate() const { return ready_to_animate_; } |
| |
| // Pushes animation configuration (tween type, duration) through to the |
| // animation itself. |
| void UpdateAnimationParameters(); |
| |
| // Starts the animation. |
| void Animate(); |
| |
| // Cancels and resets the current animation (if any). |
| void Reset(); |
| |
| // If the current layout is not yet ready to animate, transitions into the |
| // ready-to-animate state, possibly resetting the current layout and |
| // invalidating the host to make sure the layout is up to date. |
| void MakeReadyForAnimation(); |
| |
| private: |
| // Observer used to watch for the host view being parented to a widget. |
| class ViewWidgetObserver : public ViewObserver { |
| public: |
| explicit ViewWidgetObserver(AnimationDelegate* animation_delegate) |
| : animation_delegate_(animation_delegate) {} |
| |
| void OnViewAddedToWidget(View* observed_view) override { |
| animation_delegate_->MakeReadyForAnimation(); |
| } |
| |
| void OnViewIsDeleting(View* observed_view) override { |
| if (animation_delegate_->scoped_observer_.IsObserving(observed_view)) |
| animation_delegate_->scoped_observer_.Remove(observed_view); |
| } |
| |
| private: |
| AnimationDelegate* const animation_delegate_; |
| }; |
| friend class Observer; |
| |
| // AnimationDelegateViews: |
| void AnimationProgressed(const gfx::Animation* animation) override; |
| void AnimationCanceled(const gfx::Animation* animation) override; |
| void AnimationEnded(const gfx::Animation* animation) override; |
| |
| bool ready_to_animate_ = false; |
| bool resetting_animation_ = false; |
| AnimatingLayoutManager* const target_layout_manager_; |
| std::unique_ptr<gfx::SlideAnimation> animation_; |
| ViewWidgetObserver view_widget_observer_{this}; |
| ScopedObserver<View, ViewObserver> scoped_observer_{&view_widget_observer_}; |
| }; |
| |
| AnimatingLayoutManager::AnimationDelegate::AnimationDelegate( |
| AnimatingLayoutManager* layout_manager) |
| : AnimationDelegateViews(layout_manager->host_view()), |
| target_layout_manager_(layout_manager), |
| animation_(std::make_unique<gfx::SlideAnimation>(this)) { |
| animation_->SetContainer(new gfx::AnimationContainer()); |
| View* const host_view = layout_manager->host_view(); |
| DCHECK(host_view); |
| if (host_view->GetWidget()) |
| MakeReadyForAnimation(); |
| else |
| scoped_observer_.Add(host_view); |
| UpdateAnimationParameters(); |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::UpdateAnimationParameters() { |
| animation_->SetTweenType(target_layout_manager_->tween_type()); |
| animation_->SetSlideDuration(target_layout_manager_->animation_duration()); |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::Animate() { |
| DCHECK(ready_to_animate_); |
| Reset(); |
| animation_->Show(); |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::Reset() { |
| if (!ready_to_animate_) |
| return; |
| base::AutoReset<bool> setter(&resetting_animation_, true); |
| animation_->Reset(); |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::MakeReadyForAnimation() { |
| if (!ready_to_animate_) { |
| target_layout_manager_->ResetLayout(); |
| ready_to_animate_ = true; |
| if (scoped_observer_.IsObserving(target_layout_manager_->host_view())) |
| scoped_observer_.Remove(target_layout_manager_->host_view()); |
| } |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::AnimationProgressed( |
| const gfx::Animation* animation) { |
| DCHECK(animation_.get() == animation); |
| target_layout_manager_->AnimateTo(animation->GetCurrentValue()); |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::AnimationCanceled( |
| const gfx::Animation* animation) { |
| AnimationEnded(animation); |
| } |
| |
| void AnimatingLayoutManager::AnimationDelegate::AnimationEnded( |
| const gfx::Animation* animation) { |
| if (resetting_animation_) |
| return; |
| DCHECK(animation_.get() == animation); |
| target_layout_manager_->AnimateTo(1.0); |
| } |
| |
| // AnimatingLayoutManager: |
| |
| AnimatingLayoutManager::AnimatingLayoutManager() = default; |
| AnimatingLayoutManager::~AnimatingLayoutManager() = default; |
| |
| AnimatingLayoutManager& AnimatingLayoutManager::SetShouldAnimateBounds( |
| bool should_animate_bounds) { |
| if (should_animate_bounds_ != should_animate_bounds) { |
| should_animate_bounds_ = should_animate_bounds; |
| ResetLayout(); |
| } |
| return *this; |
| } |
| |
| AnimatingLayoutManager& AnimatingLayoutManager::SetAnimationDuration( |
| base::TimeDelta animation_duration) { |
| DCHECK_GE(animation_duration, base::TimeDelta()); |
| animation_duration_ = animation_duration; |
| if (animation_delegate_) |
| animation_delegate_->UpdateAnimationParameters(); |
| return *this; |
| } |
| |
| AnimatingLayoutManager& AnimatingLayoutManager::SetTweenType( |
| gfx::Tween::Type tween_type) { |
| tween_type_ = tween_type; |
| if (animation_delegate_) |
| animation_delegate_->UpdateAnimationParameters(); |
| return *this; |
| } |
| |
| AnimatingLayoutManager& AnimatingLayoutManager::SetOrientation( |
| LayoutOrientation orientation) { |
| if (orientation_ != orientation) { |
| orientation_ = orientation; |
| ResetLayout(); |
| } |
| return *this; |
| } |
| |
| AnimatingLayoutManager& AnimatingLayoutManager::SetDefaultFadeMode( |
| FadeInOutMode default_fade_mode) { |
| default_fade_mode_ = default_fade_mode; |
| return *this; |
| } |
| |
| void AnimatingLayoutManager::ResetLayout() { |
| if (!target_layout_manager()) |
| return; |
| ResetLayoutToTargetSize(); |
| InvalidateHost(false); |
| } |
| |
| void AnimatingLayoutManager::FadeOut(View* child_view) { |
| DCHECK(child_view); |
| DCHECK(child_view->parent()); |
| DCHECK_EQ(host_view(), child_view->parent()); |
| |
| // If the view in question is already incapable of being visible, either: |
| // 1. the view wasn't capable of being visible in the first place |
| // 2. the view is already invisible because the layout has chosen to hide it |
| // In either case, it is generally useful to recalculate the layout just in |
| // case the caller has made other changes that won't directly cause a layout - |
| // for example, the user has changed a layout-affecting class property. Worst |
| // case this ends up being a slightly costly no-op but we don't expect this |
| // method to be called very often. |
| if (!CanBeVisible(child_view)) { |
| InvalidateHost(true); |
| return; |
| } |
| |
| // Indicate that the view should become hidden in the layout without |
| // immediately changing its visibility. Instead, this triggers an animation |
| // which results in the view being hidden. |
| // |
| // This method is typically only called from View and has a private final |
| // implementation in LayoutManagerBase so we have to cast to call it. |
| static_cast<LayoutManager*>(this)->ViewVisibilitySet( |
| host_view(), child_view, child_view->GetVisible(), false); |
| } |
| |
| void AnimatingLayoutManager::FadeIn(View* child_view) { |
| DCHECK(child_view); |
| DCHECK(child_view->parent()); |
| DCHECK_EQ(host_view(), child_view->parent()); |
| |
| // If the view in question is already capable of being visible, either: |
| // 1. the view is already visible so this is a no-op |
| // 2. the view is not visible because the target layout has chosen to hide it |
| // In either case, it is generally useful to recalculate the layout just in |
| // case the caller has made other changes that won't directly cause a layout - |
| // for example, the user has changed a layout-affecting class property. Worst |
| // case this ends up being a slightly costly no-op but we don't expect this |
| // method to be called very often. |
| if (CanBeVisible(child_view)) { |
| InvalidateHost(true); |
| return; |
| } |
| |
| // Indicate that the view should become visible in the layout without |
| // immediately changing its visibility. Instead, this triggers an animation |
| // which results in the view being shown. |
| // |
| // This method is typically only called from View and has a private final |
| // implementation in LayoutManagerBase so we have to cast to call it. |
| static_cast<LayoutManager*>(this)->ViewVisibilitySet( |
| host_view(), child_view, child_view->GetVisible(), true); |
| } |
| |
| void AnimatingLayoutManager::AddObserver(Observer* observer) { |
| if (!observers_.HasObserver(observer)) |
| observers_.AddObserver(observer); |
| } |
| |
| void AnimatingLayoutManager::RemoveObserver(Observer* observer) { |
| if (observers_.HasObserver(observer)) |
| observers_.RemoveObserver(observer); |
| } |
| |
| bool AnimatingLayoutManager::HasObserver(Observer* observer) const { |
| return observers_.HasObserver(observer); |
| } |
| |
| gfx::Size AnimatingLayoutManager::GetPreferredSize(const View* host) const { |
| if (!target_layout_manager()) |
| return gfx::Size(); |
| |
| return should_animate_bounds_ |
| ? current_layout_.host_size |
| : target_layout_manager()->GetPreferredSize(host); |
| } |
| |
| gfx::Size AnimatingLayoutManager::GetMinimumSize(const View* host) const { |
| if (!target_layout_manager()) |
| return gfx::Size(); |
| // TODO(dfried): consider cases where the minimum size might not be just the |
| // minimum size of the embedded layout. |
| gfx::Size minimum_size = target_layout_manager()->GetMinimumSize(host); |
| if (should_animate_bounds_) |
| minimum_size.SetToMin(current_layout_.host_size); |
| return minimum_size; |
| } |
| |
| int AnimatingLayoutManager::GetPreferredHeightForWidth(const View* host, |
| int width) const { |
| if (!target_layout_manager()) |
| return 0; |
| |
| // TODO(dfried): revisit this computation. |
| return should_animate_bounds_ |
| ? current_layout_.host_size.height() |
| : target_layout_manager()->GetPreferredHeightForWidth(host, width); |
| } |
| |
| std::vector<View*> AnimatingLayoutManager::GetChildViewsInPaintOrder( |
| const View* host) const { |
| DCHECK_EQ(host_view(), host); |
| |
| if (!is_animating()) |
| return LayoutManagerBase::GetChildViewsInPaintOrder(host); |
| |
| std::vector<View*> result; |
| std::set<View*> fading; |
| |
| // Put all fading views to the front of the list (back of the Z-order). |
| for (const LayoutFadeInfo& fade_info : fade_infos_) { |
| result.push_back(fade_info.child_view); |
| fading.insert(fade_info.child_view); |
| } |
| |
| // Add the result of the views. |
| for (View* child : host->children()) { |
| if (!base::Contains(fading, child)) |
| result.push_back(child); |
| } |
| |
| return result; |
| } |
| |
| bool AnimatingLayoutManager::OnViewRemoved(View* host, View* view) { |
| // Remove any fade infos corresponding to the removed view. |
| base::EraseIf(fade_infos_, [view](const LayoutFadeInfo& fade_info) { |
| return fade_info.child_view == view; |
| }); |
| |
| // Remove any elements in the current layout corresponding to the removed |
| // view. |
| base::EraseIf(current_layout_.child_layouts, |
| [view](const ChildLayout& child_layout) { |
| return child_layout.child_view == view; |
| }); |
| |
| return LayoutManagerBase::OnViewRemoved(host, view); |
| } |
| |
| void AnimatingLayoutManager::PostOrQueueAction(base::OnceClosure action) { |
| queued_actions_.push_back(std::move(action)); |
| if (!is_animating()) |
| PostQueuedActions(); |
| } |
| |
| FlexRule AnimatingLayoutManager::GetDefaultFlexRule() const { |
| return base::BindRepeating(&AnimatingLayoutManager::DefaultFlexRuleImpl, |
| base::Unretained(this)); |
| } |
| |
| gfx::AnimationContainer* |
| AnimatingLayoutManager::GetAnimationContainerForTesting() { |
| DCHECK(animation_delegate_); |
| animation_delegate_->MakeReadyForAnimation(); |
| DCHECK(animation_delegate_->ready_to_animate()); |
| return animation_delegate_->container(); |
| } |
| |
| void AnimatingLayoutManager::EnableAnimationForTesting() { |
| DCHECK(animation_delegate_); |
| animation_delegate_->MakeReadyForAnimation(); |
| DCHECK(animation_delegate_->ready_to_animate()); |
| } |
| |
| ProposedLayout AnimatingLayoutManager::CalculateProposedLayout( |
| const SizeBounds& size_bounds) const { |
| // This class directly overrides Layout() so GetProposedLayout() and |
| // CalculateProposedLayout() are not called. |
| NOTREACHED(); |
| return ProposedLayout(); |
| } |
| |
| void AnimatingLayoutManager::OnInstalled(View* host) { |
| DCHECK(!animation_delegate_); |
| animation_delegate_ = std::make_unique<AnimationDelegate>(this); |
| } |
| |
| void AnimatingLayoutManager::OnLayoutChanged() { |
| // This replaces the normal behavior of clearing cached layouts. |
| RecalculateTarget(); |
| } |
| |
| void AnimatingLayoutManager::LayoutImpl() { |
| // Changing the size of a view directly will lead to a layout call rather |
| // than an invalidation. This should reset the layout (but see the note in |
| // RecalculateTarget() below). |
| const gfx::Size host_size = host_view()->size(); |
| if (should_animate_bounds_) { |
| // Reset the layout immediately if the current or target layout exceeds the |
| // host size or the available space. |
| const SizeBounds available_size = GetAvailableHostSize(); |
| const base::Optional<int> bounds_main = |
| GetMainAxis(orientation(), available_size); |
| const int host_main = GetMainAxis(orientation(), host_size); |
| const int current_main = |
| GetMainAxis(orientation(), current_layout_.host_size); |
| if (current_main > host_main || |
| (bounds_main && current_main > *bounds_main)) { |
| last_available_host_size_ = available_size; |
| ResetLayoutToSize(host_size); |
| } else if (available_size != last_available_host_size_) { |
| // May need to re-trigger animation if our bounds were relaxed; let us |
| // expand into the new available space. |
| RecalculateTarget(); |
| } |
| |
| // Verify that the last available size has been updated. |
| DCHECK_EQ(available_size, last_available_host_size_); |
| |
| } else if (!cached_layout_size() || host_size != *cached_layout_size()) { |
| // Host size changed, so reset the layout. |
| ResetLayoutToTargetSize(); |
| } |
| |
| ApplyLayout(current_layout_); |
| |
| // Send animating stopped events on layout so the current layout during the |
| // event represents the final state instead of an intermediate state. |
| if (is_animating_ && current_offset_ == 1.0) |
| OnAnimationEnded(); |
| } |
| |
| void AnimatingLayoutManager::OnAnimationEnded() { |
| DCHECK(is_animating_); |
| is_animating_ = false; |
| fade_infos_.clear(); |
| PostQueuedActions(); |
| NotifyIsAnimatingChanged(); |
| } |
| |
| void AnimatingLayoutManager::ResetLayoutToTargetSize() { |
| ResetLayoutToSize(GetAvailableTargetLayoutSize()); |
| } |
| |
| void AnimatingLayoutManager::ResetLayoutToSize(const gfx::Size& target_size) { |
| if (animation_delegate_) |
| animation_delegate_->Reset(); |
| |
| ResolveFades(); |
| |
| target_layout_ = target_layout_manager()->GetProposedLayout(target_size); |
| current_layout_ = target_layout_; |
| starting_layout_ = current_layout_; |
| fade_infos_.clear(); |
| current_offset_ = 1.0; |
| set_cached_layout_size(target_size); |
| |
| if (is_animating_) |
| OnAnimationEnded(); |
| } |
| |
| bool AnimatingLayoutManager::RecalculateTarget() { |
| constexpr double kResetAnimationThreshold = 0.8; |
| |
| if (!target_layout_manager()) |
| return false; |
| |
| if (!cached_layout_size() || !animation_delegate_ || |
| !animation_delegate_->ready_to_animate()) { |
| ResetLayoutToTargetSize(); |
| return true; |
| } |
| |
| const gfx::Size target_size = GetAvailableTargetLayoutSize(); |
| |
| // For layouts that are confined to available space, changing the available |
| // space causes a fresh layout, not an animation. |
| // TODO(dfried): define a way for views to animate into and out of empty |
| // space as adjacent child views appear/disappear. This will be useful in |
| // animating tab titles, which currently slide over when the favicon |
| // disappears. |
| if (!should_animate_bounds_ && *cached_layout_size() != target_size) { |
| ResetLayoutToSize(target_size); |
| return true; |
| } |
| |
| set_cached_layout_size(target_size); |
| |
| // If there has been no appreciable change in layout, there's no reason to |
| // start or update an animation. |
| const ProposedLayout proposed_layout = |
| target_layout_manager()->GetProposedLayout(target_size); |
| if (target_layout_ == proposed_layout) |
| return false; |
| |
| target_layout_ = proposed_layout; |
| if (current_offset_ > kResetAnimationThreshold) { |
| starting_layout_ = current_layout_; |
| starting_offset_ = 0.0; |
| current_offset_ = 0.0; |
| animation_delegate_->Animate(); |
| if (!is_animating_) { |
| is_animating_ = true; |
| NotifyIsAnimatingChanged(); |
| } |
| } else if (current_offset_ > starting_offset_) { |
| // Only update the starting layout if the animation has progressed. This has |
| // the effect of "batching up" changes that all happen on the same frame, |
| // keeping the same starting point. (A common example of this is multiple |
| // child views' visibility changing.) |
| starting_layout_ = current_layout_; |
| starting_offset_ = current_offset_; |
| } |
| CalculateFadeInfos(); |
| UpdateCurrentLayout(0.0); |
| |
| return true; |
| } |
| |
| void AnimatingLayoutManager::AnimateTo(double value) { |
| DCHECK_GE(value, 0.0); |
| DCHECK_LE(value, 1.0); |
| DCHECK_GE(value, starting_offset_); |
| DCHECK_GE(value, current_offset_); |
| if (current_offset_ == value) |
| return; |
| current_offset_ = value; |
| const double percent = |
| (current_offset_ - starting_offset_) / (1.0 - starting_offset_); |
| UpdateCurrentLayout(percent); |
| InvalidateHost(false); |
| } |
| |
| void AnimatingLayoutManager::NotifyIsAnimatingChanged() { |
| for (auto& observer : observers_) |
| observer.OnLayoutIsAnimatingChanged(this, is_animating()); |
| } |
| |
| void AnimatingLayoutManager::RunQueuedActions() { |
| run_queued_actions_is_pending_ = false; |
| std::vector<base::OnceClosure> actions = std::move(queued_actions_to_run_); |
| for (auto& action : actions) |
| std::move(action).Run(); |
| } |
| |
| void AnimatingLayoutManager::PostQueuedActions() { |
| // Move queued actions over to actions that should run during the next |
| // PostTask(). This prevents a race between old PostTask() calls and new |
| // delayed actions. See the header for more detail. |
| for (auto& action : queued_actions_) |
| queued_actions_to_run_.push_back(std::move(action)); |
| queued_actions_.clear(); |
| |
| // Early return to prevent multiple RunQueuedAction() tasks. |
| if (run_queued_actions_is_pending_) |
| return; |
| |
| // Post to self (instead of posting the queued actions directly) which lets |
| // us: |
| // * Keep "AnimatingLayoutManager::RunDelayedActions" in the stack frame. |
| // * Tie the task lifetimes to AnimatingLayoutManager. |
| run_queued_actions_is_pending_ = |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(&AnimatingLayoutManager::RunQueuedActions, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void AnimatingLayoutManager::UpdateCurrentLayout(double percent) { |
| // This drops out any child view elements that don't exist in the target |
| // layout. We'll add them back in later. |
| current_layout_ = |
| ProposedLayoutBetween(percent, starting_layout_, target_layout_); |
| |
| for (const LayoutFadeInfo& fade_info : fade_infos_) { |
| // This shouldn't happen but we should ensure that with a check. |
| DCHECK_NE(-1, host_view()->GetIndexOf(fade_info.child_view)); |
| |
| // Views that were previously fading are animated as normal, so nothing to |
| // do here. |
| if (fade_info.fade_type == LayoutFadeType::kContinuingFade) |
| continue; |
| |
| ChildLayout child_layout; |
| |
| if (percent == 1.0) { |
| // At the end of the animation snap to the final state of the child view. |
| child_layout.child_view = fade_info.child_view; |
| switch (fade_info.fade_type) { |
| case LayoutFadeType::kFadingIn: |
| child_layout.visible = true; |
| child_layout.bounds = |
| Denormalize(orientation(), fade_info.reference_bounds); |
| break; |
| case LayoutFadeType::kFadingOut: |
| child_layout.visible = false; |
| break; |
| case LayoutFadeType::kContinuingFade: |
| NOTREACHED(); |
| continue; |
| } |
| } else if (default_fade_mode_ == FadeInOutMode::kHide) { |
| child_layout.child_view = fade_info.child_view; |
| child_layout.visible = false; |
| } else { |
| const double scale_percent = |
| fade_info.fade_type == LayoutFadeType::kFadingIn ? percent |
| : 1.0 - percent; |
| |
| switch (default_fade_mode_) { |
| case FadeInOutMode::kHide: |
| NOTREACHED(); |
| break; |
| case FadeInOutMode::kScaleFromMinimum: |
| child_layout = CalculateScaleFade(fade_info, scale_percent, |
| /* scale_from_zero */ false); |
| break; |
| case FadeInOutMode::kScaleFromZero: |
| child_layout = CalculateScaleFade(fade_info, scale_percent, |
| /* scale_from_zero */ true); |
| break; |
| case FadeInOutMode::kSlideFromLeadingEdge: |
| child_layout = CalculateSlideFade(fade_info, scale_percent, |
| /* slide_from_leading */ true); |
| break; |
| case FadeInOutMode::kSlideFromTrailingEdge: |
| child_layout = CalculateSlideFade(fade_info, scale_percent, |
| /* slide_from_leading */ false); |
| break; |
| } |
| } |
| |
| ChildLayout* const to_overwrite = |
| FindChildViewInLayout(¤t_layout_, fade_info.child_view); |
| if (to_overwrite) |
| *to_overwrite = child_layout; |
| else |
| current_layout_.child_layouts.push_back(child_layout); |
| } |
| } |
| |
| void AnimatingLayoutManager::CalculateFadeInfos() { |
| // Save any views that were previously fading so we don't try to key off of |
| // them when calculating leading/trailing edge. |
| std::set<const View*> previously_fading; |
| for (const auto& fade_info : fade_infos_) |
| previously_fading.insert(fade_info.child_view); |
| |
| fade_infos_.clear(); |
| |
| struct ChildInfo { |
| base::Optional<size_t> start; |
| NormalizedRect start_bounds; |
| bool start_visible = false; |
| base::Optional<size_t> target; |
| NormalizedRect target_bounds; |
| bool target_visible = false; |
| }; |
| |
| std::map<View*, ChildInfo> child_to_info; |
| std::map<int, View*> start_leading_edges; |
| std::map<int, View*> target_leading_edges; |
| |
| // Collect some bookkeping info to prevent linear searches later. |
| |
| for (View* child : host_view()->children()) { |
| if (IsChildIncludedInLayout(child, /* include hidden */ true)) |
| child_to_info.emplace(child, ChildInfo()); |
| } |
| |
| for (size_t i = 0; i < starting_layout_.child_layouts.size(); ++i) { |
| const auto& child_layout = starting_layout_.child_layouts[i]; |
| auto it = child_to_info.find(child_layout.child_view); |
| if (it != child_to_info.end()) { |
| it->second.start = i; |
| it->second.start_bounds = Normalize(orientation(), child_layout.bounds); |
| it->second.start_visible = child_layout.visible; |
| } |
| } |
| |
| for (size_t i = 0; i < target_layout_.child_layouts.size(); ++i) { |
| const auto& child_layout = target_layout_.child_layouts[i]; |
| auto it = child_to_info.find(child_layout.child_view); |
| if (it != child_to_info.end()) { |
| it->second.target = i; |
| it->second.target_bounds = Normalize(orientation(), child_layout.bounds); |
| it->second.target_visible = child_layout.visible; |
| } |
| } |
| |
| for (View* child : host_view()->children()) { |
| const auto& index = child_to_info[child]; |
| if (index.start_visible && index.target_visible && |
| !base::Contains(previously_fading, child)) { |
| start_leading_edges.emplace(index.start_bounds.origin_main(), child); |
| target_leading_edges.emplace(index.target_bounds.origin_main(), child); |
| } |
| } |
| |
| // Build the LayoutFadeInfo data. |
| |
| const NormalizedSize start_host_size = |
| Normalize(orientation(), starting_layout_.host_size); |
| const NormalizedSize target_host_size = |
| Normalize(orientation(), target_layout_.host_size); |
| |
| for (View* child : host_view()->children()) { |
| const auto& current = child_to_info[child]; |
| if (current.start_visible && !current.target_visible) { |
| LayoutFadeInfo fade_info; |
| fade_info.fade_type = LayoutFadeType::kFadingOut; |
| fade_info.child_view = child; |
| fade_info.reference_bounds = current.start_bounds; |
| auto next = |
| start_leading_edges.upper_bound(current.start_bounds.origin_main()); |
| if (next == start_leading_edges.end()) { |
| fade_info.next_view = nullptr; |
| fade_info.offsets.set_trailing(start_host_size.main() - |
| current.start_bounds.max_main()); |
| } else { |
| fade_info.next_view = next->second; |
| fade_info.offsets.set_trailing(next->first - |
| current.start_bounds.max_main()); |
| } |
| if (next == start_leading_edges.begin()) { |
| fade_info.prev_view = nullptr; |
| fade_info.offsets.set_leading(current.start_bounds.origin_main()); |
| } else { |
| auto prev = next; |
| --prev; |
| const auto& prev_info = child_to_info[prev->second]; |
| fade_info.prev_view = prev->second; |
| fade_info.offsets.set_leading(current.start_bounds.origin_main() - |
| prev_info.start_bounds.max_main()); |
| } |
| fade_infos_.push_back(fade_info); |
| } else if (!current.start_visible && current.target_visible) { |
| LayoutFadeInfo fade_info; |
| fade_info.fade_type = LayoutFadeType::kFadingIn; |
| fade_info.child_view = child; |
| fade_info.reference_bounds = current.target_bounds; |
| auto next = |
| target_leading_edges.upper_bound(current.target_bounds.origin_main()); |
| if (next == target_leading_edges.end()) { |
| fade_info.next_view = nullptr; |
| fade_info.offsets.set_trailing(target_host_size.main() - |
| current.target_bounds.max_main()); |
| } else { |
| fade_info.next_view = next->second; |
| fade_info.offsets.set_trailing(next->first - |
| current.target_bounds.max_main()); |
| } |
| if (next == target_leading_edges.begin()) { |
| fade_info.prev_view = nullptr; |
| fade_info.offsets.set_leading(current.target_bounds.origin_main()); |
| } else { |
| auto prev = next; |
| --prev; |
| const auto& prev_info = child_to_info[prev->second]; |
| fade_info.prev_view = prev->second; |
| fade_info.offsets.set_leading(current.target_bounds.origin_main() - |
| prev_info.target_bounds.max_main()); |
| } |
| fade_infos_.push_back(fade_info); |
| } else if (base::Contains(previously_fading, child)) { |
| // Capture the fact that this view was fading as part of an animation that |
| // was interrupted. (It is therefore technically still fading.) This |
| // status goes away when the animation ends. |
| LayoutFadeInfo fade_info; |
| fade_info.fade_type = LayoutFadeType::kContinuingFade; |
| fade_info.child_view = child; |
| // No reference bounds or offsets since we'll use the normal animation |
| // pathway for this view. |
| fade_infos_.push_back(fade_info); |
| } |
| } |
| } |
| |
| void AnimatingLayoutManager::ResolveFades() { |
| // Views that need faded out are views which were were fading out previously |
| // because they were set to not be visible, either by calling SetVisible() or |
| // FadeOut(). Those views will not be included in the new layout but may not |
| // have been allowed to become invisible yet because of the fade-out |
| // animation. Even in the case of FadeInOutMode::kHide, if no frames of the |
| // animation have run, the relevant view may still be visible. |
| for (const LayoutFadeInfo& fade_info : fade_infos_) { |
| View* const child = fade_info.child_view; |
| if (fade_info.fade_type == LayoutFadeType::kFadingOut && |
| host_view()->GetIndexOf(child) >= 0 && |
| !IsChildViewIgnoredByLayout(child) && !IsChildIncludedInLayout(child)) { |
| SetViewVisibility(child, false); |
| } |
| } |
| } |
| |
| ChildLayout AnimatingLayoutManager::CalculateScaleFade( |
| const LayoutFadeInfo& fade_info, |
| double scale_percent, |
| bool scale_from_zero) const { |
| ChildLayout child_layout; |
| |
| int leading_reference_point = 0; |
| if (fade_info.prev_view) { |
| // Since prev/next view is always a view in the start and target layouts, it |
| // should also be in the current layout. Therefore this should never return |
| // null. |
| const ChildLayout* const prev_layout = |
| FindChildViewInLayout(current_layout_, fade_info.prev_view); |
| leading_reference_point = |
| Normalize(orientation(), prev_layout->bounds).max_main(); |
| } |
| leading_reference_point += fade_info.offsets.leading(); |
| |
| int trailing_reference_point; |
| if (fade_info.next_view) { |
| // Since prev/next view is always a view in the start and target layouts, it |
| // should also be in the current layout. Therefore this should never return |
| // null. |
| const ChildLayout* const next_layout = |
| FindChildViewInLayout(current_layout_, fade_info.next_view); |
| trailing_reference_point = |
| Normalize(orientation(), next_layout->bounds).origin_main(); |
| } else { |
| trailing_reference_point = |
| Normalize(orientation(), current_layout_.host_size).main(); |
| } |
| trailing_reference_point -= fade_info.offsets.trailing(); |
| |
| const int new_size = |
| std::min(int{scale_percent * fade_info.reference_bounds.size_main()}, |
| trailing_reference_point - leading_reference_point); |
| |
| child_layout.child_view = fade_info.child_view; |
| if (new_size > 0 && |
| (scale_from_zero || |
| new_size >= |
| Normalize(orientation(), fade_info.child_view->GetMinimumSize()) |
| .main())) { |
| child_layout.visible = true; |
| NormalizedRect new_bounds = fade_info.reference_bounds; |
| switch (fade_info.fade_type) { |
| case LayoutFadeType::kFadingIn: |
| new_bounds.set_origin_main(leading_reference_point); |
| break; |
| case LayoutFadeType::kFadingOut: |
| new_bounds.set_origin_main(trailing_reference_point - new_size); |
| break; |
| case LayoutFadeType::kContinuingFade: |
| NOTREACHED(); |
| break; |
| } |
| new_bounds.set_size_main(new_size); |
| child_layout.bounds = Denormalize(orientation(), new_bounds); |
| } |
| |
| return child_layout; |
| } |
| |
| ChildLayout AnimatingLayoutManager::CalculateSlideFade( |
| const LayoutFadeInfo& fade_info, |
| double scale_percent, |
| bool slide_from_leading) const { |
| // Fall back to kScaleFromMinimum if there is no edge to slide out from. |
| if (!fade_info.prev_view && !fade_info.next_view) |
| return CalculateScaleFade(fade_info, scale_percent, false); |
| |
| // Slide from the other direction if against the edge of the host view. |
| if (slide_from_leading && !fade_info.prev_view) |
| slide_from_leading = false; |
| else if (!slide_from_leading && !fade_info.next_view) |
| slide_from_leading = true; |
| |
| NormalizedRect new_bounds = fade_info.reference_bounds; |
| |
| // Determine which layout the sliding view will be completely faded in. |
| const ProposedLayout* fully_faded_layout; |
| switch (fade_info.fade_type) { |
| case LayoutFadeType::kFadingIn: |
| fully_faded_layout = &starting_layout_; |
| break; |
| case LayoutFadeType::kFadingOut: |
| fully_faded_layout = &target_layout_; |
| break; |
| case LayoutFadeType::kContinuingFade: |
| NOTREACHED(); |
| break; |
| } |
| |
| if (slide_from_leading) { |
| // Get the layout info for the leading child. |
| const ChildLayout* const leading_child = |
| FindChildViewInLayout(*fully_faded_layout, fade_info.prev_view); |
| |
| // This is the right side of the leading control that will eclipse the |
| // sliding view at the start/end of the animation. |
| const int initial_trailing = |
| Normalize(orientation(), leading_child->bounds).max_main(); |
| |
| // Interpolate between initial and final trailing edge. |
| const int new_trailing = gfx::Tween::IntValueBetween( |
| scale_percent, initial_trailing, new_bounds.max_main()); |
| |
| // Adjust the bounding rectangle of the view. |
| new_bounds.Offset(new_trailing - new_bounds.max_main(), 0); |
| |
| } else { |
| // Get the layout info for the trailing child. |
| const ChildLayout* const trailing_child = |
| FindChildViewInLayout(*fully_faded_layout, fade_info.next_view); |
| |
| // This is the left side of the trailing control that will eclipse the |
| // sliding view at the start/end of the animation. |
| const int initial_leading = |
| Normalize(orientation(), trailing_child->bounds).origin_main(); |
| |
| // Interpolate between initial and final leading edge. |
| const int new_leading = gfx::Tween::IntValueBetween( |
| scale_percent, initial_leading, new_bounds.origin_main()); |
| |
| // Adjust the bounding rectangle of the view. |
| new_bounds.Offset(new_leading - new_bounds.origin_main(), 0); |
| } |
| |
| // Actual bounds are a linear interpolation between starting and reference |
| // bounds. |
| ChildLayout child_layout; |
| child_layout.child_view = fade_info.child_view; |
| child_layout.visible = true; |
| child_layout.bounds = Denormalize(orientation(), new_bounds); |
| |
| return child_layout; |
| } |
| |
| // Returns the space in which to calculate the target layout. |
| gfx::Size AnimatingLayoutManager::GetAvailableTargetLayoutSize() { |
| if (!should_animate_bounds_) |
| return host_view()->size(); |
| |
| const SizeBounds bounds = GetAvailableHostSize(); |
| last_available_host_size_ = bounds; |
| const gfx::Size preferred_size = |
| target_layout_manager()->GetPreferredSize(host_view()); |
| if (!bounds.width() || *bounds.width() > preferred_size.width()) { |
| return gfx::Size(preferred_size.width(), |
| bounds.height() |
| ? std::min(preferred_size.height(), *bounds.height()) |
| : preferred_size.height()); |
| } |
| |
| const int height = target_layout_manager()->GetPreferredHeightForWidth( |
| host_view(), *bounds.width()); |
| return gfx::Size(*bounds.width(), bounds.height() |
| ? std::min(height, *bounds.height()) |
| : height); |
| } |
| |
| // static |
| gfx::Size AnimatingLayoutManager::DefaultFlexRuleImpl( |
| const AnimatingLayoutManager* animating_layout, |
| const View* view, |
| const SizeBounds& size_bounds) { |
| DCHECK_EQ(view->GetLayoutManager(), animating_layout); |
| |
| // This is the current preferred size, which takes animation into account. |
| const gfx::Size preferred_size = animating_layout->GetPreferredSize(view); |
| |
| // Does the preferred size fit in the bounds? If so, return the preferred |
| // size. Note that the *target* size might not fit in the bounds, but we'll |
| // recalculate that the next time we lay out. |
| if (CanFitInBounds(preferred_size, size_bounds)) |
| return preferred_size; |
| |
| // Special case - if we're being asked for a zero-size layout we'll return the |
| // minimum size of the layout. This is because we're being probed for how |
| // small we can get, not being asked for an actual size. |
| const base::Optional<int> bounds_main = |
| GetMainAxis(animating_layout->orientation(), size_bounds); |
| if (bounds_main && *bounds_main <= 0) |
| return animating_layout->GetMinimumSize(view); |
| |
| // We know our current size does not fit into the bounds being given to us. |
| // This is going to force a snap to a new size, which will be the ideal size |
| // of the target layout in the provided space. |
| const LayoutManagerBase* const target_layout = |
| animating_layout->target_layout_manager(); |
| |
| // Easiest case is that the target layout's preferred size *does* fit, in |
| // which case we can use that. |
| const gfx::Size target_preferred = target_layout->GetPreferredSize(view); |
| if (CanFitInBounds(target_preferred, size_bounds)) |
| return target_preferred; |
| |
| // We know that at least one of the width and height are constrained, so we |
| // need to ask the target layout how large it wants to be in the space |
| // provided. |
| gfx::Size size; |
| if (size_bounds.width() && size_bounds.height()) { |
| // Both width and height are specified. Constraining the width may change |
| // the desired height, so we can't just blindly return the minimum in both |
| // dimensions. Instead, query the target layout in the constrained space |
| // and return its size. |
| size = gfx::Size(*size_bounds.width(), *size_bounds.height()); |
| } else if (size_bounds.width()) { |
| // The width is specified and too small. Use the height-for-width |
| // calculation. |
| // TODO(dfried): This should be rare, but it is also inefficient. See if we |
| // can't add an alternative to GetPreferredHeightForWidth() that actually |
| // calculates the layout in this space so we don't have to do it twice. |
| const int width = *size_bounds.width(); |
| size = gfx::Size(width, |
| target_layout->GetPreferredHeightForWidth(view, width)); |
| } else { |
| DCHECK(size_bounds.height()); |
| // The height is specified and too small. Fortunately the height of a |
| // layout can't (shouldn't?) affect its width. |
| size = gfx::Size(target_preferred.width(), *size_bounds.height()); |
| } |
| |
| return target_layout->GetProposedLayout(size).host_size; |
| } |
| |
| } // namespace views |