blob: 1e8169ac448325d9cba467cbc64e412f98d148d0 [file] [log] [blame]
// Copyright 2017 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/expand_arrow_view.h"
#include <memory>
#include <utility>
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/animation/ink_drop_mask.h"
#include "ui/views/animation/ink_drop_painted_layer_delegates.h"
namespace app_list {
namespace {
// The width of this view.
constexpr int kTileWidth = 60;
// The arrow dimension of expand arrow.
constexpr int kArrowDimension = 12;
constexpr int kInkDropRadius = 18;
constexpr int kCircleRadius = 18;
// The y position of circle center in closed, peeking and fullscreen state.
constexpr int kCircleCenterClosedY = 18;
constexpr int kCircleCenterPeekingY = 42;
constexpr int kCircleCenterFullscreenY = 8;
// The arrow's y position in peeking and fullscreen state.
constexpr int kArrowClosedY = 12;
constexpr int kArrowPeekingY = 36;
constexpr int kArrowFullscreenY = 2;
// The stroke width of the arrow.
constexpr int kExpandArrowStrokeWidth = 2;
// The three points of arrow in peeking and fullscreen state.
constexpr size_t kPointCount = 3;
constexpr gfx::PointF kPeekingPoints[] = {
gfx::PointF(0, kArrowDimension / 4 * 3),
gfx::PointF(kArrowDimension / 2, kArrowDimension / 4),
gfx::PointF(kArrowDimension, kArrowDimension / 4 * 3)};
constexpr gfx::PointF kFullscreenPoints[] = {
gfx::PointF(0, kArrowDimension / 2),
gfx::PointF(kArrowDimension / 2, kArrowDimension / 2),
gfx::PointF(kArrowDimension, kArrowDimension / 2)};
// Arrow vertical transiton animation related constants.
constexpr int kTotalArrowYOffset = 24;
constexpr int kPulseMinRadius = 2;
constexpr int kPulseMaxRadius = 30;
constexpr float kPulseMinOpacity = 0.f;
constexpr float kPulseMaxOpacity = 0.3f;
constexpr int kAnimationInitialWaitTimeInSec = 3;
constexpr int kAnimationIntervalInSec = 10;
constexpr int kCycleDurationInMs = 1000;
constexpr int kCycleIntervalInMs = 500;
constexpr int kPulseOpacityShowBeginTimeInMs = 100;
constexpr int kPulseOpacityShowEndTimeInMs = 200;
constexpr int kPulseOpacityHideBeginTimeInMs = 800;
constexpr int kPulseOpacityHideEndTimeInMs = 1000;
constexpr int kArrowMoveOutBeginTimeInMs = 100;
constexpr int kArrowMoveOutEndTimeInMs = 500;
constexpr int kArrowMoveInBeginTimeInMs = 500;
constexpr int kArrowMoveInEndTimeInMs = 900;
constexpr SkColor kExpandArrowColor = SK_ColorWHITE;
constexpr SkColor kPulseColor = SK_ColorWHITE;
constexpr SkColor kBackgroundColor = SkColorSetARGB(0xF, 0xFF, 0xFF, 0xFF);
constexpr SkColor kInkDropRippleColor = SkColorSetARGB(0x14, 0xFF, 0xFF, 0xFF);
constexpr SkColor kFocusRingColor = gfx::kGoogleBlue300;
constexpr int kFocusRingWidth = 2;
} // namespace
ExpandArrowView::ExpandArrowView(ContentsView* contents_view,
AppListView* app_list_view)
: views::Button(this),
contents_view_(contents_view),
app_list_view_(app_list_view),
weak_ptr_factory_(this) {
SetFocusBehavior(FocusBehavior::ALWAYS);
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
SetInkDropMode(InkDropMode::ON);
SetAccessibleName(l10n_util::GetStringUTF16(IDS_APP_LIST_EXPAND_BUTTON));
animation_ = std::make_unique<gfx::SlideAnimation>(this);
animation_->SetTweenType(gfx::Tween::LINEAR);
animation_->SetSlideDuration(kCycleDurationInMs * 2 + kCycleIntervalInMs);
ResetHintingAnimation();
// When side shelf or tablet mode is enabled, the peeking launcher won't be
// shown, so the hint animation is unnecessary. Also, do not run the animation
// during test since we are not testing the animation and it might cause msan
// crash when spoken feedbacke is enabled (See https://crbug.com/926038).
if (!app_list_view_->is_side_shelf() && !app_list_view_->is_tablet_mode() &&
!AppListView::ShortAnimationsForTesting()) {
ScheduleHintingAnimation(true);
}
}
ExpandArrowView::~ExpandArrowView() = default;
void ExpandArrowView::PaintButtonContents(gfx::Canvas* canvas) {
gfx::PointF circle_center(kTileWidth / 2, kCircleCenterPeekingY);
gfx::PointF arrow_origin((kTileWidth - kArrowDimension) / 2, kArrowPeekingY);
gfx::PointF arrow_points[kPointCount];
for (size_t i = 0; i < kPointCount; ++i)
arrow_points[i] = kPeekingPoints[i];
SkColor circle_color = kBackgroundColor;
const float progress = app_list_view_->GetAppListTransitionProgress();
if (progress <= 1) {
// Currently transition progress is between closed and peeking state.
// Change the y positions of arrow and circle.
circle_center.set_y(gfx::Tween::FloatValueBetween(
progress, kCircleCenterClosedY, kCircleCenterPeekingY));
arrow_origin.set_y(
gfx::Tween::FloatValueBetween(progress, kArrowClosedY, kArrowPeekingY));
} else {
const float peeking_to_full_progress = progress - 1;
// Currently transition progress is between peeking and fullscreen state.
// Change the y positions of arrow and circle. Also change the shape of
// the arrow and the opacity of the circle.
circle_center.set_y(gfx::Tween::FloatValueBetween(
peeking_to_full_progress, kCircleCenterPeekingY,
kCircleCenterFullscreenY));
arrow_origin.set_y(gfx::Tween::FloatValueBetween(
peeking_to_full_progress, kArrowPeekingY, kArrowFullscreenY));
for (size_t i = 0; i < kPointCount; ++i) {
arrow_points[i].set_y(gfx::Tween::FloatValueBetween(
peeking_to_full_progress, kPeekingPoints[i].y(),
kFullscreenPoints[i].y()));
}
circle_color = gfx::Tween::ColorValueBetween(
peeking_to_full_progress, circle_color, SK_ColorTRANSPARENT);
}
if (animation_->is_animating() && progress <= 1) {
// If app list is peeking state or below peeking state, the arrow should
// keep runing transition animation.
arrow_origin.Offset(0, arrow_y_offset_);
}
// Draw a circle.
cc::PaintFlags circle_flags;
circle_flags.setAntiAlias(true);
circle_flags.setColor(circle_color);
circle_flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->DrawCircle(circle_center, kCircleRadius, circle_flags);
if (HasFocus()) {
cc::PaintFlags focus_ring_flags;
focus_ring_flags.setAntiAlias(true);
focus_ring_flags.setColor(kFocusRingColor);
focus_ring_flags.setStyle(cc::PaintFlags::Style::kStroke_Style);
focus_ring_flags.setStrokeWidth(kFocusRingWidth);
// Creates a focus ring with 1px wider radius to create a border.
canvas->DrawCircle(circle_center, kCircleRadius + 1, focus_ring_flags);
}
if (animation_->is_animating() && progress <= 1) {
// Draw a pulse that expands around the circle.
cc::PaintFlags pulse_flags;
pulse_flags.setStyle(cc::PaintFlags::kStroke_Style);
pulse_flags.setColor(
SkColorSetA(kPulseColor, static_cast<U8CPU>(255 * pulse_opacity_)));
pulse_flags.setAntiAlias(true);
canvas->DrawCircle(circle_center, pulse_radius_, pulse_flags);
}
// Add a clip path so that arrow will only be shown within the circular
// highlight area.
SkPath arrow_mask_path;
arrow_mask_path.addCircle(circle_center.x(), circle_center.y(),
kCircleRadius);
canvas->ClipPath(arrow_mask_path, true);
// Draw an arrow. (It becomes a horizontal line in fullscreen state.)
for (auto& point : arrow_points)
point.Offset(arrow_origin.x(), arrow_origin.y());
cc::PaintFlags arrow_flags;
arrow_flags.setAntiAlias(true);
arrow_flags.setColor(kExpandArrowColor);
arrow_flags.setStrokeWidth(kExpandArrowStrokeWidth);
arrow_flags.setStrokeCap(cc::PaintFlags::Cap::kRound_Cap);
arrow_flags.setStrokeJoin(cc::PaintFlags::Join::kRound_Join);
arrow_flags.setStyle(cc::PaintFlags::kStroke_Style);
SkPath arrow_path;
arrow_path.moveTo(arrow_points[0].x(), arrow_points[0].y());
for (size_t i = 1; i < kPointCount; ++i)
arrow_path.lineTo(arrow_points[i].x(), arrow_points[i].y());
canvas->DrawPath(arrow_path, arrow_flags);
}
void ExpandArrowView::ButtonPressed(views::Button* /*sender*/,
const ui::Event& /*event*/) {
button_pressed_ = true;
ResetHintingAnimation();
TransitToFullscreenAllAppsState();
GetInkDrop()->AnimateToState(views::InkDropState::ACTION_TRIGGERED);
}
gfx::Size ExpandArrowView::CalculatePreferredSize() const {
return gfx::Size(kTileWidth,
AppListConfig::instance().expand_arrow_tile_height());
}
bool ExpandArrowView::OnKeyPressed(const ui::KeyEvent& event) {
if (event.key_code() != ui::VKEY_RETURN)
return false;
TransitToFullscreenAllAppsState();
return true;
}
void ExpandArrowView::OnFocus() {
SchedulePaint();
Button::OnFocus();
}
void ExpandArrowView::OnBlur() {
SchedulePaint();
Button::OnBlur();
}
const char* ExpandArrowView::GetClassName() const {
return "ExpandArrowView";
}
std::unique_ptr<views::InkDrop> ExpandArrowView::CreateInkDrop() {
std::unique_ptr<views::InkDropImpl> ink_drop =
Button::CreateDefaultInkDropImpl();
ink_drop->SetShowHighlightOnHover(false);
ink_drop->SetShowHighlightOnFocus(false);
ink_drop->SetAutoHighlightMode(views::InkDropImpl::AutoHighlightMode::NONE);
return std::move(ink_drop);
}
std::unique_ptr<views::InkDropMask> ExpandArrowView::CreateInkDropMask() const {
return std::make_unique<views::CircleInkDropMask>(
size(), gfx::Point(kTileWidth / 2, kCircleCenterPeekingY),
kInkDropRadius);
}
std::unique_ptr<views::InkDropRipple> ExpandArrowView::CreateInkDropRipple()
const {
gfx::Point center(kTileWidth / 2, kCircleCenterPeekingY);
gfx::Rect bounds(center.x() - kInkDropRadius, center.y() - kInkDropRadius,
2 * kInkDropRadius, 2 * kInkDropRadius);
return std::make_unique<views::FloodFillInkDropRipple>(
size(), GetLocalBounds().InsetsFrom(bounds),
GetInkDropCenterBasedOnLastEvent(), kInkDropRippleColor, 1.0f);
}
void ExpandArrowView::AnimationProgressed(const gfx::Animation* animation) {
// There are two cycles in one animation.
const int animation_duration = kCycleDurationInMs * 2 + kCycleIntervalInMs;
const int first_cycle_end_time = kCycleDurationInMs;
const int interval_end_time = kCycleDurationInMs + kCycleIntervalInMs;
const int second_cycle_end_time = kCycleDurationInMs * 2 + kCycleIntervalInMs;
int time_in_ms = animation->GetCurrentValue() * animation_duration;
if (time_in_ms > first_cycle_end_time && time_in_ms <= interval_end_time) {
// There's no animation in the interval between cycles.
return;
} else if (time_in_ms > interval_end_time &&
time_in_ms <= second_cycle_end_time) {
// Convert to time in one single cycle.
time_in_ms -= interval_end_time;
}
// Update pulse opacity.
if (time_in_ms > kPulseOpacityShowBeginTimeInMs &&
time_in_ms <= kPulseOpacityShowEndTimeInMs) {
pulse_opacity_ =
kPulseMinOpacity +
(kPulseMaxOpacity - kPulseMinOpacity) *
(time_in_ms - kPulseOpacityShowBeginTimeInMs) /
(kPulseOpacityShowEndTimeInMs - kPulseOpacityShowBeginTimeInMs);
} else if (time_in_ms > kPulseOpacityHideBeginTimeInMs &&
time_in_ms <= kPulseOpacityHideEndTimeInMs) {
pulse_opacity_ =
kPulseMaxOpacity -
(kPulseMaxOpacity - kPulseMinOpacity) *
(time_in_ms - kPulseOpacityHideBeginTimeInMs) /
(kPulseOpacityHideEndTimeInMs - kPulseOpacityHideBeginTimeInMs);
}
// Update pulse radius.
pulse_radius_ = static_cast<int>(
(kPulseMaxRadius - kPulseMinRadius) *
gfx::Tween::CalculateValue(
gfx::Tween::EASE_IN_OUT,
static_cast<double>(time_in_ms) / kCycleDurationInMs));
// Update y position offset of the arrow.
if (time_in_ms > kArrowMoveOutBeginTimeInMs &&
time_in_ms <= kArrowMoveOutEndTimeInMs) {
const double progress =
static_cast<double>(time_in_ms - kArrowMoveOutBeginTimeInMs) /
(kArrowMoveOutEndTimeInMs - kArrowMoveOutBeginTimeInMs);
arrow_y_offset_ = static_cast<int>(
-kTotalArrowYOffset *
gfx::Tween::CalculateValue(gfx::Tween::EASE_IN, progress));
} else if (time_in_ms > kArrowMoveInBeginTimeInMs &&
time_in_ms <= kArrowMoveInEndTimeInMs) {
const double progress =
static_cast<double>(time_in_ms - kArrowMoveInBeginTimeInMs) /
(kArrowMoveInEndTimeInMs - kArrowMoveInBeginTimeInMs);
arrow_y_offset_ = static_cast<int>(
kTotalArrowYOffset *
(1 - gfx::Tween::CalculateValue(gfx::Tween::EASE_OUT, progress)));
}
// Apply updates.
SchedulePaint();
}
void ExpandArrowView::AnimationEnded(const gfx::Animation* /*animation*/) {
ResetHintingAnimation();
// Only reschedule hinting animation if app list is not fullscreen. Once the
// user has made the app_list fullscreen, a hint to do so is no longer needed
if (!app_list_view_->is_fullscreen())
ScheduleHintingAnimation(false);
}
void ExpandArrowView::TransitToFullscreenAllAppsState() {
UMA_HISTOGRAM_ENUMERATION(kPageOpenedHistogram, ash::AppListState::kStateApps,
ash::AppListState::kStateLast);
UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram, kExpandArrow,
kMaxPeekingToFullscreen);
contents_view_->SetActiveState(ash::AppListState::kStateApps);
app_list_view_->SetState(ash::mojom::AppListViewState::kFullscreenAllApps);
}
void ExpandArrowView::ScheduleHintingAnimation(bool is_first_time) {
int delay_in_sec = kAnimationIntervalInSec;
if (is_first_time)
delay_in_sec = kAnimationInitialWaitTimeInSec;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ExpandArrowView::StartHintingAnimation,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromSeconds(delay_in_sec));
}
void ExpandArrowView::StartHintingAnimation() {
if (!button_pressed_)
animation_->Show();
}
void ExpandArrowView::ResetHintingAnimation() {
pulse_opacity_ = kPulseMinOpacity;
pulse_radius_ = kPulseMinRadius;
animation_->Reset();
Layout();
}
} // namespace app_list