blob: 7d1aabf25a5f689a8f0e1f5acb78d340fd95e7e0 [file] [log] [blame]
// Copyright 2018 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/login/ui/scrollable_users_list_view.h"
#include <limits>
#include <memory>
#include "ash/login/ui/login_display_style.h"
#include "ash/login/ui/login_user_view.h"
#include "ash/login/ui/non_accessible_view.h"
#include "ash/login/ui/views_utils.h"
#include "ash/public/cpp/login_constants.h"
#include "ash/public/interfaces/login_user_info.mojom.h"
#include "ash/shell.h"
#include "ash/wallpaper/wallpaper_controller.h"
#include "base/bind.h"
#include "base/numerics/ranges.h"
#include "base/timer/timer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/color_utils.h"
#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
#include "ui/views/controls/scrollbar/scroll_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
namespace ash {
namespace {
// Vertical padding between user rows in the small display style.
constexpr int kSmallVerticalDistanceBetweenUsersDp = 53;
// Padding around user list.
constexpr int kSmallPaddingLeftRightOfUserListDp = 45;
constexpr int kSmallPaddingTopBottomOfUserListDp = 60;
constexpr int kExtraSmallPaddingAroundUserListLandscapeDp = 72;
constexpr int kExtraSmallPaddingLeftOfUserListPortraitDp = 46;
constexpr int kExtraSmallPaddingRightOfUserListPortraitDp = 12;
constexpr int kExtraSmallPaddingTopBottomOfUserListPortraitDp = 66;
// Vertical padding between user rows in extra small display style.
constexpr int kExtraSmallVerticalDistanceBetweenUsersDp = 32;
// Height of gradient shown at the top/bottom of the user list in the extra
// small display style.
constexpr int kExtraSmallGradientHeightDp = 112;
// Thickness of scroll bar thumb.
constexpr int kScrollThumbThicknessDp = 6;
// Padding on the right of scroll bar thumb.
constexpr int kScrollThumbPaddingDp = 8;
// Radius of the scroll bar thumb.
constexpr int kScrollThumbRadiusDp = 8;
// Alpha of scroll bar thumb (43/255 = 17%).
constexpr int kScrollThumbAlpha = 43;
// How long for the scrollbar to hide after no scroll events have been received?
constexpr base::TimeDelta kScrollThumbHideTimeout =
base::TimeDelta::FromMilliseconds(500);
// How long for the scrollbar to fade away?
constexpr base::TimeDelta kScrollThumbFadeDuration =
base::TimeDelta::FromMilliseconds(240);
constexpr char kScrollableUsersListContentViewName[] =
"ScrollableUsersListContent";
// A view that is at least as tall as its parent.
class EnsureMinHeightView : public NonAccessibleView {
public:
EnsureMinHeightView()
: NonAccessibleView(kScrollableUsersListContentViewName) {}
~EnsureMinHeightView() override = default;
// NonAccessibleView:
void Layout() override {
// Make sure our height is at least as tall as the parent, so the layout
// manager will center us properly.
int min_height = parent()->height();
if (size().height() < min_height) {
gfx::Size new_size = size();
new_size.set_height(min_height);
SetSize(new_size);
}
NonAccessibleView::Layout();
}
private:
DISALLOW_COPY_AND_ASSIGN(EnsureMinHeightView);
};
class ScrollBarThumb : public views::BaseScrollBarThumb {
public:
explicit ScrollBarThumb(views::ScrollBar* scroll_bar)
: BaseScrollBarThumb(scroll_bar) {}
~ScrollBarThumb() override = default;
// views::BaseScrollBarThumb:
gfx::Size CalculatePreferredSize() const override {
return gfx::Size(kScrollThumbThicknessDp, kScrollThumbThicknessDp);
}
void OnPaint(gfx::Canvas* canvas) override {
cc::PaintFlags fill_flags;
fill_flags.setStyle(cc::PaintFlags::kFill_Style);
fill_flags.setColor(SkColorSetA(SK_ColorWHITE, kScrollThumbAlpha));
canvas->DrawRoundRect(GetLocalBounds(), kScrollThumbRadiusDp, fill_flags);
}
private:
DISALLOW_COPY_AND_ASSIGN(ScrollBarThumb);
};
struct LayoutParams {
// Spacing between user entries on users list.
int between_child_spacing;
// Insets around users list used in landscape orientation.
gfx::Insets insets_landscape;
// Insets around users list used in portrait orientation.
gfx::Insets insets_portrait;
};
// static
LayoutParams BuildLayoutForStyle(LoginDisplayStyle style) {
switch (style) {
case LoginDisplayStyle::kExtraSmall: {
LayoutParams params;
params.between_child_spacing = kExtraSmallVerticalDistanceBetweenUsersDp;
params.insets_landscape =
gfx::Insets(kExtraSmallPaddingAroundUserListLandscapeDp);
params.insets_portrait =
gfx::Insets(kExtraSmallPaddingTopBottomOfUserListPortraitDp,
kExtraSmallPaddingLeftOfUserListPortraitDp,
kExtraSmallPaddingTopBottomOfUserListPortraitDp,
kExtraSmallPaddingRightOfUserListPortraitDp);
return params;
}
case LoginDisplayStyle::kSmall: {
LayoutParams params;
params.insets_landscape = gfx::Insets(kSmallPaddingTopBottomOfUserListDp,
kSmallPaddingLeftRightOfUserListDp);
params.insets_portrait = gfx::Insets(kSmallPaddingTopBottomOfUserListDp,
kSmallPaddingLeftRightOfUserListDp);
params.between_child_spacing = kSmallVerticalDistanceBetweenUsersDp;
return params;
}
default: {
NOTREACHED();
return LayoutParams();
}
}
}
// Shows a scrollbar that automatically displays and hides itself when content
// is scrolled.
class UsersListScrollBar : public views::ScrollBar {
public:
explicit UsersListScrollBar(bool horizontal)
: ScrollBar(horizontal),
hide_scrollbar_timer_(
FROM_HERE,
kScrollThumbHideTimeout,
base::BindRepeating(&UsersListScrollBar::HideScrollBar,
base::Unretained(this))) {
SetThumb(new ScrollBarThumb(this));
GetThumb()->SetPaintToLayer();
GetThumb()->layer()->SetFillsBoundsOpaquely(false);
// The thumb is hidden by default.
GetThumb()->layer()->SetOpacity(0);
}
~UsersListScrollBar() override = default;
// views::ScrollBar:
gfx::Rect GetTrackBounds() const override { return GetLocalBounds(); }
bool OverlapsContent() const override { return true; }
int GetThickness() const override {
return kScrollThumbThicknessDp + kScrollThumbPaddingDp;
}
void OnMouseEntered(const ui::MouseEvent& event) override {
mouse_over_scrollbar_ = true;
ShowScrollbar();
}
void OnMouseExited(const ui::MouseEvent& event) override {
mouse_over_scrollbar_ = false;
if (!hide_scrollbar_timer_.IsRunning())
hide_scrollbar_timer_.Reset();
}
void ScrollToPosition(int position) override {
ShowScrollbar();
views::ScrollBar::ScrollToPosition(position);
}
void ObserveScrollEvent(const ui::ScrollEvent& event) override {
// Scroll fling events are generated by moving a single finger over the
// trackpad; do not show the scrollbar for these events.
if (event.type() == ui::ET_SCROLL_FLING_CANCEL)
return;
ShowScrollbar();
}
private:
void ShowScrollbar() {
bool currently_hidden =
base::IsApproximatelyEqual(GetThumb()->layer()->GetTargetOpacity(), 0.f,
std::numeric_limits<float>::epsilon());
if (!mouse_over_scrollbar_)
hide_scrollbar_timer_.Reset();
if (currently_hidden) {
ui::ScopedLayerAnimationSettings animation(
GetThumb()->layer()->GetAnimator());
animation.SetTransitionDuration(kScrollThumbFadeDuration);
GetThumb()->layer()->SetOpacity(1);
}
}
void HideScrollBar() {
// Never hide the scrollbar if the mouse is over it. The auto-hide timer
// will be reset when the mouse leaves the scrollable area.
if (mouse_over_scrollbar_)
return;
hide_scrollbar_timer_.Stop();
ui::ScopedLayerAnimationSettings animation(
GetThumb()->layer()->GetAnimator());
animation.SetTransitionDuration(kScrollThumbFadeDuration);
GetThumb()->layer()->SetOpacity(0);
}
// When the mouse is hovering over the scrollbar, the scrollbar should always
// be displayed.
bool mouse_over_scrollbar_ = false;
// Timer that will start the scrollbar's hiding animation when it reaches 0.
base::RetainingOneShotTimer hide_scrollbar_timer_;
DISALLOW_COPY_AND_ASSIGN(UsersListScrollBar);
};
} // namespace
// static
ScrollableUsersListView::GradientParams
ScrollableUsersListView::GradientParams::BuildForStyle(
LoginDisplayStyle style) {
switch (style) {
case LoginDisplayStyle::kExtraSmall: {
SkColor dark_muted_color =
Shell::Get()->wallpaper_controller()->GetProminentColor(
color_utils::ColorProfile(color_utils::LumaRange::DARK,
color_utils::SaturationRange::MUTED));
SkColor tint_color = color_utils::GetResultingPaintColor(
SkColorSetA(login_constants::kDefaultBaseColor,
login_constants::kTranslucentColorDarkenAlpha),
SkColorSetA(dark_muted_color, SK_AlphaOPAQUE));
tint_color =
SkColorSetA(tint_color, login_constants::kScrollTranslucentAlpha);
GradientParams params;
params.color_from = dark_muted_color;
params.color_to = tint_color;
params.height = kExtraSmallGradientHeightDp;
return params;
}
case LoginDisplayStyle::kSmall: {
GradientParams params;
params.height = 0.f;
return params;
}
default: {
NOTREACHED();
return GradientParams();
}
}
}
ScrollableUsersListView::TestApi::TestApi(ScrollableUsersListView* view)
: view_(view) {}
ScrollableUsersListView::TestApi::~TestApi() = default;
const std::vector<LoginUserView*>&
ScrollableUsersListView::TestApi::user_views() const {
return view_->user_views_;
}
ScrollableUsersListView::ScrollableUsersListView(
const std::vector<mojom::LoginUserInfoPtr>& users,
const ActionWithUser& on_tap_user,
LoginDisplayStyle display_style)
: display_style_(display_style) {
auto layout_params = BuildLayoutForStyle(display_style);
gradient_params_ = GradientParams::BuildForStyle(display_style);
user_view_host_ = new NonAccessibleView();
user_view_host_layout_ =
user_view_host_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(),
layout_params.between_child_spacing));
user_view_host_layout_->set_minimum_cross_axis_size(
LoginUserView::WidthForLayoutStyle(display_style));
user_view_host_layout_->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kCenter);
user_view_host_layout_->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
for (std::size_t i = 1u; i < users.size(); ++i) {
auto* view = new LoginUserView(
display_style, false /*show_dropdown*/, false /*show_domain*/,
base::BindRepeating(on_tap_user, i - 1), base::RepeatingClosure(),
base::RepeatingClosure());
user_views_.push_back(view);
view->UpdateForUser(users[i], false /*animate*/);
user_view_host_->AddChildView(view);
}
// |user_view_host_| is the same size as the user views, which may be shorter
// than or taller than the display height. We need the exact height of all
// user views to render a background if the wallpaper is not blurred.
//
// |user_view_host_| is a child of |ensure_min_height|, which has a layout
// manager which will ensure |user_view_host_| is vertically centered if
// |user_view_host_| is shorter than the display height.
//
// |user_view_host_| cannot be set as |contents()| directly because it needs
// to be vertically centered when non-scrollable.
auto ensure_min_height = std::make_unique<EnsureMinHeightView>();
ensure_min_height
->SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical))
->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
ensure_min_height->AddChildView(user_view_host_);
SetContents(std::move(ensure_min_height));
SetBackgroundColor(SK_ColorTRANSPARENT);
set_draw_overflow_indicator(false);
SetVerticalScrollBar(new UsersListScrollBar(false));
SetHorizontalScrollBar(new UsersListScrollBar(true));
observer_.Add(Shell::Get()->wallpaper_controller());
}
ScrollableUsersListView::~ScrollableUsersListView() = default;
LoginUserView* ScrollableUsersListView::GetUserView(
const AccountId& account_id) {
for (auto* view : user_views_) {
if (view->current_user()->basic_user_info->account_id == account_id)
return view;
}
return nullptr;
}
void ScrollableUsersListView::Layout() {
DCHECK(user_view_host_layout_);
// Update clipping height.
if (parent()) {
int parent_height = parent()->size().height();
ClipHeightTo(parent_height, parent_height);
if (height() != parent_height)
PreferredSizeChanged();
}
// Update the user view layout.
bool should_show_landscape =
login_views_utils::ShouldShowLandscape(GetWidget());
LayoutParams layout_params = BuildLayoutForStyle(display_style_);
user_view_host_layout_->set_inside_border_insets(
should_show_landscape ? layout_params.insets_landscape
: layout_params.insets_portrait);
// Layout everything.
ScrollView::Layout();
}
void ScrollableUsersListView::OnPaintBackground(gfx::Canvas* canvas) {
// Find the bounds of the actual contents.
gfx::RectF render_bounds(user_view_host_->GetLocalBounds());
views::View::ConvertRectToTarget(user_view_host_, this, &render_bounds);
// In extra-small, the render bounds height always match the display height.
if (display_style_ == LoginDisplayStyle::kExtraSmall) {
render_bounds.set_y(0);
render_bounds.set_height(height());
}
// Only draw a gradient if the wallpaper is blurred. Otherwise, draw a rounded
// rectangle.
if (ash::Shell::Get()->wallpaper_controller()->IsWallpaperBlurred()) {
cc::PaintFlags flags;
// Only draw a gradient if the content can be scrolled.
if (vertical_scroll_bar()->visible()) {
// Draws symmetrical linear gradient at the top and bottom of the view.
SkScalar view_height = render_bounds.height();
SkScalar gradient_height = gradient_params_.height;
if (gradient_height == 0)
gradient_height = view_height;
// Start and end point of the drawing in view space.
SkPoint in_view_coordinates[2] = {SkPoint(),
SkPoint::Make(0.f, view_height)};
// Positions of colors to create gradient define in 0 to 1 range.
SkScalar top_gradient_end = gradient_height / view_height;
SkScalar bottom_gradient_start = 1.f - top_gradient_end;
SkScalar color_positions[4] = {0.f, top_gradient_end,
bottom_gradient_start, 1.f};
SkColor colors[4] = {gradient_params_.color_from,
gradient_params_.color_to, gradient_params_.color_to,
gradient_params_.color_from};
flags.setShader(cc::PaintShader::MakeLinearGradient(
in_view_coordinates, colors, color_positions, 4, SkTileMode::kClamp));
} else {
flags.setColor(gradient_params_.color_to);
}
flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->DrawRect(render_bounds, flags);
} else {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(
SkColorSetA(login_constants::kDefaultBaseColor,
login_constants::kNonBlurredWallpaperBackgroundAlpha));
canvas->DrawRoundRect(
render_bounds, login_constants::kNonBlurredWallpaperBackgroundRadiusDp,
flags);
}
}
// When the active user is updated, the wallpaper changes. The gradient color
// should be updated in response to the new primary wallpaper color.
void ScrollableUsersListView::OnWallpaperColorsChanged() {
gradient_params_ = GradientParams::BuildForStyle(display_style_);
SchedulePaint();
}
void ScrollableUsersListView::OnWallpaperBlurChanged() {
gradient_params_ = GradientParams::BuildForStyle(display_style_);
SchedulePaint();
}
} // namespace ash