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 @@
#include "ash/wm/pip/pip_window_resizer.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/widget_finder.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ui/aura/window.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
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
// the pip window has to move.
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
PipWindowResizer::PipWindowResizer(wm::WindowState* window_state)
: WindowResizer(window_state) {
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() {}
// TODO(edcourtney): Implement swipe-to-dismiss on fling.
void PipWindowResizer::Drag(const gfx::Point& location_in_parent,
int event_flags) {
last_location_in_screen_ = location_in_parent;
::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();
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;
}
// Lock to the axis if we've started the swipe-to-dismiss, or, if the PIP
// window is no longer poking outside of the movement area, disable any
// 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;
}
::wm::ConvertRectToScreen(GetTarget()->parent(), &bounds);
bounds = PipPositioner::GetBoundsForDrag(display, bounds);
::wm::ConvertRectFromScreen(GetTarget()->parent(), &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 (bounds != GetTarget()->bounds()) {
// 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;
GetTarget()->SetBounds(bounds);
GetTarget()->SetBounds(new_bounds);
}
}
......@@ -50,21 +156,37 @@ void PipWindowResizer::CompleteDrag() {
window_state()->ClearRestoreBounds();
window_state()->set_bounds_changed_by_user(moved_or_resized_);
// Animate the PIP window to its resting position.
gfx::Rect bounds = PipPositioner::GetRestingPosition(
window_state()->GetDisplay(), GetTarget()->GetBoundsInScreen());
base::TimeDelta duration =
base::TimeDelta::FromMilliseconds(kPipSnapToEdgeAnimationDurationMs);
wm::SetBoundsEvent event(wm::WM_EVENT_SET_BOUNDS, bounds, /*animate=*/true,
duration);
window_state()->OnWMEvent(&event);
// If the pip work area changes (e.g. message center, virtual keyboard),
// we want to restore to the last explicitly set position.
// 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
// the work area changes.
window_state()->SaveCurrentBoundsForRestore();
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.
gfx::Rect bounds = PipPositioner::GetRestingPosition(
window_state()->GetDisplay(), GetTarget()->GetBoundsInScreen());
base::TimeDelta duration =
base::TimeDelta::FromMilliseconds(kPipSnapToEdgeAnimationDurationMs);
wm::SetBoundsEvent event(wm::WM_EVENT_SET_BOUNDS, bounds, /*animate=*/true,
duration);
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),
// we want to restore to the last explicitly set position.
// 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
// the work area changes.
window_state()->SaveCurrentBoundsForRestore();
}
}
void PipWindowResizer::RevertDrag() {
......
......@@ -37,7 +37,10 @@ class ASH_EXPORT PipWindowResizer : public WindowResizer {
wm::WindowState* window_state() { return window_state_; }
gfx::Point last_location_in_screen_;
float dismiss_fraction_ = 1.f;
bool moved_or_resized_ = false;
bool may_dismiss_horizontally_ = false;
bool may_dismiss_vertically_ = false;
DISALLOW_COPY_AND_ASSIGN(PipWindowResizer);
};
......
......@@ -20,6 +20,7 @@
#include "ui/keyboard/keyboard_controller.h"
#include "ui/keyboard/keyboard_switches.h"
#include "ui/keyboard/keyboard_util.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace wm {
......@@ -72,28 +73,35 @@ class PipWindowResizerTest : public AshTestBase {
keyboard::SetTouchKeyboardEnabled(true);
Shell::Get()->EnableKeyboard();
window_.reset(
CreateTestWindowInShellWithBounds(gfx::Rect(200, 200, 100, 100)));
wm::WindowState* window_state = wm::GetWindowState(window_.get());
widget_ = CreateWidgetForTest(gfx::Rect(200, 200, 100, 100));
window_ = widget_->GetNativeWindow();
window_->SetProperty(aura::client::kAlwaysOnTopKey, true);
test_state_ = new FakeWindowState(mojom::WindowStateType::PIP);
window_state->SetStateObject(
wm::GetWindowState(window_)->SetStateObject(
std::unique_ptr<wm::WindowState::State>(test_state_));
window_->SetProperty(aura::client::kAlwaysOnTopKey, true);
}
void TearDown() override {
window_.reset();
keyboard::SetTouchKeyboardEnabled(false);
AshTestBase::TearDown();
}
protected:
aura::Window* window() { return window_.get(); }
aura::Window* window() { return window_; }
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) {
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,
::wm::WINDOW_MOVE_SOURCE_MOUSE);
return new PipWindowResizer(window_state);
......@@ -115,7 +123,8 @@ class PipWindowResizerTest : public AshTestBase {
}
private:
std::unique_ptr<aura::Window> window_;
std::unique_ptr<views::Widget> widget_;
aura::Window* window_;
FakeWindowState* test_state_;
DISALLOW_COPY_AND_ASSIGN(PipWindowResizerTest);
......@@ -183,5 +192,146 @@ TEST_F(PipWindowResizerTest, PipWindowCanBeResizedInTabletMode) {
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 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