blob: 3cc0c899010587b81ecfd6444b53a7f396ce9bc8 [file] [log] [blame]
// Copyright (c) 2012 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/contents_view.h"
#include <algorithm>
#include <vector>
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/views/app_list_folder_view.h"
#include "ash/app_list/views/app_list_main_view.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/app_list/views/assistant/assistant_page_view.h"
#include "ash/app_list/views/expand_arrow_view.h"
#include "ash/app_list/views/horizontal_page_container.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/app_list/views/search_result_answer_card_view.h"
#include "ash/app_list/views/search_result_list_view.h"
#include "ash/app_list/views/search_result_page_view.h"
#include "ash/app_list/views/search_result_tile_item_list_view.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_switches.h"
#include "base/logging.h"
#include "base/numerics/ranges.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/view_model.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// The range of app list transition progress in which the expand arrow'
// opacity changes from 0 to 1.
constexpr float kExpandArrowOpacityStartProgress = 0.61;
constexpr float kExpandArrowOpacityEndProgress = 1;
constexpr int kSearchBarMinWidth = 440;
bool ShouldShowDenseLayout(int height,
ash::AppListViewState target_view_state) {
return (height < 600 &&
(app_list_features::IsScalableAppListEnabled() ||
target_view_state == ash::AppListViewState::kFullscreenAllApps ||
target_view_state == ash::AppListViewState::kFullscreenSearch));
}
// Notifies assistive technology that all schedules animations have completed on
// this view and that a location change event has occurred. This should be used
// for notifying a11y to update view locations after transformation animations.
// This object will delete itself after running OnImplicitAnimationsCompleted.
class AccessibilityAnimationObserver : public ui::ImplicitAnimationObserver {
public:
explicit AccessibilityAnimationObserver(views::View* view) : view_(view) {}
~AccessibilityAnimationObserver() override = default;
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
view_->NotifyAccessibilityEvent(ax::mojom::Event::kLocationChanged, true);
delete this;
}
private:
views::View* const view_;
DISALLOW_COPY_AND_ASSIGN(AccessibilityAnimationObserver);
};
} // namespace
ContentsView::ContentsView(AppListView* app_list_view)
: app_list_view_(app_list_view) {
pagination_model_.SetTransitionDurations(
AppListConfig::instance().page_transition_duration(),
AppListConfig::instance().overscroll_page_transition_duration());
pagination_model_.AddObserver(this);
}
ContentsView::~ContentsView() {
pagination_model_.RemoveObserver(this);
}
void ContentsView::Init(AppListModel* model) {
DCHECK(model);
model_ = model;
AppListViewDelegate* view_delegate = GetAppListMainView()->view_delegate();
horizontal_page_container_ = new HorizontalPageContainer(this, model);
AddLauncherPage(horizontal_page_container_, AppListState::kStateApps);
// Search results UI.
search_results_page_view_ =
new SearchResultPageView(view_delegate, view_delegate->GetSearchModel());
// Search result containers:
if (app_list_features::IsAnswerCardEnabled()) {
search_result_answer_card_view_ =
new SearchResultAnswerCardView(view_delegate);
search_results_page_view_->AddSearchResultContainerView(
search_result_answer_card_view_);
}
expand_arrow_view_ =
AddChildView(std::make_unique<ExpandArrowView>(this, app_list_view_));
search_result_tile_item_list_view_ = new SearchResultTileItemListView(
search_results_page_view_, GetSearchBoxView()->search_box(),
view_delegate);
search_results_page_view_->AddSearchResultContainerView(
search_result_tile_item_list_view_);
search_result_list_view_ =
new SearchResultListView(GetAppListMainView(), view_delegate);
search_results_page_view_->AddSearchResultContainerView(
search_result_list_view_);
AddLauncherPage(search_results_page_view_, AppListState::kStateSearchResults);
if (app_list_features::IsAssistantLauncherUIEnabled()) {
assistant_page_view_ =
new AssistantPageView(view_delegate->GetAssistantViewDelegate(), this);
assistant_page_view_->SetVisible(false);
AddLauncherPage(assistant_page_view_,
AppListState::kStateEmbeddedAssistant);
}
int initial_page_index = GetPageIndexForState(AppListState::kStateApps);
DCHECK_GE(initial_page_index, 0);
page_before_search_ = initial_page_index;
page_before_assistant_ = initial_page_index;
// Must only call SetTotalPages once all the launcher pages have been added
// (as it will trigger a SelectedPageChanged call).
pagination_model_.SetTotalPages(app_list_pages_.size());
// Page 0 is selected by SetTotalPages and needs to be 'hidden' when selecting
// the initial page.
app_list_pages_[GetActivePageIndex()]->OnWillBeHidden();
pagination_model_.SelectPage(initial_page_index, false);
// Update suggestion chips after valid page is selected to prevent the update
// from being ignored.
GetAppsContainerView()->UpdateSuggestionChips();
ActivePageChanged();
// Hide the search results initially.
ShowSearchResults(false);
}
void ContentsView::ResetForShow() {
target_page_for_last_view_state_update_ = base::nullopt;
GetAppsContainerView()->ResetForShowApps();
// SearchBoxView::ResetForShow() before SetActiveState(). It clears the search
// query internally, which can show the search results page through
// QueryChanged(). Since it wants to reset to kStateApps, first reset the
// search box and then set its active state to kStateApps.
GetSearchBoxView()->ResetForShow();
// Make sure the default visibilities of the pages. This should be done before
// SetActiveState() since it checks the visibility of the pages.
horizontal_page_container_->SetVisible(true);
search_results_page_view_->SetVisible(false);
// SearchBoxView::UpdateOpacity() may change search result page opacity during
// drag - make sure that opacity value is reset to 1.0f.
search_results_page_view_->layer()->SetOpacity(1.0f);
if (assistant_page_view_)
assistant_page_view_->SetVisible(false);
SetActiveState(AppListState::kStateApps, /*animate=*/false);
// In side shelf, the opacity of the contents is not animated so set it to the
// final state. In tablet mode, opacity of the elements is controlled by the
// HomeLauncherGestureHandler which expects these elements to be opaque.
// Otherwise the contents animate from 0 to 1 so set the initial opacity to 0.
if (app_list_view_->is_side_shelf() || app_list_view_->is_tablet_mode()) {
AnimateToViewState(AppListViewState::kFullscreenAllApps, base::TimeDelta());
} else if (last_target_view_state_.has_value() &&
*last_target_view_state_ != AppListViewState::kClosed) {
AnimateToViewState(AppListViewState::kClosed, base::TimeDelta());
}
}
void ContentsView::CancelDrag() {
if (GetAppsContainerView()->apps_grid_view()->has_dragged_view())
GetAppsContainerView()->apps_grid_view()->EndDrag(true);
if (GetAppsContainerView()
->app_list_folder_view()
->items_grid_view()
->has_dragged_view()) {
GetAppsContainerView()->app_list_folder_view()->items_grid_view()->EndDrag(
true);
}
}
void ContentsView::SetDragAndDropHostOfCurrentAppList(
ApplicationDragAndDropHost* drag_and_drop_host) {
GetAppsContainerView()->SetDragAndDropHostOfCurrentAppList(
drag_and_drop_host);
}
void ContentsView::OnAppListViewTargetStateChanged(
AppListViewState target_state) {
target_view_state_ = target_state;
if (target_state == AppListViewState::kClosed) {
CancelDrag();
expand_arrow_view_->MaybeEnableHintingAnimation(false);
return;
}
UpdateExpandArrowBehavior(target_state);
}
void ContentsView::SetActiveState(AppListState state) {
SetActiveState(state, !AppListView::ShortAnimationsForTesting());
}
void ContentsView::SetActiveState(AppListState state, bool animate) {
if (IsStateActive(state))
return;
// The primary way to set the state to search or Assistant results should be
// via |ShowSearchResults| or |ShowEmbeddedAssistantUI|.
DCHECK(state != AppListState::kStateSearchResults &&
state != AppListState::kStateEmbeddedAssistant);
const int page_index = GetPageIndexForState(state);
page_before_search_ = page_index;
page_before_assistant_ = page_index;
SetActiveStateInternal(page_index, animate);
}
int ContentsView::GetActivePageIndex() const {
// The active page is changed at the beginning of an animation, not the end.
return pagination_model_.SelectedTargetPage();
}
AppListState ContentsView::GetActiveState() const {
return GetStateForPageIndex(GetActivePageIndex());
}
bool ContentsView::IsStateActive(AppListState state) const {
int active_page_index = GetActivePageIndex();
return active_page_index >= 0 &&
GetPageIndexForState(state) == active_page_index;
}
int ContentsView::GetPageIndexForState(AppListState state) const {
// Find the index of the view corresponding to the given state.
std::map<AppListState, int>::const_iterator it = state_to_view_.find(state);
if (it == state_to_view_.end())
return -1;
return it->second;
}
AppListState ContentsView::GetStateForPageIndex(int index) const {
std::map<int, AppListState>::const_iterator it = view_to_state_.find(index);
if (it == view_to_state_.end())
return AppListState::kInvalidState;
return it->second;
}
int ContentsView::NumLauncherPages() const {
return pagination_model_.total_pages();
}
AppsContainerView* ContentsView::GetAppsContainerView() {
return horizontal_page_container_->apps_container_view();
}
gfx::Size ContentsView::AdjustSearchBoxSizeToFitMargins(
const gfx::Size& preferred_size) const {
if (!app_list_features::IsScalableAppListEnabled())
return preferred_size;
const int padded_width =
GetContentsBounds().width() -
2 * app_list_view_->GetAppListConfig().GetIdealHorizontalMargin(
GetContentsBounds());
return gfx::Size(base::ClampToRange(padded_width, kSearchBarMinWidth,
preferred_size.width()),
preferred_size.height());
}
void ContentsView::SetActiveStateInternal(int page_index, bool animate) {
if (!GetPageView(page_index)->GetVisible())
return;
app_list_pages_[GetActivePageIndex()]->OnWillBeHidden();
// Start animating to the new page.
const bool should_animate = animate && !set_active_state_without_animation_;
// There's a chance of selecting page during the transition animation. To
// reschedule the new animation from the beginning, |pagination_model_| needs
// to finish the ongoing animation here.
if (should_animate && pagination_model_.has_transition() &&
pagination_model_.transition().target_page != page_index) {
pagination_model_.FinishAnimation();
// If the pending animation was animating from the current target page, the
// target page might have got hidden as the animation was finished. Make
// sure the page is reshown in that case.
GetPageView(page_index)->SetVisible(true);
}
pagination_model_.SelectPage(page_index, should_animate);
ActivePageChanged();
if (!should_animate)
Layout();
}
void ContentsView::ActivePageChanged() {
AppListState state = AppListState::kInvalidState;
std::map<int, AppListState>::const_iterator it =
view_to_state_.find(GetActivePageIndex());
if (it != view_to_state_.end())
state = it->second;
app_list_pages_[GetActivePageIndex()]->OnWillBeShown();
GetAppListMainView()->model()->SetState(state);
UpdateSearchBoxVisibility(state);
}
void ContentsView::ShowSearchResults(bool show) {
int search_page = GetPageIndexForState(AppListState::kStateSearchResults);
DCHECK_GE(search_page, 0);
// Hide or Show results
GetPageView(search_page)->SetVisible(show);
page_before_assistant_ = show ? search_page : page_before_search_;
SetActiveStateInternal(show ? search_page : page_before_search_,
!AppListView::ShortAnimationsForTesting());
}
bool ContentsView::IsShowingSearchResults() const {
return IsStateActive(AppListState::kStateSearchResults);
}
void ContentsView::ShowEmbeddedAssistantUI(bool show) {
expand_arrow_view_->SetVisible(!show);
const int assistant_page =
GetPageIndexForState(AppListState::kStateEmbeddedAssistant);
DCHECK_GE(assistant_page, 0);
// Hide or Show results.
auto* page_view = GetPageView(assistant_page);
page_view->SetVisible(show);
if (show) {
page_view->RequestFocus();
// RequestFocus() might cause ResetForShow() method through
// AppListView::OnHomeLauncherGainingFocusWithoutAnimation() and it can hide
// |page_view|. Thus |page_view|'s visibility should be set again. See
// b/140831868.
page_view->SetVisible(show);
}
const int search_results_page =
GetPageIndexForState(AppListState::kStateSearchResults);
DCHECK_GE(search_results_page, 0);
GetPageView(page_before_assistant_)->SetVisible(!show);
// No animation when transiting from/to |search_results_page| and in test.
const bool animate = !AppListView::ShortAnimationsForTesting() &&
page_before_assistant_ != search_results_page;
const int current_page = pagination_model_.selected_page();
SetActiveStateInternal(show ? assistant_page : page_before_assistant_,
animate);
// Sometimes the page stays in |assistant_page|, but the preferred bounds
// might change meanwhile.
if (show && current_page == assistant_page) {
page_view->UpdatePageBoundsForState(
AppListState::kStateEmbeddedAssistant, GetContentsBounds(),
GetSearchBoxBounds(AppListState::kStateEmbeddedAssistant));
}
// If |page_before_assistant_| is kStateApps, we need to set app_list_view to
// kPeeking and layout the suggestion chips.
if (!show && page_before_assistant_ ==
GetPageIndexForState(AppListState::kStateApps)) {
GetSearchBoxView()->ClearSearch();
GetSearchBoxView()->SetSearchBoxActive(false, ui::ET_UNKNOWN);
GetAppsContainerView()->Layout();
}
}
bool ContentsView::IsShowingEmbeddedAssistantUI() const {
return IsStateActive(AppListState::kStateEmbeddedAssistant);
}
void ContentsView::FocusEmbeddedAssistantPage() {
const int assistant_page =
GetPageIndexForState(AppListState::kStateEmbeddedAssistant);
DCHECK_GE(assistant_page, 0);
auto* page_view = GetPageView(assistant_page);
page_view->RequestFocus();
page_view->SetVisible(true);
}
void ContentsView::InitializeSearchBoxAnimation(AppListState current_state,
AppListState target_state) {
SearchBoxView* search_box = GetSearchBoxView();
if (!search_box->GetWidget())
return;
search_box->UpdateLayout(
1.f, current_state, GetSearchBoxSize(current_state).height(),
target_state, GetSearchBoxSize(target_state).height());
search_box->UpdateBackground(1.f, current_state, target_state);
gfx::Rect target_bounds = GetSearchBoxBounds(target_state);
target_bounds = search_box->GetViewBoundsForSearchBoxContentsBounds(
ConvertRectToWidgetWithoutTransform(target_bounds));
// The search box animation is conducted as transform animation. Initially
// search box changes its bounds to the target bounds but sets the transform
// to be original bounds. Note that this transform shouldn't be animated
// through ui::LayerAnimator since intermediate transformed bounds might not
// match with other animation and that could look janky.
search_box->GetWidget()->SetBounds(target_bounds);
UpdateSearchBoxAnimation(0.0f, current_state, target_state);
}
void ContentsView::UpdateSearchBoxAnimation(double progress,
AppListState current_state,
AppListState target_state) {
SearchBoxView* search_box = GetSearchBoxView();
if (!search_box->GetWidget())
return;
gfx::Rect previous_bounds = GetSearchBoxBounds(current_state);
previous_bounds = search_box->GetViewBoundsForSearchBoxContentsBounds(
ConvertRectToWidgetWithoutTransform(previous_bounds));
gfx::Rect target_bounds = GetSearchBoxBounds(target_state);
target_bounds = search_box->GetViewBoundsForSearchBoxContentsBounds(
ConvertRectToWidgetWithoutTransform(target_bounds));
gfx::Rect current_bounds =
gfx::Tween::RectValueBetween(progress, previous_bounds, target_bounds);
gfx::Transform transform;
if (current_bounds != target_bounds) {
transform.Translate(current_bounds.origin() - target_bounds.origin());
transform.Scale(
static_cast<float>(current_bounds.width()) / target_bounds.width(),
static_cast<float>(current_bounds.height()) / target_bounds.height());
}
search_box->GetWidget()->GetLayer()->SetTransform(transform);
}
void ContentsView::UpdateExpandArrowOpacity(AppListState target_state,
bool animate) {
float expand_arrow_target_opacity = 0.0f;
if (target_state == AppListState::kStateApps) {
expand_arrow_target_opacity = 1.0f;
} else if (target_state == AppListState::kStateSearchResults ||
target_state == AppListState::kStateEmbeddedAssistant) {
expand_arrow_target_opacity = 0.0f;
} else {
// not updated.
return;
}
std::unique_ptr<ui::ScopedLayerAnimationSettings> settings;
if (animate)
settings = CreateTransitionAnimationSettings(expand_arrow_view_->layer());
expand_arrow_view_->layer()->SetOpacity(expand_arrow_target_opacity);
}
void ContentsView::UpdateExpandArrowBehavior(AppListViewState target_state) {
const bool expand_arrow_enabled = target_state == AppListViewState::kPeeking;
// The expand arrow is only focusable and has InkDropMode on in peeking
// state.
expand_arrow_view_->SetFocusBehavior(
expand_arrow_enabled ? FocusBehavior::ALWAYS : FocusBehavior::NEVER);
expand_arrow_view_->SetInkDropMode(
expand_arrow_enabled ? views::InkDropHostView::InkDropMode::ON
: views::InkDropHostView::InkDropMode::OFF);
// Allow ChromeVox to focus the expand arrow only when peeking launcher.
expand_arrow_view_->GetViewAccessibility().OverrideIsIgnored(
!expand_arrow_enabled);
expand_arrow_view_->GetViewAccessibility().NotifyAccessibilityEvent(
ax::mojom::Event::kTreeChanged);
expand_arrow_view_->MaybeEnableHintingAnimation(expand_arrow_enabled);
}
void ContentsView::UpdateSearchBoxVisibility(AppListState current_state) {
auto* search_box_widget = GetSearchBoxView()->GetWidget();
if (search_box_widget) {
// Hide search box widget in order to click on the embedded Assistant UI.
const bool show_search_box =
current_state != AppListState::kStateEmbeddedAssistant;
show_search_box ? search_box_widget->Show() : search_box_widget->Hide();
}
}
PaginationModel* ContentsView::GetAppsPaginationModel() {
return GetAppsContainerView()->apps_grid_view()->pagination_model();
}
void ContentsView::ShowFolderContent(AppListFolderItem* item) {
GetAppsContainerView()->ShowActiveFolder(item);
}
AppListPage* ContentsView::GetPageView(int index) const {
DCHECK_GT(static_cast<int>(app_list_pages_.size()), index);
return app_list_pages_[index];
}
SearchBoxView* ContentsView::GetSearchBoxView() const {
return GetAppListMainView()->search_box_view();
}
AppListMainView* ContentsView::GetAppListMainView() const {
return app_list_view_->app_list_main_view();
}
int ContentsView::AddLauncherPage(AppListPage* view) {
view->set_contents_view(this);
AddChildView(view);
app_list_pages_.push_back(view);
return app_list_pages_.size() - 1;
}
int ContentsView::AddLauncherPage(AppListPage* view, AppListState state) {
int page_index = AddLauncherPage(view);
bool success =
state_to_view_.insert(std::make_pair(state, page_index)).second;
success = success &&
view_to_state_.insert(std::make_pair(page_index, state)).second;
// There shouldn't be duplicates in either map.
DCHECK(success);
return page_index;
}
gfx::Rect ContentsView::GetSearchBoxBounds(AppListState state) const {
if (app_list_view_->is_in_drag()) {
return GetSearchBoxExpectedBoundsForProgress(
state, app_list_view_->GetAppListTransitionProgress(
state == AppListState::kStateSearchResults
? AppListView::kProgressFlagSearchResults
: AppListView::kProgressFlagNone));
}
return GetSearchBoxBoundsForViewState(state, target_view_state());
}
gfx::Size ContentsView::GetSearchBoxSize(AppListState state) const {
AppListPage* page = GetPageView(GetPageIndexForState(state));
gfx::Size size_preferred_by_page = page->GetPreferredSearchBoxSize();
if (!size_preferred_by_page.IsEmpty())
return AdjustSearchBoxSizeToFitMargins(size_preferred_by_page);
gfx::Size preferred_size = GetSearchBoxView()->GetPreferredSize();
// Reduce the search box size in fullscreen view state when the work area
// height is less than 600 dip - the goal is to increase the amount of space
// available to the apps grid.
if (ShouldShowDenseLayout(GetContentsBounds().height(), target_view_state_)) {
preferred_size.set_height(
AppListConfig::instance().search_box_height_for_dense_layout());
} else {
preferred_size.set_height(AppListConfig::instance().search_box_height());
}
return AdjustSearchBoxSizeToFitMargins(preferred_size);
}
gfx::Rect ContentsView::GetSearchBoxBoundsForViewState(
AppListState state,
AppListViewState view_state) const {
gfx::Size size = GetSearchBoxSize(state);
return gfx::Rect(gfx::Point((width() - size.width()) / 2,
GetSearchBoxTopForViewState(state, view_state)),
size);
}
gfx::Rect ContentsView::GetSearchBoxExpectedBoundsForProgress(
AppListState state,
float progress) const {
AppListViewState baseline_state = state == AppListState::kStateSearchResults
? AppListViewState::kHalf
: AppListViewState::kPeeking;
gfx::Rect bounds = GetSearchBoxBoundsForViewState(state, baseline_state);
if (progress <= 1) {
bounds.set_y(gfx::Tween::IntValueBetween(progress, 0, bounds.y()));
} else {
const int fullscreen_y = GetSearchBoxTopForViewState(
state, AppListViewState::kFullscreenAllApps);
bounds.set_y(
gfx::Tween::IntValueBetween(progress - 1, bounds.y(), fullscreen_y));
}
return bounds;
}
bool ContentsView::Back() {
// If the virtual keyboard is visible, dismiss the keyboard and return early
auto* const keyboard_controller = keyboard::KeyboardUIController::Get();
if (keyboard_controller->IsKeyboardVisible()) {
keyboard_controller->HideKeyboardByUser();
return true;
}
AppListState state = view_to_state_[GetActivePageIndex()];
switch (state) {
case AppListState::kStateApps: {
PaginationModel* pagination_model =
GetAppsContainerView()->apps_grid_view()->pagination_model();
if (GetAppsContainerView()->IsInFolderView()) {
GetAppsContainerView()->app_list_folder_view()->CloseFolderPage();
} else if (app_list_view_->is_tablet_mode() &&
pagination_model->total_pages() > 0 &&
pagination_model->selected_page() > 0) {
pagination_model->SelectPage(
0, !app_list_view_->ShortAnimationsForTesting());
} else {
// Close the app list when Back() is called from the apps page.
return false;
}
break;
}
case AppListState::kStateSearchResults:
GetSearchBoxView()->ClearSearchAndDeactivateSearchBox();
ShowSearchResults(false);
for (auto& observer : search_box_observers_)
observer.OnSearchBoxClearAndDeactivated();
break;
case AppListState::kStateEmbeddedAssistant:
ShowEmbeddedAssistantUI(false);
break;
case AppListState::kStateStart_DEPRECATED:
case AppListState::kInvalidState:
NOTREACHED();
break;
}
return true;
}
void ContentsView::Layout() {
const gfx::Rect rect = GetContentsBounds();
if (rect.IsEmpty())
return;
// Layout expand arrow.
gfx::Rect arrow_rect(GetContentsBounds());
const gfx::Size arrow_size(expand_arrow_view_->GetPreferredSize());
arrow_rect.set_height(arrow_size.height());
arrow_rect.ClampToCenteredSize(arrow_size);
expand_arrow_view_->SetBoundsRect(arrow_rect);
expand_arrow_view_->SchedulePaint();
if (pagination_model_.has_transition())
return;
// The bounds calculations will potentially be mid-transition (depending on
// the state of the PaginationModel).
int current_page = std::max(0, pagination_model_.selected_page());
AppListState current_state = GetStateForPageIndex(current_page);
const gfx::Rect search_box_bounds = GetSearchBoxBounds(current_state);
// Update app list pages.
for (AppListPage* page : app_list_pages_) {
page->UpdatePageBoundsForState(current_state, rect, search_box_bounds);
page->UpdateOpacityForState(current_state);
}
UpdateExpandArrowOpacity(current_state, false);
// Update the searchbox bounds.
auto* search_box = GetSearchBoxView();
// Convert search box bounds to the search box widget's coordinate system.
const gfx::Rect search_box_widget_bounds =
search_box->GetViewBoundsForSearchBoxContentsBounds(
ConvertRectToWidgetWithoutTransform(search_box_bounds));
search_box->GetWidget()->SetBounds(search_box_widget_bounds);
search_box->UpdateLayout(1.f, current_state, search_box_bounds.height(),
current_state, search_box_bounds.height());
search_box->UpdateBackground(1.f, current_state, current_state);
// Reset the transform which can be set through animation.
search_box->GetWidget()->GetLayer()->SetTransform(gfx::Transform());
}
const char* ContentsView::GetClassName() const {
return "ContentsView";
}
void ContentsView::TotalPagesChanged(int previous_page_count,
int new_page_count) {}
void ContentsView::SelectedPageChanged(int old_selected, int new_selected) {
if (old_selected >= 0)
app_list_pages_[old_selected]->OnHidden();
if (new_selected >= 0)
app_list_pages_[new_selected]->OnShown();
}
void ContentsView::TransitionStarted() {
const int current_page = pagination_model_.selected_page();
const int target_page = pagination_model_.transition().target_page;
const AppListState current_state = GetStateForPageIndex(current_page);
const AppListState target_state = GetStateForPageIndex(target_page);
for (AppListPage* page : app_list_pages_)
page->OnAnimationStarted(current_state, target_state);
InitializeSearchBoxAnimation(current_state, target_state);
UpdateExpandArrowOpacity(target_state, true);
}
void ContentsView::TransitionChanged() {
const int current_page = pagination_model_.selected_page();
const int target_page = pagination_model_.transition().target_page;
const AppListState current_state = GetStateForPageIndex(current_page);
const AppListState target_state = GetStateForPageIndex(target_page);
const double progress = pagination_model_.transition().progress;
for (AppListPage* page : app_list_pages_) {
if (!page->GetVisible() ||
!ShouldLayoutPage(page, current_state, target_state)) {
continue;
}
page->OnAnimationUpdated(progress, current_state, target_state);
}
// Update search box's transform gradually. See the comment in
// InitiateSearchBoxAnimation for why it's not animated through
// ui::LayerAnimator.
UpdateSearchBoxAnimation(progress, current_state, target_state);
}
views::View* ContentsView::GetSelectedView() const {
return app_list_pages_[GetActivePageIndex()]->GetSelectedView();
}
void ContentsView::UpdateYPositionAndOpacity() {
const int current_page = pagination_model_.has_transition()
? pagination_model_.transition().target_page
: pagination_model_.selected_page();
const AppListState current_state = GetStateForPageIndex(current_page);
float progress = 0.0f;
if (app_list_view_->is_in_drag()) {
progress = app_list_view_->GetAppListTransitionProgress(
current_state == AppListState::kStateSearchResults
? AppListView::kProgressFlagSearchResults
: AppListView::kProgressFlagNone);
} else {
progress = AppListView::GetTransitionProgressForState(target_view_state());
}
const bool restore_opacity = !app_list_view_->is_in_drag() &&
target_view_state() != AppListViewState::kClosed;
if (current_state != AppListState::kStateApps) {
expand_arrow_view_->layer()->SetOpacity(0.0f);
} else if (restore_opacity) {
expand_arrow_view_->layer()->SetOpacity(1.0f);
} else {
// Changes the opacity of expand arrow between 0 and 1 when app list
// transition progress changes between |kExpandArrowOpacityStartProgress|
// and |kExpandArrowOpacityEndProgress|.
expand_arrow_view_->layer()->SetOpacity(
std::min(std::max((progress - kExpandArrowOpacityStartProgress) /
(kExpandArrowOpacityEndProgress -
kExpandArrowOpacityStartProgress),
0.f),
1.0f));
}
expand_arrow_view_->SchedulePaint();
SearchBoxView* search_box = GetSearchBoxView();
const gfx::Rect search_box_bounds = GetSearchBoxBounds(current_state);
const gfx::Rect search_rect =
search_box->GetViewBoundsForSearchBoxContentsBounds(
ConvertRectToWidgetWithoutTransform(search_box_bounds));
search_box->GetWidget()->SetBounds(search_rect);
for (AppListPage* page : app_list_pages_) {
page->UpdatePageBoundsForState(current_state, GetContentsBounds(),
search_box_bounds);
}
// If in drag, reset the transforms that might have been set in
// AnimateToViewState().
if (app_list_view_->is_in_drag()) {
search_box->layer()->SetTransform(gfx::Transform());
expand_arrow_view_->layer()->SetTransform(gfx::Transform());
for (AppListPage* page : app_list_pages_)
page->layer()->SetTransform(gfx::Transform());
}
if (app_list_features::IsScalableAppListEnabled() ||
current_state == AppListState::kStateApps) {
// Layout the apps container at the position where it would be with apps
// page active with the current app list height - use apps state app list
// progress to aciheve that.
const float apps_container_progress =
app_list_view_->is_in_drag()
? app_list_view_->GetAppListTransitionProgress(
AppListView::kProgressFlagNone)
: progress;
GetAppsContainerView()->UpdateYPositionAndOpacity(apps_container_progress,
restore_opacity);
}
target_page_for_last_view_state_update_ = current_state;
}
void ContentsView::AnimateToViewState(AppListViewState target_view_state,
const base::TimeDelta& duration) {
const AppListState target_page =
GetStateForPageIndex(pagination_model_.has_transition()
? pagination_model_.transition().target_page
: pagination_model_.selected_page());
// Animates layer's opacity.
// |layer| - The layer to animate.
// |target_opacity| - The target layer opacity.
// |half_duration| - Whether the animation duration should be half of the
// overall view state transition. Used during transition to closed state
// to speed up the search box and contenst view animation so the don't
// show under the shelf.
auto animate_opacity = [](base::TimeDelta duration, views::View* view,
bool target_visibility) {
ui::Layer* const layer = view->layer();
ui::ScopedLayerAnimationSettings animation(layer->GetAnimator());
animation.SetTransitionDuration(duration / (target_visibility ? 1 : 2));
animation.SetTweenType(gfx::Tween::EASE_IN);
animation.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
layer->SetOpacity(target_visibility ? 1.0f : 0.0f);
};
// Fade in or out the contents view, the search box.
const bool closing = target_view_state == AppListViewState::kClosed;
animate_opacity(duration, GetSearchBoxView(), !closing /*target_visibility*/);
// Fade in or out the expand arrow.
const bool target_arrow_visibility =
target_page == AppListState::kStateApps && !closing;
animate_opacity(duration, expand_arrow_view_, target_arrow_visibility);
// Animates layer's vertical position (using transform animation).
// |layer| - The layer to transform.
// |y_offset| - The initial vertical offset - the layer's vertical offset will
// be animated to 0.
auto animate_transform = [](base::TimeDelta duration, float y_offset,
views::View* view) {
ui::Layer* layer = view->layer();
gfx::Transform transform;
transform.Translate(0, y_offset);
layer->SetTransform(transform);
auto settings = std::make_unique<ui::ScopedLayerAnimationSettings>(
layer->GetAnimator());
settings->SetTweenType(gfx::Tween::EASE_OUT);
settings->SetTransitionDuration(duration);
settings->SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
// Observer will delete itself after animation completes.
settings->AddObserver(new AccessibilityAnimationObserver(view));
layer->SetTransform(gfx::Transform());
};
// Animate the app list contents to the target state. The transform is
// performed by setting the target view bounds (for search box and the app
// list pages), applying a transform that positions the views into their
// current states, and finally setting up transform animation to the identity
// transform (to move the layers into their target bounds).
// Note that pages are positioned relative to the search box view, and the
// vertical offset from the search box remains constant through out the
// animation, so it's sufficient to calculate the target search box view
// offset and apply the transform to the whole contents view.
const gfx::Rect target_search_box_bounds =
GetSearchBoxBoundsForViewState(target_page, target_view_state);
SearchBoxView* search_box = GetSearchBoxView();
const gfx::Rect target_search_box_widget_bounds =
search_box->GetViewBoundsForSearchBoxContentsBounds(
ConvertRectToWidgetWithoutTransform(target_search_box_bounds));
search_box->GetWidget()->SetBounds(target_search_box_widget_bounds);
// Even though the target bounds are calculated for the target page, use the
// last page for which app list view state was updated - in case page
// transition is in progress, the total search box position change can be
// described as composite of:
// 1. Change in contents view padding due to app list view state change.
// 2. Change in contents view padding due to page change.
// Only the first part is expected to be handled by this animation, and this
// uses the last used page as reference.
// The second change will be handled by the page transition animation.
const AppListState selected_page =
target_page_for_last_view_state_update_.value_or(
GetStateForPageIndex(pagination_model_.selected_page()));
const int progress_baseline_flag =
selected_page == AppListState::kStateSearchResults
? AppListView::kProgressFlagSearchResults
: AppListView::kProgressFlagNone;
const float progress = app_list_view_->GetAppListTransitionProgress(
AppListView::kProgressFlagWithTransform | progress_baseline_flag);
const gfx::Rect current_search_box_bounds =
GetSearchBoxExpectedBoundsForProgress(selected_page, progress);
const int y_offset =
current_search_box_bounds.y() -
GetSearchBoxBoundsForViewState(selected_page, target_view_state).y();
// For search box, animate the search_box view layer instead of the widget
// layer to avoid conflict with pagination model transitions (which update the
// search box widget layer transform as the transition progresses).
animate_transform(duration, y_offset, search_box);
// Update app list page bounds to their target values. This assumes that
// potential in-progress pagination transition does not directly animate page
// bounds.
for (AppListPage* page : app_list_pages_) {
page->UpdatePageBoundsForState(target_page, GetContentsBounds(),
target_search_box_bounds);
if (page != horizontal_page_container_) {
animate_opacity(duration, page, closing ? 0.0f : 1.0f);
animate_transform(duration, y_offset, page);
} else {
GetAppsContainerView()->AnimateOpacity(
progress, target_view_state,
base::BindRepeating(animate_opacity, duration));
GetAppsContainerView()->AnimateYPosition(
target_view_state,
base::BindRepeating(animate_transform, duration, y_offset));
}
}
last_target_view_state_ = target_view_state;
target_page_for_last_view_state_update_ = target_page;
// Schedule expand arrow repaint to ensure the view picks up the new target
// state.
expand_arrow_view()->SchedulePaint();
animate_transform(
duration,
expand_arrow_view()->CalculateOffsetFromCurrentAppListProgress(progress),
expand_arrow_view());
}
void ContentsView::SetExpandArrowViewVisibility(bool show) {
if (expand_arrow_view_->GetVisible() == show)
return;
expand_arrow_view_->SetVisible(show);
}
std::unique_ptr<ui::ScopedLayerAnimationSettings>
ContentsView::CreateTransitionAnimationSettings(ui::Layer* layer) const {
DCHECK(pagination_model_.has_transition());
auto settings =
std::make_unique<ui::ScopedLayerAnimationSettings>(layer->GetAnimator());
settings->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
settings->SetTransitionDuration(
pagination_model_.GetTransitionAnimationSlideDuration());
return settings;
}
void ContentsView::NotifySearchBoxBoundsUpdated() {
for (auto& observer : search_box_observers_)
observer.OnSearchBoxBoundsUpdated();
}
void ContentsView::AddSearchBoxUpdateObserver(
SearchBoxUpdateObserver* observer) {
search_box_observers_.AddObserver(observer);
}
void ContentsView::RemoveSearchBoxUpdateObserver(
SearchBoxUpdateObserver* observer) {
search_box_observers_.RemoveObserver(observer);
}
bool ContentsView::ShouldLayoutPage(AppListPage* page,
AppListState current_state,
AppListState target_state) const {
if ((page == horizontal_page_container_ &&
app_list_features::IsScalableAppListEnabled()) ||
page == search_results_page_view_) {
return ((current_state == AppListState::kStateSearchResults &&
target_state == AppListState::kStateApps) ||
(current_state == AppListState::kStateApps &&
target_state == AppListState::kStateSearchResults));
}
if (page == horizontal_page_container_) {
return (current_state == AppListState::kStateSearchResults &&
target_state == AppListState::kStateApps);
}
if (page == assistant_page_view_) {
return current_state == AppListState::kStateEmbeddedAssistant ||
target_state == AppListState::kStateEmbeddedAssistant;
}
return false;
}
gfx::Rect ContentsView::ConvertRectToWidgetWithoutTransform(
const gfx::Rect& rect) {
gfx::Rect widget_rect = rect;
for (const views::View* v = this; v; v = v->parent()) {
widget_rect.Offset(v->GetMirroredPosition().OffsetFromOrigin());
}
return widget_rect;
}
int ContentsView::GetSearchBoxTopForViewState(
AppListState state,
AppListViewState view_state) const {
AppListPage* page = GetPageView(GetPageIndexForState(state));
base::Optional<int> value_for_page = page->GetSearchBoxTop(view_state);
if (value_for_page.has_value())
return value_for_page.value();
switch (view_state) {
case AppListViewState::kClosed:
return AppListConfig::instance().search_box_closed_top_padding();
case AppListViewState::kFullscreenAllApps:
case AppListViewState::kFullscreenSearch:
if (app_list_features::IsScalableAppListEnabled()) {
return horizontal_page_container_->apps_container_view()
->CalculateMarginsForAvailableBounds(
GetContentsBounds(), GetSearchBoxSize(AppListState::kStateApps),
true /*for_full_container_bounds*/)
.top();
}
return AppListConfig::instance().search_box_fullscreen_top_padding();
case AppListViewState::kPeeking:
case AppListViewState::kHalf:
return AppListConfig::instance().search_box_peeking_top_padding();
}
NOTREACHED();
return AppListConfig::instance().search_box_fullscreen_top_padding();
}
} // namespace ash