| // 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 "ash/app_list/views/assistant/assistant_main_stage.h" |
| |
| #include "ash/assistant/model/assistant_interaction_model.h" |
| #include "ash/assistant/model/assistant_query.h" |
| #include "ash/assistant/model/assistant_ui_model.h" |
| #include "ash/assistant/ui/assistant_ui_constants.h" |
| #include "ash/assistant/ui/assistant_view_delegate.h" |
| #include "ash/assistant/ui/assistant_view_ids.h" |
| #include "ash/assistant/ui/base/stack_layout.h" |
| #include "ash/assistant/ui/main_stage/assistant_footer_view.h" |
| #include "ash/assistant/ui/main_stage/assistant_progress_indicator.h" |
| #include "ash/assistant/ui/main_stage/assistant_query_view.h" |
| #include "ash/assistant/ui/main_stage/assistant_zero_state_view.h" |
| #include "ash/assistant/ui/main_stage/ui_element_container_view.h" |
| #include "ash/assistant/util/animation_util.h" |
| #include "ash/assistant/util/assistant_util.h" |
| #include "ash/public/cpp/assistant/controller/assistant_interaction_controller.h" |
| #include "ash/public/cpp/assistant/controller/assistant_ui_controller.h" |
| #include "base/bind.h" |
| #include "base/time/time.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/layer_animation_element.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/layout_manager.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using assistant::util::CreateLayerAnimationSequence; |
| using assistant::util::CreateOpacityElement; |
| using assistant::util::CreateTransformElement; |
| |
| // Appearance. |
| constexpr int kSeparatorThicknessDip = 1; |
| constexpr int kSeparatorWidthDip = 64; |
| |
| // Footer entry animation. |
| constexpr base::TimeDelta kFooterEntryAnimationFadeInDelay = |
| base::TimeDelta::FromMilliseconds(283); |
| constexpr base::TimeDelta kFooterEntryAnimationFadeInDuration = |
| base::TimeDelta::FromMilliseconds(167); |
| |
| // Divider animation. |
| constexpr base::TimeDelta kDividerAnimationFadeInDelay = |
| base::TimeDelta::FromMilliseconds(233); |
| constexpr base::TimeDelta kDividerAnimationFadeInDuration = |
| base::TimeDelta::FromMilliseconds(167); |
| constexpr base::TimeDelta kDividerAnimationFadeOutDuration = |
| base::TimeDelta::FromMilliseconds(83); |
| |
| // Zero state animation. |
| constexpr base::TimeDelta kZeroStateAnimationFadeOutDuration = |
| base::TimeDelta::FromMilliseconds(83); |
| constexpr int kZeroStateAnimationTranslationDip = 115; |
| constexpr base::TimeDelta kZeroStateAnimationFadeInDelay = |
| base::TimeDelta::FromMilliseconds(33); |
| constexpr base::TimeDelta kZeroStateAnimationFadeInDuration = |
| base::TimeDelta::FromMilliseconds(167); |
| constexpr base::TimeDelta kZeroStateAnimationTranslateUpDuration = |
| base::TimeDelta::FromMilliseconds(250); |
| |
| // HorizontalSeparator --------------------------------------------------------- |
| |
| // A horizontal line to separate the dialog plate. |
| class HorizontalSeparator : public views::View { |
| public: |
| explicit HorizontalSeparator(int preferred_width, int preferred_height) |
| : preferred_width_(preferred_width), |
| preferred_height_(preferred_height) {} |
| |
| ~HorizontalSeparator() override = default; |
| |
| // views::View overrides: |
| const char* GetClassName() const override { return "HorizontalSeparator"; } |
| |
| gfx::Size CalculatePreferredSize() const override { |
| return gfx::Size(preferred_width_, preferred_height_); |
| } |
| |
| void OnPaint(gfx::Canvas* canvas) override { |
| gfx::Rect draw_bounds(GetContentsBounds()); |
| draw_bounds.Inset(0, (draw_bounds.height() - kSeparatorThicknessDip) / 2); |
| canvas->FillRect(draw_bounds, gfx::kGoogleGrey300); |
| } |
| |
| private: |
| const int preferred_width_; |
| const int preferred_height_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HorizontalSeparator); |
| }; |
| |
| // A view is considered shown when it is visible and not in the process of |
| // fading out. |
| bool IsShown(const views::View* view) { |
| DCHECK(view->layer()); |
| bool is_fading_out = |
| cc::MathUtil::IsWithinEpsilon(view->layer()->GetTargetOpacity(), 0.f); |
| |
| return view->GetVisible() && !is_fading_out; |
| } |
| |
| } // namespace |
| |
| // AppListAssistantMainStage --------------------------------------------------- |
| |
| AppListAssistantMainStage::AppListAssistantMainStage( |
| AssistantViewDelegate* delegate) |
| : delegate_(delegate) { |
| SetID(AssistantViewID::kMainStage); |
| InitLayout(); |
| |
| assistant_controller_observer_.Add(AssistantController::Get()); |
| AssistantInteractionController::Get()->GetModel()->AddObserver(this); |
| AssistantUiController::Get()->GetModel()->AddObserver(this); |
| } |
| |
| AppListAssistantMainStage::~AppListAssistantMainStage() { |
| if (AssistantUiController::Get()) |
| AssistantUiController::Get()->GetModel()->RemoveObserver(this); |
| |
| if (AssistantInteractionController::Get()) |
| AssistantInteractionController::Get()->GetModel()->RemoveObserver(this); |
| } |
| |
| const char* AppListAssistantMainStage::GetClassName() const { |
| return "AppListAssistantMainStage"; |
| } |
| |
| void AppListAssistantMainStage::ChildPreferredSizeChanged(views::View* child) { |
| PreferredSizeChanged(); |
| } |
| |
| void AppListAssistantMainStage::OnViewPreferredSizeChanged(views::View* view) { |
| PreferredSizeChanged(); |
| Layout(); |
| SchedulePaint(); |
| } |
| |
| void AppListAssistantMainStage::InitLayout() { |
| // The children of AppListAssistantMainStage will be animated on their own |
| // layers and we want them to be clipped by their parent layer. |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| layer()->SetMasksToBounds(true); |
| |
| views::BoxLayout* layout = |
| SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter); |
| layout->set_cross_axis_alignment( |
| views::BoxLayout::CrossAxisAlignment::kCenter); |
| |
| layout->SetFlexForView(AddChildView(CreateContentLayoutContainer()), 1); |
| |
| AddChildView(CreateFooterLayoutContainer()); |
| } |
| |
| std::unique_ptr<views::View> |
| AppListAssistantMainStage::CreateContentLayoutContainer() { |
| // The content layout container stacks two views. |
| // On top is a main content container including the line separator, progress |
| // indicator query view and |ui_element_container_|. |
| // The |zero_state_view_| is laid out above of the main content container. As |
| // such, it floats above and does not cause repositioning to any of content |
| // layout's underlying views. |
| auto content_layout_container = std::make_unique<views::View>(); |
| |
| auto* stack_layout = content_layout_container->SetLayoutManager( |
| std::make_unique<StackLayout>()); |
| |
| auto* main_content_layout_container = content_layout_container->AddChildView( |
| CreateMainContentLayoutContainer()); |
| |
| // Do not respect height, otherwise bounds will not be set correctly for |
| // scrolling. |
| stack_layout->SetRespectDimensionForView( |
| main_content_layout_container, StackLayout::RespectDimension::kWidth); |
| |
| // Zero state, which will be animated on its own layer. |
| zero_state_view_ = content_layout_container->AddChildView( |
| std::make_unique<AssistantZeroStateView>(delegate_)); |
| zero_state_view_->SetPaintToLayer(); |
| zero_state_view_->layer()->SetFillsBoundsOpaquely(false); |
| |
| stack_layout->SetVerticalAlignmentForView( |
| zero_state_view_, StackLayout::VerticalAlignment::kCenter); |
| |
| return content_layout_container; |
| } |
| |
| std::unique_ptr<views::View> |
| AppListAssistantMainStage::CreateMainContentLayoutContainer() { |
| auto content_layout_container = std::make_unique<views::View>(); |
| views::BoxLayout* content_layout = content_layout_container->SetLayoutManager( |
| std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| content_layout->set_main_axis_alignment( |
| views::BoxLayout::MainAxisAlignment::kCenter); |
| content_layout->set_cross_axis_alignment( |
| views::BoxLayout::CrossAxisAlignment::kCenter); |
| |
| content_layout_container->AddChildView(CreateDividerLayoutContainer()); |
| |
| // Query view. Will be animated on its own layer. |
| query_view_ = content_layout_container->AddChildView( |
| std::make_unique<AssistantQueryView>()); |
| query_view_->SetPaintToLayer(); |
| query_view_->SetBackground(views::CreateSolidBackground(SK_ColorWHITE)); |
| query_view_->AddObserver(this); |
| |
| // UI element container. |
| ui_element_container_ = content_layout_container->AddChildView( |
| std::make_unique<UiElementContainerView>(delegate_)); |
| ui_element_container_->AddObserver(this); |
| content_layout->SetFlexForView(ui_element_container_, 1, |
| /*use_min_size=*/true); |
| |
| return content_layout_container; |
| } |
| |
| std::unique_ptr<views::View> |
| AppListAssistantMainStage::CreateDividerLayoutContainer() { |
| // Dividers: the progress indicator and the horizontal separator will be the |
| // separator when querying and showing the results, respectively. |
| auto divider_container = std::make_unique<views::View>(); |
| divider_container->SetLayoutManager(std::make_unique<StackLayout>()); |
| |
| // Progress indicator, which will be animated on its own layer. |
| progress_indicator_ = divider_container->AddChildView( |
| std::make_unique<AssistantProgressIndicator>()); |
| progress_indicator_->SetPaintToLayer(); |
| progress_indicator_->layer()->SetFillsBoundsOpaquely(false); |
| |
| // Horizontal separator, which will be animated on its own layer. |
| horizontal_separator_ = |
| divider_container->AddChildView(std::make_unique<HorizontalSeparator>( |
| kSeparatorWidthDip, |
| progress_indicator_->GetPreferredSize().height())); |
| horizontal_separator_->SetPaintToLayer(); |
| horizontal_separator_->layer()->SetFillsBoundsOpaquely(false); |
| |
| return divider_container; |
| } |
| |
| std::unique_ptr<views::View> |
| AppListAssistantMainStage::CreateFooterLayoutContainer() { |
| // Footer. |
| // Note that the |footer_| is placed within its own view container so that as |
| // its visibility changes, its parent container will still reserve the same |
| // layout space. This prevents jank that would otherwise occur due to |
| // |ui_element_container_| claiming that empty space. |
| auto footer_container = std::make_unique<views::View>(); |
| footer_container->SetLayoutManager(std::make_unique<views::FillLayout>()); |
| |
| footer_ = footer_container->AddChildView( |
| std::make_unique<AssistantFooterView>(delegate_)); |
| footer_->AddObserver(this); |
| |
| // The footer will be animated on its own layer. |
| footer_->SetPaintToLayer(); |
| footer_->layer()->SetFillsBoundsOpaquely(false); |
| |
| return footer_container; |
| } |
| |
| void AppListAssistantMainStage::AnimateInZeroState() { |
| zero_state_view_->layer()->GetAnimator()->StopAnimating(); |
| |
| // We're going to animate the zero state view up into position so we'll need |
| // to apply an initial transformation. |
| gfx::Transform transform; |
| transform.Translate(0, kZeroStateAnimationTranslationDip); |
| |
| // Set up our pre-animation values. |
| zero_state_view_->layer()->SetOpacity(0.f); |
| zero_state_view_->layer()->SetTransform(transform); |
| zero_state_view_->SetVisible(true); |
| |
| // Start animating the zero state view. |
| zero_state_view_->layer()->GetAnimator()->StartTogether( |
| {// Animate the transformation. |
| CreateLayerAnimationSequence(CreateTransformElement( |
| gfx::Transform(), kZeroStateAnimationTranslateUpDuration, |
| gfx::Tween::Type::FAST_OUT_SLOW_IN_2)), |
| // Animate the opacity to 100% with delay. |
| CreateLayerAnimationSequence( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::AnimatableProperty::OPACITY, |
| kZeroStateAnimationFadeInDelay), |
| CreateOpacityElement(1.f, kZeroStateAnimationFadeInDuration))}); |
| } |
| |
| void AppListAssistantMainStage::AnimateInFooter() { |
| // Set up our pre-animation values. |
| footer_->layer()->SetOpacity(0.f); |
| |
| // Animate the footer to 100% opacity with delay. |
| footer_->layer()->GetAnimator()->StartAnimation(CreateLayerAnimationSequence( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::AnimatableProperty::OPACITY, |
| kFooterEntryAnimationFadeInDelay), |
| CreateOpacityElement(1.f, kFooterEntryAnimationFadeInDuration))); |
| } |
| |
| void AppListAssistantMainStage::OnAssistantControllerDestroying() { |
| AssistantUiController::Get()->GetModel()->RemoveObserver(this); |
| AssistantInteractionController::Get()->GetModel()->RemoveObserver(this); |
| assistant_controller_observer_.Remove(AssistantController::Get()); |
| } |
| |
| void AppListAssistantMainStage::OnCommittedQueryChanged( |
| const AssistantQuery& query) { |
| // Update the view. |
| query_view_->SetQuery(query); |
| |
| // If query is empty and we are showing zero state, do not update the Ui. |
| if (query.Empty() && IsShown(zero_state_view_)) |
| return; |
| |
| // Hide the horizontal separator. |
| horizontal_separator_->layer()->GetAnimator()->StartAnimation( |
| CreateLayerAnimationSequence( |
| CreateOpacityElement(0.f, kDividerAnimationFadeOutDuration))); |
| |
| // Show the progress indicator. |
| progress_indicator_->layer()->GetAnimator()->StartAnimation( |
| CreateLayerAnimationSequence( |
| // Delay... |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::AnimatableProperty::OPACITY, |
| kDividerAnimationFadeInDelay), |
| // ...then fade in. |
| CreateOpacityElement(1.f, kDividerAnimationFadeInDuration))); |
| |
| MaybeHideZeroState(); |
| } |
| |
| void AppListAssistantMainStage::OnPendingQueryChanged( |
| const AssistantQuery& query) { |
| // Update the view. |
| query_view_->SetQuery(query); |
| |
| if (!IsShown(zero_state_view_)) |
| return; |
| |
| // Animate the opacity to 100% with delay equal to |zero_state_view_| fade out |
| // animation duration to avoid the two views displaying at the same time. |
| constexpr base::TimeDelta kQueryAnimationFadeInDuration = |
| base::TimeDelta::FromMilliseconds(433); |
| query_view_->layer()->SetOpacity(0.f); |
| query_view_->layer()->GetAnimator()->StartAnimation( |
| CreateLayerAnimationSequence( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::AnimatableProperty::OPACITY, |
| kZeroStateAnimationFadeOutDuration), |
| CreateOpacityElement(1.f, kQueryAnimationFadeInDuration))); |
| |
| if (!query.Empty()) |
| MaybeHideZeroState(); |
| } |
| |
| void AppListAssistantMainStage::OnPendingQueryCleared(bool due_to_commit) { |
| // When a pending query is cleared, it may be because the interaction was |
| // cancelled, or because the query was committed. If the query was committed, |
| // reseting the query here will have no visible effect. If the interaction was |
| // cancelled, we set the query here to restore the previously committed query. |
| query_view_->SetQuery( |
| AssistantInteractionController::Get()->GetModel()->committed_query()); |
| } |
| |
| void AppListAssistantMainStage::OnResponseChanged( |
| const scoped_refptr<AssistantResponse>& response) { |
| MaybeHideZeroState(); |
| |
| // Show the horizontal separator. |
| horizontal_separator_->layer()->GetAnimator()->StartAnimation( |
| CreateLayerAnimationSequence( |
| // Delay... |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::AnimatableProperty::OPACITY, |
| kDividerAnimationFadeInDelay), |
| // ...then fade in. |
| CreateOpacityElement(1.f, kDividerAnimationFadeInDuration))); |
| |
| // Hide the progress indicator. |
| progress_indicator_->layer()->GetAnimator()->StartAnimation( |
| CreateLayerAnimationSequence( |
| CreateOpacityElement(0.f, kDividerAnimationFadeOutDuration))); |
| } |
| |
| void AppListAssistantMainStage::OnUiVisibilityChanged( |
| AssistantVisibility new_visibility, |
| AssistantVisibility old_visibility, |
| base::Optional<AssistantEntryPoint> entry_point, |
| base::Optional<AssistantExitPoint> exit_point) { |
| if (assistant::util::IsStartingSession(new_visibility, old_visibility)) { |
| // When Assistant is starting a new session, we animate in the appearance of |
| // the zero state view and footer. |
| const bool from_search = |
| entry_point == AssistantEntryPoint::kLauncherSearchResult; |
| progress_indicator_->layer()->SetOpacity(0.f); |
| horizontal_separator_->layer()->SetOpacity(from_search ? 1.f : 0.f); |
| |
| if (!from_search) |
| AnimateInZeroState(); |
| else |
| zero_state_view_->SetVisible(false); |
| |
| AnimateInFooter(); |
| return; |
| } |
| |
| query_view_->SetQuery(AssistantNullQuery()); |
| |
| footer_->SetVisible(true); |
| footer_->layer()->SetOpacity(1.f); |
| footer_->set_can_process_events_within_subtree(true); |
| } |
| |
| void AppListAssistantMainStage::MaybeHideZeroState() { |
| if (!IsShown(zero_state_view_)) |
| return; |
| |
| assistant::util::FadeOutAndHide(zero_state_view_, |
| kZeroStateAnimationFadeOutDuration); |
| } |
| |
| } // namespace ash |