Commit 59f09f79 authored by Eliot Courtney's avatar Eliot Courtney Committed by Commit Bot

Implement swipe-to-dismiss for PIP windows.

in the direction of initial movement and then locked on the axis
off-screen and didn't dismiss if not.
off-screen.
screen doesn't allow a swipe-to-dismiss to initiate.
the screen correctly disables initiation of another swipe-to-dismiss for
the rest of the drag-to-move.

Bug: 883114
Bug: 841886
Bug: b/115291749
Test: Added unittests
Test: tried swipe-to-dismiss from all four corners, it started swiping
Test: Tried swiping on the edges of the screen
Test: Tried swiping around 50% area - correctly dismissed if >50% area
Test: Popped back after swiping out with less than 50% of the area
Test: Starting drag-to-move while not on the edge or corner of the
Test: Starting to swipe-to-dismiss but then dragging into the middle of
Change-Id: I350a8824a0d21162f7356a01632cd4787bd0392c
Reviewed-on: https://chromium-review.googlesource.com/c/1221646
Commit-Queue: Eliot Courtney <edcourtney@chromium.org>
Reviewed-by: default avatarMitsuru Oshima <oshima@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604524}
parent 05cec5c6
...@@ -5,42 +5,148 @@ ...@@ -5,42 +5,148 @@
#include "ash/wm/pip/pip_window_resizer.h" #include "ash/wm/pip/pip_window_resizer.h"
#include "ash/wm/pip/pip_positioner.h" #include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/widget_finder.h"
#include "ash/wm/window_util.h" #include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h" #include "ash/wm/wm_event.h"
#include "ui/aura/window.h" #include "ui/aura/window.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h" #include "ui/display/screen.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/core/coordinate_conversion.h"
namespace ash { namespace ash {
namespace { namespace {
// Radius in which the touch can move in a non-dismiss direction before we
// no longer consider this gesture as a candidate for swipe-to-dismiss.
const int kPipDismissSlop = 8;
// How much area by proportion needs to be off-screen to consider this
// a dismissal during swipe-to-dismiss.
const float kPipDismissFraction = 0.5f;
// TODO(edcourtney): Consider varying the animation duration based on how far // TODO(edcourtney): Consider varying the animation duration based on how far
// the pip window has to move. // the pip window has to move.
const int kPipSnapToEdgeAnimationDurationMs = 50; const int kPipSnapToEdgeAnimationDurationMs = 50;
bool IsAtTopOrBottomEdge(const gfx::Rect& bounds, const gfx::Rect& area) {
return (bounds.y() < area.y() + kPipDismissSlop && bounds.y() >= area.y()) ||
(bounds.bottom() > area.bottom() - kPipDismissSlop &&
bounds.bottom() <= area.bottom());
}
bool IsPastTopOrBottomEdge(const gfx::Rect& bounds, const gfx::Rect& area) {
return bounds.y() < area.y() || bounds.bottom() > area.bottom();
}
bool IsAtLeftOrRightEdge(const gfx::Rect& bounds, const gfx::Rect& area) {
return (bounds.x() < area.x() + kPipDismissSlop && bounds.x() >= area.x()) ||
(bounds.right() > area.right() - kPipDismissSlop &&
bounds.right() <= area.right());
}
bool IsPastLeftOrRightEdge(const gfx::Rect& bounds, const gfx::Rect& area) {
return bounds.x() < area.x() || bounds.right() > area.right();
}
} // namespace } // namespace
PipWindowResizer::PipWindowResizer(wm::WindowState* window_state) PipWindowResizer::PipWindowResizer(wm::WindowState* window_state)
: WindowResizer(window_state) { : WindowResizer(window_state) {
window_state->OnDragStarted(details().window_component); window_state->OnDragStarted(details().window_component);
bool is_resize = details().bounds_change & kBoundsChange_Resizes;
// Don't allow swipe-to-dismiss for resizes.
if (!is_resize) {
gfx::Rect area = PipPositioner::GetMovementArea(window_state->GetDisplay());
// Check in which directions we can dismiss. Usually this is only in one
// direction, except when the PIP window is in the corner. In that case,
// we initially mark both directions as viable, and later choose one based
// on the direction of drag.
may_dismiss_horizontally_ =
IsAtLeftOrRightEdge(GetTarget()->GetBoundsInScreen(), area);
may_dismiss_vertically_ =
IsAtTopOrBottomEdge(GetTarget()->GetBoundsInScreen(), area);
}
} }
PipWindowResizer::~PipWindowResizer() {} PipWindowResizer::~PipWindowResizer() {}
// TODO(edcourtney): Implement swipe-to-dismiss on fling.
void PipWindowResizer::Drag(const gfx::Point& location_in_parent, void PipWindowResizer::Drag(const gfx::Point& location_in_parent,
int event_flags) { int event_flags) {
last_location_in_screen_ = location_in_parent; last_location_in_screen_ = location_in_parent;
::wm::ConvertPointToScreen(GetTarget()->parent(), &last_location_in_screen_); ::wm::ConvertPointToScreen(GetTarget()->parent(), &last_location_in_screen_);
gfx::Rect bounds = CalculateBoundsForDrag(location_in_parent); gfx::Vector2d movement_direction =
location_in_parent - details().initial_location_in_parent;
// If we are not sure if this is a swipe or not yet, don't modify any bounds.
int movement_distance2 = movement_direction.x() * movement_direction.x() +
movement_direction.y() * movement_direction.y();
if ((may_dismiss_horizontally_ || may_dismiss_vertically_) &&
movement_distance2 <= kPipDismissSlop * kPipDismissSlop) {
return;
}
gfx::Rect new_bounds = CalculateBoundsForDrag(location_in_parent);
display::Display display = window_state()->GetDisplay(); display::Display display = window_state()->GetDisplay();
gfx::Rect area = PipPositioner::GetMovementArea(display);
// If the PIP window is at a corner, lock swipe to dismiss to the axis
// of movement. Require that the direction of movement is mainly in the
// direction of dismissing to start a swipe-to-dismiss gesture.
if (dismiss_fraction_ == 1.f) {
bool swipe_is_horizontal =
std::abs(movement_direction.x()) > std::abs(movement_direction.y());
may_dismiss_horizontally_ =
may_dismiss_horizontally_ && swipe_is_horizontal;
may_dismiss_vertically_ = may_dismiss_vertically_ && !swipe_is_horizontal;
}
::wm::ConvertRectToScreen(GetTarget()->parent(), &bounds); // Lock to the axis if we've started the swipe-to-dismiss, or, if the PIP
bounds = PipPositioner::GetBoundsForDrag(display, bounds); // window is no longer poking outside of the movement area, disable any
::wm::ConvertRectFromScreen(GetTarget()->parent(), &bounds); // further swipe-to-dismiss gesture for this drag. Use the initial bounds
// to decide the locked axis position.
if (may_dismiss_horizontally_) {
if (IsPastLeftOrRightEdge(new_bounds, area))
new_bounds.set_y(details().initial_bounds_in_parent.y());
else if (!IsAtLeftOrRightEdge(new_bounds, area))
may_dismiss_horizontally_ = false;
} else if (may_dismiss_vertically_) {
if (IsPastTopOrBottomEdge(new_bounds, area))
new_bounds.set_x(details().initial_bounds_in_parent.x());
else if (!IsAtTopOrBottomEdge(new_bounds, area))
may_dismiss_vertically_ = false;
}
if (bounds != GetTarget()->bounds()) { // If we aren't dismissing, make sure to collide with objects.
if (!may_dismiss_horizontally_ && !may_dismiss_vertically_) {
// Reset opacity if it's not a dismiss gesture.
GetTarget()->layer()->SetOpacity(1.f);
::wm::ConvertRectToScreen(GetTarget()->parent(), &new_bounds);
new_bounds = PipPositioner::GetBoundsForDrag(display, new_bounds);
::wm::ConvertRectFromScreen(GetTarget()->parent(), &new_bounds);
} else {
gfx::Rect dismiss_bounds = new_bounds;
dismiss_bounds.Intersect(area);
float bounds_area = new_bounds.width() * new_bounds.height();
float dismiss_area = dismiss_bounds.width() * dismiss_bounds.height();
if (bounds_area != 0.f) {
dismiss_fraction_ = dismiss_area / bounds_area;
GetTarget()->layer()->SetOpacity(dismiss_fraction_);
}
}
// If the user has dragged the PIP window more than kPipDismissSlop distance
// and no dismiss gesture has begun, make it impossible to initiate one for
// the rest of the drag.
if (dismiss_fraction_ == 1.f &&
movement_distance2 > kPipDismissSlop * kPipDismissSlop) {
may_dismiss_horizontally_ = false;
may_dismiss_vertically_ = false;
}
if (new_bounds != GetTarget()->bounds()) {
moved_or_resized_ = true; moved_or_resized_ = true;
GetTarget()->SetBounds(bounds); GetTarget()->SetBounds(new_bounds);
} }
} }
...@@ -50,6 +156,13 @@ void PipWindowResizer::CompleteDrag() { ...@@ -50,6 +156,13 @@ void PipWindowResizer::CompleteDrag() {
window_state()->ClearRestoreBounds(); window_state()->ClearRestoreBounds();
window_state()->set_bounds_changed_by_user(moved_or_resized_); window_state()->set_bounds_changed_by_user(moved_or_resized_);
if (dismiss_fraction_ < kPipDismissFraction) {
// Close the widget. This will trigger an animation dismissing the PIP
// window.
auto* widget = GetInternalWidgetForWindow(window_state()->window());
if (widget)
widget->Close();
} else {
// Animate the PIP window to its resting position. // Animate the PIP window to its resting position.
gfx::Rect bounds = PipPositioner::GetRestingPosition( gfx::Rect bounds = PipPositioner::GetRestingPosition(
window_state()->GetDisplay(), GetTarget()->GetBoundsInScreen()); window_state()->GetDisplay(), GetTarget()->GetBoundsInScreen());
...@@ -59,12 +172,21 @@ void PipWindowResizer::CompleteDrag() { ...@@ -59,12 +172,21 @@ void PipWindowResizer::CompleteDrag() {
duration); duration);
window_state()->OnWMEvent(&event); window_state()->OnWMEvent(&event);
// Animate opacity back to normal opacity:
ui::Layer* layer = GetTarget()->layer();
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings.SetTransitionDuration(duration);
layer->SetOpacity(1.f);
// If the pip work area changes (e.g. message center, virtual keyboard), // If the pip work area changes (e.g. message center, virtual keyboard),
// we want to restore to the last explicitly set position. // we want to restore to the last explicitly set position.
// TODO(edcourtney): This may not be the best place for this. Consider // TODO(edcourtney): This may not be the best place for this. Consider
// doing this a different way or saving these bounds at a later point when // doing this a different way or saving these bounds at a later point when
// the work area changes. // the work area changes.
window_state()->SaveCurrentBoundsForRestore(); window_state()->SaveCurrentBoundsForRestore();
}
} }
void PipWindowResizer::RevertDrag() { void PipWindowResizer::RevertDrag() {
......
...@@ -37,7 +37,10 @@ class ASH_EXPORT PipWindowResizer : public WindowResizer { ...@@ -37,7 +37,10 @@ class ASH_EXPORT PipWindowResizer : public WindowResizer {
wm::WindowState* window_state() { return window_state_; } wm::WindowState* window_state() { return window_state_; }
gfx::Point last_location_in_screen_; gfx::Point last_location_in_screen_;
float dismiss_fraction_ = 1.f;
bool moved_or_resized_ = false; bool moved_or_resized_ = false;
bool may_dismiss_horizontally_ = false;
bool may_dismiss_vertically_ = false;
DISALLOW_COPY_AND_ASSIGN(PipWindowResizer); DISALLOW_COPY_AND_ASSIGN(PipWindowResizer);
}; };
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include "ui/keyboard/keyboard_controller.h" #include "ui/keyboard/keyboard_controller.h"
#include "ui/keyboard/keyboard_switches.h" #include "ui/keyboard/keyboard_switches.h"
#include "ui/keyboard/keyboard_util.h" #include "ui/keyboard/keyboard_util.h"
#include "ui/views/widget/widget.h"
namespace ash { namespace ash {
namespace wm { namespace wm {
...@@ -72,28 +73,35 @@ class PipWindowResizerTest : public AshTestBase { ...@@ -72,28 +73,35 @@ class PipWindowResizerTest : public AshTestBase {
keyboard::SetTouchKeyboardEnabled(true); keyboard::SetTouchKeyboardEnabled(true);
Shell::Get()->EnableKeyboard(); Shell::Get()->EnableKeyboard();
window_.reset( widget_ = CreateWidgetForTest(gfx::Rect(200, 200, 100, 100));
CreateTestWindowInShellWithBounds(gfx::Rect(200, 200, 100, 100))); window_ = widget_->GetNativeWindow();
wm::WindowState* window_state = wm::GetWindowState(window_.get()); window_->SetProperty(aura::client::kAlwaysOnTopKey, true);
test_state_ = new FakeWindowState(mojom::WindowStateType::PIP); test_state_ = new FakeWindowState(mojom::WindowStateType::PIP);
window_state->SetStateObject( wm::GetWindowState(window_)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state_)); std::unique_ptr<wm::WindowState::State>(test_state_));
window_->SetProperty(aura::client::kAlwaysOnTopKey, true);
} }
void TearDown() override { void TearDown() override {
window_.reset();
keyboard::SetTouchKeyboardEnabled(false); keyboard::SetTouchKeyboardEnabled(false);
AshTestBase::TearDown(); AshTestBase::TearDown();
} }
protected: protected:
aura::Window* window() { return window_.get(); } aura::Window* window() { return window_; }
FakeWindowState* test_state() { return test_state_; } FakeWindowState* test_state() { return test_state_; }
std::unique_ptr<views::Widget> CreateWidgetForTest(const gfx::Rect& bounds) {
return CreateTestWidget(nullptr, kShellWindowId_AlwaysOnTopContainer,
bounds);
}
PipWindowResizer* CreateResizerForTest(int window_component) { PipWindowResizer* CreateResizerForTest(int window_component) {
wm::WindowState* window_state = wm::GetWindowState(window()); return CreateResizerForTest(window_component, window());
}
PipWindowResizer* CreateResizerForTest(int window_component,
aura::Window* window) {
wm::WindowState* window_state = wm::GetWindowState(window);
window_state->CreateDragDetails(gfx::Point(), window_component, window_state->CreateDragDetails(gfx::Point(), window_component,
::wm::WINDOW_MOVE_SOURCE_MOUSE); ::wm::WINDOW_MOVE_SOURCE_MOUSE);
return new PipWindowResizer(window_state); return new PipWindowResizer(window_state);
...@@ -115,7 +123,8 @@ class PipWindowResizerTest : public AshTestBase { ...@@ -115,7 +123,8 @@ class PipWindowResizerTest : public AshTestBase {
} }
private: private:
std::unique_ptr<aura::Window> window_; std::unique_ptr<views::Widget> widget_;
aura::Window* window_;
FakeWindowState* test_state_; FakeWindowState* test_state_;
DISALLOW_COPY_AND_ASSIGN(PipWindowResizerTest); DISALLOW_COPY_AND_ASSIGN(PipWindowResizerTest);
...@@ -183,5 +192,146 @@ TEST_F(PipWindowResizerTest, PipWindowCanBeResizedInTabletMode) { ...@@ -183,5 +192,146 @@ TEST_F(PipWindowResizerTest, PipWindowCanBeResizedInTabletMode) {
EXPECT_EQ(gfx::Rect(200, 200, 100, 110), test_state()->last_bounds()); EXPECT_EQ(gfx::Rect(200, 200, 100, 110), test_state()->last_bounds());
} }
TEST_F(PipWindowResizerTest, PipWindowCanBeSwipeDismissed) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 8, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Drag to the left.
resizer->Drag(CalculateDragPoint(*resizer, -100, 0), 0);
// Should be dismissed when the drag completes.
resizer->CompleteDrag();
EXPECT_TRUE(widget->IsClosed());
}
TEST_F(PipWindowResizerTest, PipWindowPartiallySwipedDoesNotDismiss) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 8, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Drag to the left, but only a little bit.
resizer->Drag(CalculateDragPoint(*resizer, -30, 0), 0);
// Should not be dismissed when the drag completes.
resizer->CompleteDrag();
EXPECT_FALSE(widget->IsClosed());
EXPECT_EQ(gfx::Rect(8, 8, 100, 100), test_state->last_bounds());
}
TEST_F(PipWindowResizerTest, PipWindowInSwipeToDismissGestureLocksToAxis) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 8, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Drag to the left, but only a little bit, to start a swipe-to-dismiss.
resizer->Drag(CalculateDragPoint(*resizer, -30, 0), 0);
EXPECT_EQ(gfx::Rect(-22, 8, 100, 100), test_state->last_bounds());
// Now try to drag down, it should be locked to the horizontal axis.
resizer->Drag(CalculateDragPoint(*resizer, -30, 30), 0);
EXPECT_EQ(gfx::Rect(-22, 8, 100, 100), test_state->last_bounds());
}
TEST_F(PipWindowResizerTest,
PipWindowMovedAwayFromScreenEdgeNoLongerCanSwipeToDismiss) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 16, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Drag to the right and up a bit.
resizer->Drag(CalculateDragPoint(*resizer, 30, -8), 0);
EXPECT_EQ(gfx::Rect(38, 8, 100, 100), test_state->last_bounds());
// Now try to drag to the left start a swipe-to-dismiss. It should stop
// at the edge of the work area.
resizer->Drag(CalculateDragPoint(*resizer, -30, -8), 0);
EXPECT_EQ(gfx::Rect(8, 8, 100, 100), test_state->last_bounds());
}
TEST_F(PipWindowResizerTest, PipWindowAtCornerLocksToOneAxisOnSwipeToDismiss) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 8, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Try dragging up and to the left. It should lock onto the axis with the
// largest displacement.
resizer->Drag(CalculateDragPoint(*resizer, -30, -40), 0);
EXPECT_EQ(gfx::Rect(8, -32, 100, 100), test_state->last_bounds());
}
TEST_F(
PipWindowResizerTest,
PipWindowMustBeDraggedMostlyInDirectionOfDismissToInitiateSwipeToDismiss) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 8, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Try a lot downward and a bit to the left. Swiping should not be initiated.
resizer->Drag(CalculateDragPoint(*resizer, -30, 50), 0);
EXPECT_EQ(gfx::Rect(8, 58, 100, 100), test_state->last_bounds());
}
TEST_F(PipWindowResizerTest,
PipWindowDoesNotMoveUntilStatusOfSwipeToDismissGestureIsKnown) {
UpdateWorkArea("400x400");
auto widget = CreateWidgetForTest(gfx::Rect(8, 8, 100, 100));
auto* window = widget->GetNativeWindow();
FakeWindowState* test_state =
new FakeWindowState(mojom::WindowStateType::PIP);
wm::GetWindowState(window)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state));
std::unique_ptr<PipWindowResizer> resizer(
CreateResizerForTest(HTCAPTION, window));
ASSERT_TRUE(resizer.get());
// Move a small amount - this should not trigger any bounds change, since
// we don't know whether a swipe will start or not.
resizer->Drag(CalculateDragPoint(*resizer, -4, 0), 0);
EXPECT_TRUE(test_state->last_bounds().IsEmpty());
}
} // namespace wm } // namespace wm
} // namespace ash } // namespace ash
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment