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