0

float: Add tuck education nudge and bounce

Add a basic implementation of the tuck education nudge and bounce
animation. Note that checks to limit the amount of appearances still
need to be implemented and the nudge styling needs updating.

Bug: b/266616451
Test: manual + unit
Change-Id: Ic1245aaa2d95c7cf602bd403658fa82b7f7ff003
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4336899
Reviewed-by: Sammie Quon <sammiequon@chromium.org>
Commit-Queue: Elijah Hewer <hewer@chromium.org>
Code-Coverage: Findit <findit-for-me@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/heads/main@{#1123450}
This commit is contained in:
Elijah Hewer
2023-03-29 05:01:12 +00:00
committed by Chromium LUCI CQ
parent 1cd1ee98ee
commit 03160ca06b
7 changed files with 281 additions and 3 deletions

@ -2261,6 +2261,8 @@ component("ash") {
"wm/float/scoped_window_tucker.h",
"wm/float/tablet_mode_float_window_resizer.cc",
"wm/float/tablet_mode_float_window_resizer.h",
"wm/float/tablet_mode_tuck_education.cc",
"wm/float/tablet_mode_tuck_education.h",
"wm/fullscreen_window_finder.cc",
"wm/fullscreen_window_finder.h",
"wm/gestures/back_gesture/back_gesture_affordance.cc",

@ -6462,6 +6462,11 @@ New install
To shut down the device, press and hold the power button on the device again.
</message>
</messages>
</release>
<!-- Tuck Education -->
<message name="IDS_ASH_TUCK_EDUCATIONAL_NUDGE_LABEL" desc="The text shown to users when a window is floated in tablet mode to indicate the tuck gesture." >
Swipe to hide your floating window
</message>
</messages>
</release>
</grit>

@ -0,0 +1 @@
0050a11809ee6464fa630187e23b8ffb4a77113f

@ -17,6 +17,7 @@
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/scoped_window_tucker.h"
#include "ash/wm/float/tablet_mode_tuck_education.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
@ -150,6 +151,11 @@ class FloatController::FloatedWindowInfo : public aura::WindowObserver {
if (desk->is_active())
float_start_time_ = base::TimeTicks::Now();
if (Shell::Get()->tablet_mode_controller()->InTabletMode()) {
tuck_education_ =
std::make_unique<TabletModeTuckEducation>(floated_window);
}
}
FloatedWindowInfo(const FloatedWindowInfo&) = delete;
@ -260,6 +266,9 @@ class FloatController::FloatedWindowInfo : public aura::WindowObserver {
// a normal window state. Null when `floated_window_` is currently not tucked.
std::unique_ptr<ScopedWindowTucker> scoped_window_tucker_;
// An object responsible for managing the tuck education nudge and animations.
std::unique_ptr<TabletModeTuckEducation> tuck_education_;
// Used to get the tucked window bounds (as opposed to normal floated). False
// during `scoped_window_tucker_` construction.
bool is_tucked_for_tablet_ = false;
@ -750,8 +759,9 @@ void FloatController::FloatForTablet(aura::Window* window,
FloatImpl(window);
if (!chromeos::IsSnappedWindowStateType(old_state_type))
if (!chromeos::IsSnappedWindowStateType(old_state_type)) {
return;
}
// Update magnetism so that the float window is roughly in the same location
// as it was when it was snapped.

@ -59,6 +59,7 @@
#include "ui/views/controls/button/label_button.h"
#include "ui/views/test/test_widget_observer.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/wm/core/window_util.h"
namespace ash {
@ -1814,6 +1815,31 @@ TEST_F(TabletWindowFloatTest, FlingVertical) {
EXPECT_EQ(0, user_action_tester_.GetActionCount(kTuckUserAction));
}
// Tests that the tuck education nudge appears when the window is first floated.
TEST_F(TabletWindowFloatTest, BasicTuckNudge) {
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
std::unique_ptr<aura::Window> window = CreateAppWindow();
// Add observer to check that the tuck education nudge was created.
views::NamedWidgetShownWaiter widget_waiter(
views::test::AnyWidgetTestPasskey{}, "TuckEducationNudgeWidget");
// Float window using accelerator.
PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
// If waiter never sees the tuck education nudge, it will hang forever, and
// the test will fail.
widget_waiter.WaitIfNeededAndGet();
// Nudge should dismiss properly after animations end.
EXPECT_TRUE(window->children().empty());
// TODO(hewer): Add a callback to check that the nudge has properly dismissed
// after the bounce animations and timer have ended.
}
using TabletWindowFloatSplitviewTest = TabletWindowFloatTest;
// Tests the expected behaviour when a window is floated when there are snapped

@ -0,0 +1,179 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wm/float/tablet_mode_tuck_education.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/rounded_label.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/property_change_reason.h"
#include "ui/display/display.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// The vertical distance from the top of `window_` to the nudge.
constexpr int kNudgeYOffset = 38;
// The time it takes for the nudge to fade in or out.
constexpr base::TimeDelta kNudgeFadeDuration = base::Milliseconds(100);
// The timer after the second bounce finishes until the nudge starts to fade
// out.
constexpr base::TimeDelta kNudgeFadeOutDelay = base::Milliseconds(150);
// The maximum distance the window translates by during the bounce.
constexpr float kBounceDistance = 50.0f;
// The time after the first bounce ends and the second bounce begins.
constexpr base::TimeDelta kSecondBounceDelay = base::Seconds(1);
// The time for the window to move from its starting position to its furthest
// position.
constexpr base::TimeDelta kBounceStartDuration = base::Milliseconds(400);
// The time for the window to move from its furthest position to back to its
// original position.
constexpr base::TimeDelta kBounceEndDuration = base::Milliseconds(700);
// RoundedLabel construction values.
constexpr int kLabelPadding = 8;
constexpr int kLabelHeight = 28;
constexpr float kRoundedDivisor = 2.f;
std::unique_ptr<views::Widget> CreateWidget(aura::Window* window) {
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.name = "TuckEducationNudgeWidget";
params.accept_events = false;
params.parent = window;
params.child = true;
auto widget = std::make_unique<views::Widget>(std::move(params));
auto nudge_label = std::make_unique<RoundedLabel>(
kLabelPadding, kLabelPadding, kLabelHeight / kRoundedDivisor,
kLabelHeight,
l10n_util::GetStringUTF16(IDS_ASH_TUCK_EDUCATIONAL_NUDGE_LABEL));
// TODO(hewer): Update label color to `ui::kColorSysSurface3` to match other
// nudges.
widget->SetContentsView(std::move(nudge_label));
return widget;
}
} // namespace
TabletModeTuckEducation::TabletModeTuckEducation(aura::Window* floated_window) {
// Observe for end of floating crossfade animation to begin education.
window_ = floated_window;
window_observation_.Observe(window_);
}
TabletModeTuckEducation::~TabletModeTuckEducation() = default;
void TabletModeTuckEducation::OnWindowTransformed(
aura::Window* window,
ui::PropertyChangeReason reason) {
// Floating a window causes a crossfade animation that can interfere with
// other animations or actions performed on the window at the same time. We
// observe for the crossfade to finish before performing the bounce.
bool animating = window->layer()->GetAnimator()->IsAnimatingProperty(
ui::LayerAnimationElement::TRANSFORM);
if (!nudge_widget_ && !animating &&
reason == ui::PropertyChangeReason::FROM_ANIMATION) {
ActivateTuckEducation();
}
}
void TabletModeTuckEducation::ActivateTuckEducation() {
// No need to observe for the transform animation (crossfade) to finish as
// soon as it has happened once.
window_observation_.Reset();
// TODO(b/275255478): Add checks so the nudge only shows 3 times max with at
// least 24h between.
nudge_widget_ = CreateWidget(window_);
nudge_widget_->Show();
auto* nudge_layer = nudge_widget_->GetLayer();
nudge_layer->SetOpacity(0.0f);
gfx::Size nudge_pref_size =
nudge_widget_->GetContentsView()->GetPreferredSize();
gfx::Rect window_bounds = window_->GetBoundsInScreen();
gfx::Rect new_bounds((window_bounds.width() - nudge_pref_size.width()) / 2,
kNudgeYOffset, nudge_pref_size.width(),
nudge_pref_size.height());
nudge_widget_->SetBounds(new_bounds);
const display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window_);
bool bounce_right =
window_bounds.CenterPoint().x() > display.bounds().CenterPoint().x();
// Move towards edge of screen.
const gfx::Transform side_transform = gfx::Transform::MakeTranslation(
bounce_right ? kBounceDistance : -kBounceDistance, 0);
// Move back to starting position.
const gfx::Transform reset_transform = gfx::Transform();
views::AnimationBuilder()
.OnAborted(base::BindOnce(&TabletModeTuckEducation::DismissNudge,
weak_factory_.GetWeakPtr()))
.OnEnded(base::BindOnce(&TabletModeTuckEducation::DismissNudge,
weak_factory_.GetWeakPtr()))
.SetPreemptionStrategy(ui::LayerAnimator::ENQUEUE_NEW_ANIMATION)
// Fade nudge in.
.Once()
.SetDuration(kNudgeFadeDuration)
.SetOpacity(nudge_widget_->GetLayer(), 1.0f, gfx::Tween::LINEAR)
// First bounce.
.Then()
.SetDuration(kBounceStartDuration)
.SetTransform(window_, side_transform, gfx::Tween::ACCEL_20_DECEL_100)
.Then()
.SetDuration(kBounceEndDuration)
.SetTransform(window_, reset_transform, gfx::Tween::ACCEL_20_DECEL_100)
// Delay before second bounce.
.Then()
.SetDuration(kSecondBounceDelay)
// Second bounce.
.Then()
.SetDuration(kBounceStartDuration)
.SetTransform(window_, side_transform, gfx::Tween::ACCEL_20_DECEL_100)
.Then()
.SetDuration(kBounceEndDuration)
.SetTransform(window_, reset_transform, gfx::Tween::ACCEL_20_DECEL_100)
// Delay before fading out.
.Then()
.SetDuration(kNudgeFadeOutDelay)
// Fade nudge out.
.Then()
.SetDuration(kNudgeFadeDuration)
.SetOpacity(nudge_widget_->GetLayer(), 0.0f, gfx::Tween::LINEAR);
}
void TabletModeTuckEducation::DismissNudge() {
window_ = nullptr;
if (nudge_widget_ && !nudge_widget_->IsClosed()) {
nudge_widget_->GetLayer()->GetAnimator()->AbortAllAnimations();
nudge_widget_->CloseNow();
}
// TODO(b/275420014): Destroy `this` once animations finish as no more actions
// need to be performed.
}
} // namespace ash

@ -0,0 +1,55 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_WM_FLOAT_TABLET_MODE_TUCK_EDUCATION_H_
#define ASH_WM_FLOAT_TABLET_MODE_TUCK_EDUCATION_H_
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#include "ui/views/widget/unique_widget_ptr.h"
namespace ash {
// This class is responsible for educating users about the tuck gesture when
// a window is floated in tablet mode. It shows a nudge with text indicating the
// gesture to tuck, and bounces the floated window twice.
class TabletModeTuckEducation : public aura::WindowObserver {
public:
explicit TabletModeTuckEducation(aura::Window* floated_window);
TabletModeTuckEducation(const TabletModeTuckEducation&) = delete;
TabletModeTuckEducation& operator=(const TabletModeTuckEducation&) = delete;
~TabletModeTuckEducation() override;
// aura::WindowObserver:
void OnWindowTransformed(aura::Window* window,
ui::PropertyChangeReason reason) override;
private:
// Activates the tuck education nudge and bounce animations for
// `floated_window`.
void ActivateTuckEducation();
// Dismisses the nudge if it is still active, and cleans up all related
// pointers.
void DismissNudge();
// The widget that contains the `RoundedLabel`.
views::UniqueWidgetPtr nudge_widget_;
// The floated window that `nudge_widget_` is a child of. Guaranteed to be
// alive for the lifetime of `this` since the owner of `this` observes
// `OnWindowDestroying()`.
aura::Window* window_ = nullptr;
base::ScopedObservation<aura::Window, aura::WindowObserver>
window_observation_{this};
// Chrome's compiler toolchain enforces that any `WeakPtrFactory`
// fields are declared last, to avoid destruction ordering issues.
base::WeakPtrFactory<TabletModeTuckEducation> weak_factory_{this};
};
} // namespace ash
#endif // ASH_WM_FLOAT_TABLET_MODE_TUCK_EDUCATION_H_