1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/login/ui/scrollable_users_list_view.h"
6 
7 #include <limits>
8 #include <memory>
9 
10 #include "ash/login/ui/login_display_style.h"
11 #include "ash/login/ui/login_user_view.h"
12 #include "ash/login/ui/non_accessible_view.h"
13 #include "ash/login/ui/views_utils.h"
14 #include "ash/public/cpp/login_constants.h"
15 #include "ash/shell.h"
16 #include "ash/style/ash_color_provider.h"
17 #include "ash/style/default_color_constants.h"
18 #include "ash/style/default_colors.h"
19 #include "ash/wallpaper/wallpaper_controller_impl.h"
20 #include "base/bind.h"
21 #include "base/numerics/ranges.h"
22 #include "base/optional.h"
23 #include "base/timer/timer.h"
24 #include "ui/compositor/scoped_layer_animation_settings.h"
25 #include "ui/display/screen.h"
26 #include "ui/gfx/canvas.h"
27 #include "ui/gfx/color_analysis.h"
28 #include "ui/gfx/color_utils.h"
29 #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
30 #include "ui/views/controls/scrollbar/scroll_bar.h"
31 #include "ui/views/layout/box_layout.h"
32 #include "ui/views/layout/fill_layout.h"
33 
34 namespace ash {
35 
36 namespace {
37 
38 // Vertical padding between user rows in the small display style.
39 constexpr int kSmallVerticalDistanceBetweenUsersDp = 53;
40 
41 // Padding around user list.
42 constexpr int kSmallPaddingLeftRightOfUserListDp = 45;
43 constexpr int kSmallPaddingTopBottomOfUserListDp = 60;
44 constexpr int kExtraSmallPaddingAroundUserListLandscapeDp = 72;
45 constexpr int kExtraSmallPaddingLeftOfUserListPortraitDp = 46;
46 constexpr int kExtraSmallPaddingRightOfUserListPortraitDp = 12;
47 constexpr int kExtraSmallPaddingTopBottomOfUserListPortraitDp = 66;
48 
49 // Vertical padding between user rows in extra small display style.
50 constexpr int kExtraSmallVerticalDistanceBetweenUsersDp = 32;
51 
52 // Height of gradient shown at the top/bottom of the user list in the extra
53 // small display style.
54 constexpr int kExtraSmallGradientHeightDp = 112;
55 
56 // Thickness of scroll bar thumb.
57 constexpr int kScrollThumbThicknessDp = 6;
58 // Padding on the right of scroll bar thumb.
59 constexpr int kScrollThumbPaddingDp = 8;
60 // Radius of the scroll bar thumb.
61 constexpr int kScrollThumbRadiusDp = 8;
62 // Alpha of scroll bar thumb (43/255 = 17%).
63 constexpr int kScrollThumbAlpha = 43;
64 // How long for the scrollbar to hide after no scroll events have been received?
65 constexpr base::TimeDelta kScrollThumbHideTimeout =
66     base::TimeDelta::FromMilliseconds(500);
67 // How long for the scrollbar to fade away?
68 constexpr base::TimeDelta kScrollThumbFadeDuration =
69     base::TimeDelta::FromMilliseconds(240);
70 
71 constexpr char kScrollableUsersListContentViewName[] =
72     "ScrollableUsersListContent";
73 
74 // A view that is at least as tall as its parent.
75 class EnsureMinHeightView : public NonAccessibleView {
76  public:
EnsureMinHeightView()77   EnsureMinHeightView()
78       : NonAccessibleView(kScrollableUsersListContentViewName) {}
79   ~EnsureMinHeightView() override = default;
80 
81   // NonAccessibleView:
Layout()82   void Layout() override {
83     // Make sure our height is at least as tall as the parent, so the layout
84     // manager will center us properly.
85     int min_height = parent()->height();
86     if (size().height() < min_height) {
87       gfx::Size new_size = size();
88       new_size.set_height(min_height);
89       SetSize(new_size);
90     }
91     NonAccessibleView::Layout();
92   }
93 
94  private:
95   DISALLOW_COPY_AND_ASSIGN(EnsureMinHeightView);
96 };
97 
98 class ScrollBarThumb : public views::BaseScrollBarThumb {
99  public:
ScrollBarThumb(views::ScrollBar * scroll_bar)100   explicit ScrollBarThumb(views::ScrollBar* scroll_bar)
101       : BaseScrollBarThumb(scroll_bar) {}
102   ~ScrollBarThumb() override = default;
103 
104   // views::BaseScrollBarThumb:
CalculatePreferredSize() const105   gfx::Size CalculatePreferredSize() const override {
106     return gfx::Size(kScrollThumbThicknessDp, kScrollThumbThicknessDp);
107   }
OnPaint(gfx::Canvas * canvas)108   void OnPaint(gfx::Canvas* canvas) override {
109     cc::PaintFlags fill_flags;
110     fill_flags.setStyle(cc::PaintFlags::kFill_Style);
111     fill_flags.setColor(SkColorSetA(SK_ColorWHITE, kScrollThumbAlpha));
112     canvas->DrawRoundRect(GetLocalBounds(), kScrollThumbRadiusDp, fill_flags);
113   }
114 
115  private:
116   DISALLOW_COPY_AND_ASSIGN(ScrollBarThumb);
117 };
118 
119 struct LayoutParams {
120   // Spacing between user entries on users list.
121   int between_child_spacing;
122   // Insets around users list used in landscape orientation.
123   gfx::Insets insets_landscape;
124   // Insets around users list used in portrait orientation.
125   gfx::Insets insets_portrait;
126 };
127 
128 // static
BuildLayoutForStyle(LoginDisplayStyle style)129 LayoutParams BuildLayoutForStyle(LoginDisplayStyle style) {
130   switch (style) {
131     case LoginDisplayStyle::kExtraSmall: {
132       LayoutParams params;
133       params.between_child_spacing = kExtraSmallVerticalDistanceBetweenUsersDp;
134       params.insets_landscape =
135           gfx::Insets(kExtraSmallPaddingAroundUserListLandscapeDp);
136       params.insets_portrait =
137           gfx::Insets(kExtraSmallPaddingTopBottomOfUserListPortraitDp,
138                       kExtraSmallPaddingLeftOfUserListPortraitDp,
139                       kExtraSmallPaddingTopBottomOfUserListPortraitDp,
140                       kExtraSmallPaddingRightOfUserListPortraitDp);
141       return params;
142     }
143     case LoginDisplayStyle::kSmall: {
144       LayoutParams params;
145       params.insets_landscape = gfx::Insets(kSmallPaddingTopBottomOfUserListDp,
146                                             kSmallPaddingLeftRightOfUserListDp);
147       params.insets_portrait = gfx::Insets(kSmallPaddingTopBottomOfUserListDp,
148                                            kSmallPaddingLeftRightOfUserListDp);
149       params.between_child_spacing = kSmallVerticalDistanceBetweenUsersDp;
150       return params;
151     }
152     default: {
153       NOTREACHED();
154       return LayoutParams();
155     }
156   }
157 }
158 
159 // Shows a scrollbar that automatically displays and hides itself when content
160 // is scrolled.
161 class UsersListScrollBar : public views::ScrollBar {
162  public:
UsersListScrollBar(bool horizontal)163   explicit UsersListScrollBar(bool horizontal)
164       : ScrollBar(horizontal),
165         hide_scrollbar_timer_(
166             FROM_HERE,
167             kScrollThumbHideTimeout,
168             base::BindRepeating(&UsersListScrollBar::HideScrollBar,
169                                 base::Unretained(this))) {
170     SetThumb(new ScrollBarThumb(this));
171     GetThumb()->SetPaintToLayer();
172     GetThumb()->layer()->SetFillsBoundsOpaquely(false);
173     // The thumb is hidden by default.
174     GetThumb()->layer()->SetOpacity(0);
175   }
176   ~UsersListScrollBar() override = default;
177 
178   // views::ScrollBar:
GetTrackBounds() const179   gfx::Rect GetTrackBounds() const override { return GetLocalBounds(); }
OverlapsContent() const180   bool OverlapsContent() const override { return true; }
GetThickness() const181   int GetThickness() const override {
182     return kScrollThumbThicknessDp + kScrollThumbPaddingDp;
183   }
OnMouseEntered(const ui::MouseEvent & event)184   void OnMouseEntered(const ui::MouseEvent& event) override {
185     mouse_over_scrollbar_ = true;
186     ShowScrollbar();
187   }
OnMouseExited(const ui::MouseEvent & event)188   void OnMouseExited(const ui::MouseEvent& event) override {
189     mouse_over_scrollbar_ = false;
190     if (!hide_scrollbar_timer_.IsRunning())
191       hide_scrollbar_timer_.Reset();
192   }
ScrollToPosition(int position)193   void ScrollToPosition(int position) override {
194     ShowScrollbar();
195     views::ScrollBar::ScrollToPosition(position);
196   }
ObserveScrollEvent(const ui::ScrollEvent & event)197   void ObserveScrollEvent(const ui::ScrollEvent& event) override {
198     // Scroll fling events are generated by moving a single finger over the
199     // trackpad; do not show the scrollbar for these events.
200     if (event.type() == ui::ET_SCROLL_FLING_CANCEL)
201       return;
202     ShowScrollbar();
203   }
204 
205  private:
ShowScrollbar()206   void ShowScrollbar() {
207     bool currently_hidden =
208         base::IsApproximatelyEqual(GetThumb()->layer()->GetTargetOpacity(), 0.f,
209                                    std::numeric_limits<float>::epsilon());
210 
211     if (!mouse_over_scrollbar_)
212       hide_scrollbar_timer_.Reset();
213 
214     if (currently_hidden) {
215       ui::ScopedLayerAnimationSettings animation(
216           GetThumb()->layer()->GetAnimator());
217       animation.SetTransitionDuration(kScrollThumbFadeDuration);
218       GetThumb()->layer()->SetOpacity(1);
219     }
220   }
221 
HideScrollBar()222   void HideScrollBar() {
223     // Never hide the scrollbar if the mouse is over it. The auto-hide timer
224     // will be reset when the mouse leaves the scrollable area.
225     if (mouse_over_scrollbar_)
226       return;
227 
228     hide_scrollbar_timer_.Stop();
229     ui::ScopedLayerAnimationSettings animation(
230         GetThumb()->layer()->GetAnimator());
231     animation.SetTransitionDuration(kScrollThumbFadeDuration);
232     GetThumb()->layer()->SetOpacity(0);
233   }
234 
235   // When the mouse is hovering over the scrollbar, the scrollbar should always
236   // be displayed.
237   bool mouse_over_scrollbar_ = false;
238   // Timer that will start the scrollbar's hiding animation when it reaches 0.
239   base::RetainingOneShotTimer hide_scrollbar_timer_;
240 
241   DISALLOW_COPY_AND_ASSIGN(UsersListScrollBar);
242 };
243 
244 }  // namespace
245 
246 // static
247 ScrollableUsersListView::GradientParams
BuildForStyle(LoginDisplayStyle style)248 ScrollableUsersListView::GradientParams::BuildForStyle(
249     LoginDisplayStyle style) {
250   switch (style) {
251     case LoginDisplayStyle::kExtraSmall: {
252       SkColor dark_muted_color =
253           Shell::Get()->wallpaper_controller()->GetProminentColor(
254               color_utils::ColorProfile(color_utils::LumaRange::DARK,
255                                         color_utils::SaturationRange::MUTED));
256       SkColor tint_color = color_utils::GetResultingPaintColor(
257           AshColorProvider::Get()->GetShieldLayerColor(
258               AshColorProvider::ShieldLayerType::kShield80),
259           SkColorSetA(dark_muted_color, SK_AlphaOPAQUE));
260 
261       GradientParams params;
262       params.color_from = dark_muted_color;
263       params.color_to = tint_color;
264       params.height = kExtraSmallGradientHeightDp;
265       return params;
266     }
267     case LoginDisplayStyle::kSmall: {
268       GradientParams params;
269       params.height = 0.f;
270       return params;
271     }
272     default: {
273       NOTREACHED();
274       return GradientParams();
275     }
276   }
277 }
278 
TestApi(ScrollableUsersListView * view)279 ScrollableUsersListView::TestApi::TestApi(ScrollableUsersListView* view)
280     : view_(view) {}
281 
282 ScrollableUsersListView::TestApi::~TestApi() = default;
283 
284 const std::vector<LoginUserView*>&
user_views() const285 ScrollableUsersListView::TestApi::user_views() const {
286   return view_->user_views_;
287 }
288 
ScrollableUsersListView(const std::vector<LoginUserInfo> & users,const ActionWithUser & on_tap_user,LoginDisplayStyle display_style)289 ScrollableUsersListView::ScrollableUsersListView(
290     const std::vector<LoginUserInfo>& users,
291     const ActionWithUser& on_tap_user,
292     LoginDisplayStyle display_style)
293     : display_style_(display_style) {
294   auto layout_params = BuildLayoutForStyle(display_style);
295   gradient_params_ = GradientParams::BuildForStyle(display_style);
296 
297   user_view_host_ = new NonAccessibleView();
298   user_view_host_layout_ =
299       user_view_host_->SetLayoutManager(std::make_unique<views::BoxLayout>(
300           views::BoxLayout::Orientation::kVertical, gfx::Insets(),
301           layout_params.between_child_spacing));
302   user_view_host_layout_->set_minimum_cross_axis_size(
303       LoginUserView::WidthForLayoutStyle(display_style));
304   user_view_host_layout_->set_main_axis_alignment(
305       views::BoxLayout::MainAxisAlignment::kCenter);
306   user_view_host_layout_->set_cross_axis_alignment(
307       views::BoxLayout::CrossAxisAlignment::kCenter);
308 
309   for (std::size_t i = 1u; i < users.size(); ++i) {
310     auto* view =
311         new LoginUserView(display_style, false /*show_dropdown*/,
312                           base::BindRepeating(on_tap_user, i - 1),
313                           base::RepeatingClosure(), base::RepeatingClosure());
314     user_views_.push_back(view);
315     view->UpdateForUser(users[i], false /*animate*/);
316     user_view_host_->AddChildView(view);
317   }
318 
319   // |user_view_host_| is the same size as the user views, which may be shorter
320   // than or taller than the display height. We need the exact height of all
321   // user views to render a background if the wallpaper is not blurred.
322   //
323   // |user_view_host_| is a child of |ensure_min_height|, which has a layout
324   // manager which will ensure |user_view_host_| is vertically centered if
325   // |user_view_host_| is shorter than the display height.
326   //
327   // |user_view_host_| cannot be set as |contents()| directly because it needs
328   // to be vertically centered when non-scrollable.
329   auto ensure_min_height = std::make_unique<EnsureMinHeightView>();
330   ensure_min_height
331       ->SetLayoutManager(std::make_unique<views::BoxLayout>(
332           views::BoxLayout::Orientation::kVertical))
333       ->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
334   ensure_min_height->AddChildView(user_view_host_);
335   SetContents(std::move(ensure_min_height));
336   SetBackgroundColor(base::nullopt);
337   SetDrawOverflowIndicator(false);
338 
339   SetVerticalScrollBar(std::make_unique<UsersListScrollBar>(false));
340   SetHorizontalScrollBar(std::make_unique<UsersListScrollBar>(true));
341 
342   observer_.Add(Shell::Get()->wallpaper_controller());
343 }
344 
345 ScrollableUsersListView::~ScrollableUsersListView() = default;
346 
GetUserView(const AccountId & account_id)347 LoginUserView* ScrollableUsersListView::GetUserView(
348     const AccountId& account_id) {
349   for (auto* view : user_views_) {
350     if (view->current_user().basic_user_info.account_id == account_id)
351       return view;
352   }
353   return nullptr;
354 }
355 
Layout()356 void ScrollableUsersListView::Layout() {
357   DCHECK(user_view_host_layout_);
358 
359   // Update clipping height.
360   if (parent()) {
361     int parent_height = parent()->size().height();
362     ClipHeightTo(parent_height, parent_height);
363     if (height() != parent_height)
364       PreferredSizeChanged();
365   }
366 
367   // Update the user view layout.
368   bool should_show_landscape =
369       login_views_utils::ShouldShowLandscape(GetWidget());
370   LayoutParams layout_params = BuildLayoutForStyle(display_style_);
371   user_view_host_layout_->set_inside_border_insets(
372       should_show_landscape ? layout_params.insets_landscape
373                             : layout_params.insets_portrait);
374 
375   // Layout everything.
376   ScrollView::Layout();
377 }
378 
OnPaintBackground(gfx::Canvas * canvas)379 void ScrollableUsersListView::OnPaintBackground(gfx::Canvas* canvas) {
380   // Find the bounds of the actual contents.
381   gfx::RectF render_bounds(user_view_host_->GetLocalBounds());
382   views::View::ConvertRectToTarget(user_view_host_, this, &render_bounds);
383 
384   // In extra-small, the render bounds height always match the display height.
385   if (display_style_ == LoginDisplayStyle::kExtraSmall) {
386     render_bounds.set_y(0);
387     render_bounds.set_height(height());
388   }
389 
390   // Only draw a gradient if the wallpaper is blurred. Otherwise, draw a rounded
391   // rectangle.
392   if (Shell::Get()->wallpaper_controller()->IsWallpaperBlurredForLockState()) {
393     cc::PaintFlags flags;
394 
395     // Only draw a gradient if the content can be scrolled.
396     if (vertical_scroll_bar()->GetVisible()) {
397       // Draws symmetrical linear gradient at the top and bottom of the view.
398       SkScalar view_height = render_bounds.height();
399       SkScalar gradient_height = gradient_params_.height;
400       if (gradient_height == 0)
401         gradient_height = view_height;
402 
403       // Start and end point of the drawing in view space.
404       SkPoint in_view_coordinates[2] = {SkPoint(),
405                                         SkPoint::Make(0.f, view_height)};
406       // Positions of colors to create gradient define in 0 to 1 range.
407       SkScalar top_gradient_end = gradient_height / view_height;
408       SkScalar bottom_gradient_start = 1.f - top_gradient_end;
409       SkScalar color_positions[4] = {0.f, top_gradient_end,
410                                      bottom_gradient_start, 1.f};
411       SkColor colors[4] = {gradient_params_.color_from,
412                            gradient_params_.color_to, gradient_params_.color_to,
413                            gradient_params_.color_from};
414 
415       flags.setShader(cc::PaintShader::MakeLinearGradient(
416           in_view_coordinates, colors, color_positions, 4, SkTileMode::kClamp));
417     } else {
418       flags.setColor(gradient_params_.color_to);
419     }
420 
421     flags.setStyle(cc::PaintFlags::kFill_Style);
422     canvas->DrawRect(render_bounds, flags);
423   } else {
424     cc::PaintFlags flags;
425     flags.setAntiAlias(true);
426     flags.setStyle(cc::PaintFlags::kFill_Style);
427     flags.setColor(AshColorProvider::Get()->GetShieldLayerColor(
428         AshColorProvider::ShieldLayerType::kShield80));
429     canvas->DrawRoundRect(
430         render_bounds, login_constants::kNonBlurredWallpaperBackgroundRadiusDp,
431         flags);
432   }
433 }
434 
435 // When the active user is updated, the wallpaper changes. The gradient color
436 // should be updated in response to the new primary wallpaper color.
OnWallpaperColorsChanged()437 void ScrollableUsersListView::OnWallpaperColorsChanged() {
438   gradient_params_ = GradientParams::BuildForStyle(display_style_);
439   SchedulePaint();
440 }
441 
OnWallpaperBlurChanged()442 void ScrollableUsersListView::OnWallpaperBlurChanged() {
443   gradient_params_ = GradientParams::BuildForStyle(display_style_);
444   SchedulePaint();
445 }
446 
447 }  // namespace ash
448