| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/assistant/ui/main_stage/suggestion_container_view.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/assistant/model/assistant_response.h" |
| #include "ash/assistant/model/assistant_suggestions_model.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/main_stage/animated_container_view.h" |
| #include "ash/assistant/ui/main_stage/element_animator.h" |
| #include "ash/assistant/util/animation_util.h" |
| #include "ash/assistant/util/assistant_util.h" |
| #include "ash/public/cpp/assistant/assistant_state.h" |
| #include "ash/public/cpp/assistant/controller/assistant_suggestions_controller.h" |
| #include "ash/public/cpp/assistant/controller/assistant_ui_controller.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "components/feature_engagement/public/feature_constants.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/compositor/callback_layer_animation_observer.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animation_element.h" |
| #include "ui/views/layout/box_layout.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using assistant::util::CreateLayerAnimationSequence; |
| using assistant::util::CreateOpacityElement; |
| using assistant::util::StartLayerAnimationSequence; |
| |
| // Animation. |
| constexpr base::TimeDelta kChipFadeInDuration = base::Milliseconds(250); |
| constexpr base::TimeDelta kChipFadeOutDuration = base::Milliseconds(200); |
| |
| // Metrics. |
| constexpr char kAssistantSuggestionChipHistogram[] = |
| "Ash.Assistant.AnimationSmoothness.SuggestionChip"; |
| |
| constexpr int kPreferredHeightDip = 64; |
| |
| } // namespace |
| |
| // SuggestionChipAnimator ----------------------------------------------------- |
| |
| class SuggestionChipAnimator : public ElementAnimator { |
| public: |
| SuggestionChipAnimator(SuggestionChipView* chip, |
| const SuggestionContainerView* parent) |
| : ElementAnimator(chip), parent_(parent) {} |
| |
| SuggestionChipAnimator(const SuggestionChipAnimator&) = delete; |
| SuggestionChipAnimator& operator=(const SuggestionChipAnimator&) = delete; |
| |
| ~SuggestionChipAnimator() override = default; |
| |
| void AnimateIn(ui::CallbackLayerAnimationObserver* observer) override { |
| StartLayerAnimationSequence( |
| layer()->GetAnimator(), CreateAnimateInAnimation(), observer, |
| base::BindRepeating<void(const std::string&, int)>( |
| base::UmaHistogramPercentage, kAssistantSuggestionChipHistogram)); |
| } |
| |
| void AnimateOut(ui::CallbackLayerAnimationObserver* observer) override { |
| StartLayerAnimationSequence( |
| layer()->GetAnimator(), CreateAnimateOutAnimation(), observer, |
| base::BindRepeating<void(const std::string&, int)>( |
| base::UmaHistogramPercentage, kAssistantSuggestionChipHistogram)); |
| } |
| |
| void FadeOut(ui::CallbackLayerAnimationObserver* observer) override { |
| // If the user pressed a chip we do not fade it out. |
| if (!IsSelectedChip()) |
| ElementAnimator::FadeOut(observer); |
| } |
| |
| private: |
| bool IsSelectedChip() const { return view() == parent_->selected_chip(); } |
| |
| ui::LayerAnimationSequence* CreateAnimateInAnimation() const { |
| return CreateLayerAnimationSequence(CreateOpacityElement( |
| 1.f, kChipFadeInDuration, gfx::Tween::Type::FAST_OUT_SLOW_IN)); |
| } |
| |
| ui::LayerAnimationSequence* CreateAnimateOutAnimation() const { |
| return CreateLayerAnimationSequence(CreateOpacityElement( |
| 0.f, kChipFadeOutDuration, gfx::Tween::Type::FAST_OUT_SLOW_IN)); |
| } |
| |
| const raw_ptr<const SuggestionContainerView, ExperimentalAsh> |
| parent_; // |parent_| owns |this|. |
| }; |
| |
| // SuggestionContainerView ----------------------------------------------------- |
| |
| SuggestionContainerView::SuggestionContainerView( |
| AssistantViewDelegate* delegate) |
| : AnimatedContainerView(delegate) { |
| SetID(AssistantViewID::kSuggestionContainer); |
| InitLayout(); |
| |
| AssistantSuggestionsController::Get()->GetModel()->AddObserver(this); |
| AssistantUiController::Get()->GetModel()->AddObserver(this); |
| } |
| |
| SuggestionContainerView::~SuggestionContainerView() { |
| if (AssistantUiController::Get()) |
| AssistantUiController::Get()->GetModel()->RemoveObserver(this); |
| |
| if (AssistantSuggestionsController::Get()) |
| AssistantSuggestionsController::Get()->GetModel()->RemoveObserver(this); |
| } |
| |
| gfx::Size SuggestionContainerView::CalculatePreferredSize() const { |
| return gfx::Size(INT_MAX, GetHeightForWidth(INT_MAX)); |
| } |
| |
| int SuggestionContainerView::GetHeightForWidth(int width) const { |
| return kPreferredHeightDip; |
| } |
| |
| void SuggestionContainerView::OnContentsPreferredSizeChanged( |
| views::View* content_view) { |
| // Our contents should never be smaller than our container width because when |
| // showing conversation starters we will be center aligned. |
| const int width = |
| std::max(content_view->GetPreferredSize().width(), this->width()); |
| content_view->SetSize(gfx::Size(width, kPreferredHeightDip)); |
| } |
| |
| void SuggestionContainerView::OnAssistantControllerDestroying() { |
| AnimatedContainerView::OnAssistantControllerDestroying(); |
| |
| AssistantUiController::Get()->GetModel()->RemoveObserver(this); |
| AssistantSuggestionsController::Get()->GetModel()->RemoveObserver(this); |
| } |
| |
| void SuggestionContainerView::OnCommittedQueryChanged( |
| const AssistantQuery& query) { |
| AnimatedContainerView::OnCommittedQueryChanged(query); |
| |
| // Cache the fact that a query has been committed in this Assistant session so |
| // that we know to stop handling conversation starter updates. |
| has_committed_query_ = true; |
| } |
| |
| void SuggestionContainerView::InitLayout() { |
| layout_manager_ = |
| content_view()->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kHorizontal, |
| gfx::Insets::VH(0, assistant::ui::kHorizontalPadding), |
| /*between_child_spacing=*/kSpacingDip)); |
| |
| layout_manager_->set_cross_axis_alignment( |
| views::BoxLayout::CrossAxisAlignment::kCenter); |
| |
| // We center align when showing conversation starters. |
| layout_manager_->set_main_axis_alignment( |
| views::BoxLayout::MainAxisAlignment::kCenter); |
| } |
| |
| void SuggestionContainerView::OnConversationStartersChanged( |
| const std::vector<AssistantSuggestion>& conversation_starters) { |
| // We don't show conversation starters when showing onboarding since the |
| // onboarding experience already provides the user w/ suggestions. |
| if (delegate()->ShouldShowOnboarding()) { |
| return; |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| feature_engagement::kIPHLauncherSearchHelpUiFeature)) { |
| return; |
| } |
| |
| // If we've committed a query we should ignore changes to the cache of |
| // conversation starters as we are past the state in which they should be |
| // presented. To present them now could incorrectly associate the conversation |
| // starters with a response. |
| if (has_committed_query_) { |
| return; |
| } |
| |
| RemoveAllViews(); |
| OnSuggestionsAdded(conversation_starters); |
| } |
| |
| std::unique_ptr<ElementAnimator> SuggestionContainerView::HandleSuggestion( |
| const AssistantSuggestion& suggestion) { |
| // When no longer showing conversation starters, we start align our content. |
| layout_manager_->set_main_axis_alignment( |
| has_committed_query_ ? views::BoxLayout::MainAxisAlignment::kStart |
| : views::BoxLayout::MainAxisAlignment::kCenter); |
| |
| return AddSuggestionChip(suggestion); |
| } |
| |
| void SuggestionContainerView::OnAllViewsRemoved() { |
| // Clear the selected button. |
| selected_chip_ = nullptr; |
| } |
| |
| std::unique_ptr<ElementAnimator> SuggestionContainerView::AddSuggestionChip( |
| const AssistantSuggestion& suggestion) { |
| auto suggestion_chip_view = |
| std::make_unique<SuggestionChipView>(delegate(), suggestion); |
| suggestion_chip_view->SetCallback(base::BindRepeating( |
| &SuggestionContainerView::OnButtonPressed, base::Unretained(this), |
| base::Unretained(suggestion_chip_view.get()))); |
| |
| // The chip will be animated on its own layer. |
| suggestion_chip_view->SetPaintToLayer(); |
| suggestion_chip_view->layer()->SetFillsBoundsOpaquely(false); |
| suggestion_chip_view->layer()->SetOpacity(0.f); |
| |
| // Add to the view hierarchy and return the animator for the suggestion chip. |
| return std::make_unique<SuggestionChipAnimator>( |
| contents()->AddChildView(std::move(suggestion_chip_view)), this); |
| } |
| |
| void SuggestionContainerView::OnUiVisibilityChanged( |
| AssistantVisibility new_visibility, |
| AssistantVisibility old_visibility, |
| std::optional<AssistantEntryPoint> entry_point, |
| std::optional<AssistantExitPoint> exit_point) { |
| if (assistant::util::IsStartingSession(new_visibility, old_visibility) && |
| entry_point.value() != AssistantEntryPoint::kLauncherSearchResult) { |
| // Show conversation starters at the start of a new Assistant session except |
| // when the user already started a query in Launcher quick search box (QSB). |
| OnConversationStartersChanged(AssistantSuggestionsController::Get() |
| ->GetModel() |
| ->GetConversationStarters()); |
| return; |
| } |
| |
| if (!assistant::util::IsFinishingSession(new_visibility)) |
| return; |
| |
| // When Assistant is finishing a session, we need to reset state. |
| has_committed_query_ = false; |
| |
| // When we start a new session we will be showing conversation starters so |
| // we need to center align our content. |
| layout_manager_->set_main_axis_alignment( |
| views::BoxLayout::MainAxisAlignment::kCenter); |
| } |
| |
| void SuggestionContainerView::InitializeUIForBubbleView() { |
| OnConversationStartersChanged(AssistantSuggestionsController::Get() |
| ->GetModel() |
| ->GetConversationStarters()); |
| } |
| |
| void SuggestionContainerView::OnButtonPressed(SuggestionChipView* chip_view) { |
| // Remember which chip was selected, so we can give it a special animation. |
| selected_chip_ = chip_view; |
| delegate()->OnSuggestionPressed(selected_chip_->suggestion_id()); |
| } |
| |
| BEGIN_METADATA(SuggestionContainerView, AnimatedContainerView) |
| END_METADATA |
| |
| } // namespace ash |