blob: 2ac4a32fc8be525cec0eed3c9fe9edc1a9aa70f6 [file] [log] [blame]
// Copyright (c) 2012 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 "ui/views/animation/bounds_animator.h"
#include <algorithm>
#include <utility>
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/test/icu_test_util.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/animation/test_animation_delegate.h"
#include "ui/views/view.h"
using gfx::Animation;
using gfx::SlideAnimation;
using gfx::TestAnimationDelegate;
namespace views {
namespace {
class OwnedDelegate : public gfx::AnimationDelegate {
public:
OwnedDelegate() = default;
~OwnedDelegate() override { deleted_ = true; }
static bool GetAndClearDeleted() {
bool value = deleted_;
deleted_ = false;
return value;
}
static bool GetAndClearCanceled() {
bool value = canceled_;
canceled_ = false;
return value;
}
// Overridden from gfx::AnimationDelegate:
void AnimationCanceled(const Animation* animation) override {
canceled_ = true;
}
private:
static bool deleted_;
static bool canceled_;
DISALLOW_COPY_AND_ASSIGN(OwnedDelegate);
};
// static
bool OwnedDelegate::deleted_ = false;
bool OwnedDelegate::canceled_ = false;
class TestView : public View {
public:
TestView() = default;
void OnDidSchedulePaint(const gfx::Rect& r) override {
++repaint_count_;
if (dirty_rect_.IsEmpty())
dirty_rect_ = r;
else
dirty_rect_.Union(r);
}
const gfx::Rect& dirty_rect() const { return dirty_rect_; }
void set_repaint_count(int val) { repaint_count_ = val; }
int repaint_count() const { return repaint_count_; }
private:
gfx::Rect dirty_rect_;
int repaint_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(TestView);
};
class RTLAnimationTestDelegate : public gfx::AnimationDelegate {
public:
RTLAnimationTestDelegate(const gfx::Rect& start,
const gfx::Rect& target,
View* view,
base::RepeatingClosure quit_closure)
: start_(start),
target_(target),
view_(view),
quit_closure_(std::move(quit_closure)) {}
~RTLAnimationTestDelegate() override = default;
private:
// gfx::AnimationDelegate:
void AnimationProgressed(const Animation* animation) override {
gfx::Transform transform = view_->GetTransform();
ASSERT_TRUE(!transform.IsIdentity());
// In this test, assume that |parent| is root view.
View* parent = view_->parent();
const gfx::Rect start_rect_in_screen = parent->GetMirroredRect(start_);
const gfx::Rect target_rect_in_screen = parent->GetMirroredRect(target_);
gfx::RectF current_bounds_in_screen(
parent->GetMirroredRect(view_->bounds()));
transform.TransformRect(&current_bounds_in_screen);
// Verify that |view_|'s current bounds in screen are valid.
EXPECT_GE(current_bounds_in_screen.x(),
std::min(start_rect_in_screen.x(), target_rect_in_screen.x()));
EXPECT_LE(
current_bounds_in_screen.right(),
std::max(start_rect_in_screen.right(), target_rect_in_screen.right()));
quit_closure_.Run();
}
// Animation initial bounds.
gfx::Rect start_;
// Animation target bounds.
gfx::Rect target_;
// view to be animated.
View* view_;
base::RepeatingClosure quit_closure_;
};
} // namespace
class BoundsAnimatorTest : public testing::Test {
public:
BoundsAnimatorTest()
: task_environment_(
base::test::TaskEnvironment::TimeSource::MOCK_TIME,
base::test::SingleThreadTaskEnvironment::MainThreadType::UI),
child_(new TestView()) {
parent_.AddChildView(child_);
RecreateAnimator(/*use_transforms=*/false);
}
TestView* parent() { return &parent_; }
TestView* child() { return child_; }
BoundsAnimator* animator() { return animator_.get(); }
protected:
void RecreateAnimator(bool use_transforms) {
animator_ = std::make_unique<BoundsAnimator>(&parent_, use_transforms);
animator_->SetAnimationDuration(base::TimeDelta::FromMilliseconds(10));
}
// Animates |child_| to |target_bounds|. Returns the repaint time.
// |use_long_duration| indicates whether long or short bounds animation is
// created.
int GetRepaintTimeFromBoundsAnimation(const gfx::Rect& target_bounds,
bool use_long_duration) {
child()->set_repaint_count(0);
const base::TimeDelta animation_duration =
base::TimeDelta::FromMilliseconds(use_long_duration ? 2000 : 10);
animator()->SetAnimationDuration(animation_duration);
animator()->AnimateViewTo(child(), target_bounds);
animator()->SetAnimationDelegate(child(),
std::make_unique<TestAnimationDelegate>());
// The animator should be animating now.
EXPECT_TRUE(animator()->IsAnimating());
EXPECT_TRUE(animator()->IsAnimating(child()));
// Run the message loop; the delegate exits the loop when the animation is
// done.
if (use_long_duration)
task_environment_.FastForwardBy(animation_duration);
base::RunLoop().Run();
// Make sure the bounds match of the view that was animated match and the
// layer is destroyed.
EXPECT_EQ(target_bounds, child()->bounds());
EXPECT_FALSE(child()->layer());
// |child| shouldn't be animating anymore.
EXPECT_FALSE(animator()->IsAnimating(child()));
return child()->repaint_count();
}
base::test::SingleThreadTaskEnvironment task_environment_;
private:
TestView parent_;
TestView* child_; // Owned by |parent_|.
std::unique_ptr<BoundsAnimator> animator_;
DISALLOW_COPY_AND_ASSIGN(BoundsAnimatorTest);
};
// Checks animate view to.
TEST_F(BoundsAnimatorTest, AnimateViewTo) {
gfx::Rect initial_bounds(0, 0, 10, 10);
child()->SetBoundsRect(initial_bounds);
gfx::Rect target_bounds(10, 10, 20, 20);
animator()->AnimateViewTo(child(), target_bounds);
animator()->SetAnimationDelegate(child(),
std::make_unique<TestAnimationDelegate>());
// The animator should be animating now.
EXPECT_TRUE(animator()->IsAnimating());
EXPECT_TRUE(animator()->IsAnimating(child()));
// Run the message loop; the delegate exits the loop when the animation is
// done.
base::RunLoop().Run();
// Make sure the bounds match of the view that was animated match.
EXPECT_EQ(target_bounds, child()->bounds());
// |child| shouldn't be animating anymore.
EXPECT_FALSE(animator()->IsAnimating(child()));
// The parent should have been told to repaint as the animation progressed.
// The resulting rect is the union of the original and target bounds.
EXPECT_EQ(gfx::UnionRects(target_bounds, initial_bounds),
parent()->dirty_rect());
}
// Make sure that removing/deleting a child view while animating stops the
// view's animation and will not result in a crash.
TEST_F(BoundsAnimatorTest, DeleteWhileAnimating) {
animator()->AnimateViewTo(child(), gfx::Rect(0, 0, 10, 10));
animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>());
EXPECT_TRUE(animator()->IsAnimating(child()));
// Make sure that animation is removed upon deletion.
delete child();
EXPECT_FALSE(animator()->GetAnimationForView(child()));
EXPECT_FALSE(animator()->IsAnimating(child()));
}
// Make sure an AnimationDelegate is deleted when canceled.
TEST_F(BoundsAnimatorTest, DeleteDelegateOnCancel) {
animator()->AnimateViewTo(child(), gfx::Rect(0, 0, 10, 10));
animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>());
animator()->Cancel();
// The animator should no longer be animating.
EXPECT_FALSE(animator()->IsAnimating());
EXPECT_FALSE(animator()->IsAnimating(child()));
// The cancel should both cancel the delegate and delete it.
EXPECT_TRUE(OwnedDelegate::GetAndClearCanceled());
EXPECT_TRUE(OwnedDelegate::GetAndClearDeleted());
}
// Make sure that the AnimationDelegate of the running animation is deleted when
// a new animation is scheduled.
TEST_F(BoundsAnimatorTest, DeleteDelegateOnNewAnimate) {
const gfx::Rect target_bounds_first(0, 0, 10, 10);
animator()->AnimateViewTo(child(), target_bounds_first);
animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>());
// Start an animation on the same view with different target bounds.
const gfx::Rect target_bounds_second(0, 5, 10, 10);
animator()->AnimateViewTo(child(), target_bounds_second);
// Starting a new animation should both cancel the delegate and delete it.
EXPECT_TRUE(OwnedDelegate::GetAndClearDeleted());
EXPECT_TRUE(OwnedDelegate::GetAndClearCanceled());
}
// Make sure that the duplicate animation request does not interrupt the running
// animation.
TEST_F(BoundsAnimatorTest, HandleDuplicateAnimation) {
const gfx::Rect target_bounds(0, 0, 10, 10);
animator()->AnimateViewTo(child(), target_bounds);
animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>());
// Request the animation with the same view/target bounds.
animator()->AnimateViewTo(child(), target_bounds);
// Verify that the existing animation is not interrupted.
EXPECT_FALSE(OwnedDelegate::GetAndClearDeleted());
EXPECT_FALSE(OwnedDelegate::GetAndClearCanceled());
}
// Makes sure StopAnimating works.
TEST_F(BoundsAnimatorTest, StopAnimating) {
std::unique_ptr<OwnedDelegate> delegate(std::make_unique<OwnedDelegate>());
animator()->AnimateViewTo(child(), gfx::Rect(0, 0, 10, 10));
animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>());
animator()->StopAnimatingView(child());
// Shouldn't be animating now.
EXPECT_FALSE(animator()->IsAnimating());
EXPECT_FALSE(animator()->IsAnimating(child()));
// Stopping should both cancel the delegate and delete it.
EXPECT_TRUE(OwnedDelegate::GetAndClearDeleted());
EXPECT_TRUE(OwnedDelegate::GetAndClearCanceled());
}
// Verify that transform is used when the animation target bounds have the
// same size with the current bounds' meanwhile having the transform option
// enabled.
TEST_F(BoundsAnimatorTest, UseTransformsAnimateViewTo) {
RecreateAnimator(/*use_transforms=*/true);
const gfx::Rect initial_bounds(0, 0, 10, 10);
child()->SetBoundsRect(initial_bounds);
// Ensure that the target bounds have the same size with the initial bounds'
// to apply transform to bounds animation.
const gfx::Rect target_bounds_without_resize(gfx::Point(10, 10),
initial_bounds.size());
const int repaint_time_from_short_animation =
GetRepaintTimeFromBoundsAnimation(target_bounds_without_resize,
/*use_long_duration=*/false);
const int repaint_time_from_long_animation =
GetRepaintTimeFromBoundsAnimation(initial_bounds,
/*use_long_duration=*/true);
// The number of repaints in long animation should be the same as with the
// short animation.
EXPECT_EQ(repaint_time_from_short_animation,
repaint_time_from_long_animation);
}
// Verify that transform is not used when the animation target bounds have the
// different size from the current bounds' even if transform is preferred.
TEST_F(BoundsAnimatorTest, NoTransformForScalingAnimation) {
RecreateAnimator(/*use_transforms=*/true);
const gfx::Rect initial_bounds(0, 0, 10, 10);
child()->SetBoundsRect(initial_bounds);
// Ensure that the target bounds have the different size with the initial
// bounds' to repaint bounds in each animation tick.
const gfx::Rect target_bounds_with_reize(gfx::Point(10, 10),
gfx::Size(20, 20));
const int repaint_time_from_short_animation =
GetRepaintTimeFromBoundsAnimation(target_bounds_with_reize,
/*use_long_duration=*/false);
const int repaint_time_from_long_animation =
GetRepaintTimeFromBoundsAnimation(initial_bounds,
/*use_long_duration=*/true);
// When creating bounds animation with repaint, the longer bounds animation
// should have more repaint counts.
EXPECT_GT(repaint_time_from_long_animation,
repaint_time_from_short_animation);
}
// Tests that the transforms option does not crash when a view's bounds start
// off empty.
TEST_F(BoundsAnimatorTest, UseTransformsAnimateViewToEmptySrc) {
RecreateAnimator(/*use_transforms=*/true);
gfx::Rect initial_bounds(0, 0, 0, 0);
child()->SetBoundsRect(initial_bounds);
gfx::Rect target_bounds(10, 10, 20, 20);
child()->set_repaint_count(0);
animator()->AnimateViewTo(child(), target_bounds);
animator()->SetAnimationDelegate(child(),
std::make_unique<TestAnimationDelegate>());
// The animator should be animating now.
EXPECT_TRUE(animator()->IsAnimating());
EXPECT_TRUE(animator()->IsAnimating(child()));
// Run the message loop; the delegate exits the loop when the animation is
// done.
base::RunLoop().Run();
EXPECT_EQ(target_bounds, child()->bounds());
}
// Tests that when using the transform option on the bounds animator, cancelling
// the animation part way results in the correct bounds applied.
TEST_F(BoundsAnimatorTest, UseTransformsCancelAnimation) {
RecreateAnimator(/*use_transforms=*/true);
// Ensure that |initial_bounds| has the same size with |target_bounds| to
// create bounds animation via the transform.
const gfx::Rect initial_bounds(0, 0, 10, 10);
const gfx::Rect target_bounds(10, 10, 10, 10);
child()->SetBoundsRect(initial_bounds);
const base::TimeDelta duration = base::TimeDelta::FromMilliseconds(200);
animator()->SetAnimationDuration(duration);
// Use a linear tween so we can estimate the expected bounds.
animator()->set_tween_type(gfx::Tween::LINEAR);
animator()->AnimateViewTo(child(), target_bounds);
animator()->SetAnimationDelegate(child(),
std::make_unique<TestAnimationDelegate>());
EXPECT_TRUE(animator()->IsAnimating());
EXPECT_TRUE(animator()->IsAnimating(child()));
// Stop halfway and cancel. The child should have its bounds updated to
// exactly halfway between |initial_bounds| and |target_bounds|.
const gfx::Rect expected_bounds(5, 5, 10, 10);
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(100));
EXPECT_EQ(initial_bounds, child()->bounds());
animator()->Cancel();
EXPECT_EQ(expected_bounds, child()->bounds());
}
// Verify that the bounds animation which updates the transform of views work
// as expected under RTL (https://crbug.com/1067033).
TEST_F(BoundsAnimatorTest, VerifyBoundsAnimatorUnderRTL) {
// Enable RTL.
base::test::ScopedRestoreICUDefaultLocale scoped_locale("he");
RecreateAnimator(/*use_transforms=*/true);
parent()->SetBounds(0, 0, 40, 40);
const gfx::Rect initial_bounds(0, 0, 10, 10);
child()->SetBoundsRect(initial_bounds);
const gfx::Rect target_bounds(10, 10, 10, 10);
const base::TimeDelta animation_duration =
base::TimeDelta::FromMilliseconds(10);
animator()->SetAnimationDuration(animation_duration);
child()->set_repaint_count(0);
animator()->AnimateViewTo(child(), target_bounds);
base::RunLoop run_loop;
animator()->SetAnimationDelegate(
child(),
std::make_unique<RTLAnimationTestDelegate>(
initial_bounds, target_bounds, child(), run_loop.QuitClosure()));
// The animator should be animating now.
EXPECT_TRUE(animator()->IsAnimating());
EXPECT_TRUE(animator()->IsAnimating(child()));
run_loop.Run();
EXPECT_FALSE(animator()->IsAnimating(child()));
}
} // namespace views