Commit bae50c05 authored by wutao's avatar wutao Committed by Commit Bot

ambient: Add animation for slide show

This patch adds an animation to show next image.

Bug: b/137228115
Test: new PhotoModelTest.* and manual

Change-Id: I70e2067cb35b5315c201c0d3769c5c092005fd5a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1762061Reviewed-by: default avatarXiaohui Chen <xiaohuic@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Commit-Queue: Tao Wu <wutao@chromium.org>
Cr-Commit-Position: refs/heads/master@{#694339}
parent 73742848
...@@ -1631,6 +1631,7 @@ test("ash_unittests") { ...@@ -1631,6 +1631,7 @@ test("ash_unittests") {
"accessibility/touch_accessibility_enabler_unittest.cc", "accessibility/touch_accessibility_enabler_unittest.cc",
"accessibility/touch_exploration_controller_unittest.cc", "accessibility/touch_exploration_controller_unittest.cc",
"accessibility/touch_exploration_manager_unittest.cc", "accessibility/touch_exploration_manager_unittest.cc",
"ambient/model/photo_model_unittest.cc",
"ambient/ui/ambient_container_view_unittest.cc", "ambient/ui/ambient_container_view_unittest.cc",
"app_list/app_list_controller_impl_unittest.cc", "app_list/app_list_controller_impl_unittest.cc",
"app_list/app_list_metrics_unittest.cc", "app_list/app_list_metrics_unittest.cc",
......
...@@ -82,17 +82,43 @@ void AmbientController::RefreshImage() { ...@@ -82,17 +82,43 @@ void AmbientController::RefreshImage() {
if (!PhotoController::Get()) if (!PhotoController::Get())
return; return;
PhotoController::Get()->GetNextImage(base::BindOnce( if (model_.ShouldFetchImmediately()) {
&AmbientController::OnPhotoDownloaded, weak_factory_.GetWeakPtr())); // TODO(b/140032139): Defer downloading image if it is animating.
constexpr base::TimeDelta kAnimationDuration =
base::TimeDelta::FromMilliseconds(250);
base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AmbientController::GetNextImage,
weak_factory_.GetWeakPtr()),
kAnimationDuration);
} else {
model_.ShowNextImage();
ScheduleRefreshImage();
}
}
void AmbientController::ScheduleRefreshImage() {
base::TimeDelta refresh_interval;
if (!model_.ShouldFetchImmediately()) {
// TODO(b/139953713): Change to a correct time interval.
refresh_interval = base::TimeDelta::FromSeconds(5);
}
constexpr base::TimeDelta kTimeOut = base::TimeDelta::FromMilliseconds(1000);
refresh_timer_.Start( refresh_timer_.Start(
FROM_HERE, kTimeOut, FROM_HERE, refresh_interval,
base::BindOnce(&AmbientController::RefreshImage, base::Unretained(this))); base::BindOnce(&AmbientController::RefreshImage, base::Unretained(this)));
} }
void AmbientController::GetNextImage() {
PhotoController::Get()->GetNextImage(base::BindOnce(
&AmbientController::OnPhotoDownloaded, weak_factory_.GetWeakPtr()));
}
void AmbientController::OnPhotoDownloaded(const gfx::ImageSkia& image) { void AmbientController::OnPhotoDownloaded(const gfx::ImageSkia& image) {
model_.AddNextImage(image); if (!image.isNull())
model_.AddNextImage(image);
ScheduleRefreshImage();
} }
AmbientContainerView* AmbientController::GetAmbientContainerViewForTesting() { AmbientContainerView* AmbientController::GetAmbientContainerViewForTesting() {
......
...@@ -31,8 +31,13 @@ class ASH_EXPORT AmbientController : views::WidgetObserver { ...@@ -31,8 +31,13 @@ class ASH_EXPORT AmbientController : views::WidgetObserver {
void OnWidgetDestroying(views::Widget* widget) override; void OnWidgetDestroying(views::Widget* widget) override;
void Toggle(); void Toggle();
void AddPhotoModelObserver(PhotoModelObserver* observer); void AddPhotoModelObserver(PhotoModelObserver* observer);
void RemovePhotoModelObserver(PhotoModelObserver* observer); void RemovePhotoModelObserver(PhotoModelObserver* observer);
const PhotoModel& model() const { return model_; }
AmbientContainerView* GetAmbientContainerViewForTesting(); AmbientContainerView* GetAmbientContainerViewForTesting();
private: private:
...@@ -41,6 +46,8 @@ class ASH_EXPORT AmbientController : views::WidgetObserver { ...@@ -41,6 +46,8 @@ class ASH_EXPORT AmbientController : views::WidgetObserver {
void CreateContainerView(); void CreateContainerView();
void DestroyContainerView(); void DestroyContainerView();
void RefreshImage(); void RefreshImage();
void ScheduleRefreshImage();
void GetNextImage();
void OnPhotoDownloaded(const gfx::ImageSkia& image); void OnPhotoDownloaded(const gfx::ImageSkia& image);
AmbientContainerView* container_view_ = nullptr; AmbientContainerView* container_view_ = nullptr;
......
...@@ -8,6 +8,15 @@ ...@@ -8,6 +8,15 @@
namespace ash { namespace ash {
namespace {
// This class has a local in memory cache of downloaded photos. This is the
// desired max number of photos stored in cache. If this is an even number,
// the max number could be one larger.
constexpr int kImageBufferLength = 5;
} // namespace
PhotoModel::PhotoModel() = default; PhotoModel::PhotoModel() = default;
PhotoModel::~PhotoModel() = default; PhotoModel::~PhotoModel() = default;
...@@ -20,13 +29,70 @@ void PhotoModel::RemoveObserver(PhotoModelObserver* observer) { ...@@ -20,13 +29,70 @@ void PhotoModel::RemoveObserver(PhotoModelObserver* observer) {
observers_.RemoveObserver(observer); observers_.RemoveObserver(observer);
} }
bool PhotoModel::ShouldFetchImmediately() const {
// If currently shown image is close to the end of images cache, we prefetch
// more image.
const int next_load_image_index = GetImageBufferLength() / 2;
return images_.empty() ||
current_image_index_ >
static_cast<int>(images_.size() - 1 - next_load_image_index);
}
void PhotoModel::ShowNextImage() {
// Do not show next if have not downloaded enough images.
if (ShouldFetchImmediately())
return;
const int max_current_image_index = GetImageBufferLength() / 2;
if (current_image_index_ >= max_current_image_index) {
// Pop the first image and keep |current_image_index_| unchanged, will be
// equivalent to show next image.
images_.pop_front();
} else {
++current_image_index_;
}
NotifyImagesChanged();
}
void PhotoModel::AddNextImage(const gfx::ImageSkia& image) { void PhotoModel::AddNextImage(const gfx::ImageSkia& image) {
NotifyImageAvailable(image); images_.emplace_back(image);
// Update the first image.
if (images_.size() == 1)
NotifyImagesChanged();
}
gfx::ImageSkia PhotoModel::GetPrevImage() const {
if (current_image_index_ == 0)
return gfx::ImageSkia();
return images_[current_image_index_ - 1];
}
gfx::ImageSkia PhotoModel::GetCurrImage() const {
if (images_.empty())
return gfx::ImageSkia();
return images_[current_image_index_];
} }
void PhotoModel::NotifyImageAvailable(const gfx::ImageSkia& image) { gfx::ImageSkia PhotoModel::GetNextImage() const {
if (images_.empty() ||
static_cast<int>(images_.size() - current_image_index_) == 1) {
return gfx::ImageSkia();
}
return images_[current_image_index_ + 1];
}
void PhotoModel::NotifyImagesChanged() {
for (auto& observer : observers_) for (auto& observer : observers_)
observer.OnImageAvailable(image); observer.OnImagesChanged();
}
int PhotoModel::GetImageBufferLength() const {
return buffer_length_for_testing_ == -1 ? kImageBufferLength
: buffer_length_for_testing_;
} }
} // namespace ash } // namespace ash
...@@ -5,12 +5,11 @@ ...@@ -5,12 +5,11 @@
#ifndef ASH_AMBIENT_MODEL_PHOTO_MODEL_H_ #ifndef ASH_AMBIENT_MODEL_PHOTO_MODEL_H_
#define ASH_AMBIENT_MODEL_PHOTO_MODEL_H_ #define ASH_AMBIENT_MODEL_PHOTO_MODEL_H_
#include "ash/ash_export.h"
#include "base/containers/circular_deque.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/observer_list.h" #include "base/observer_list.h"
#include "ui/gfx/image/image_skia.h"
namespace gfx {
class ImageSkia;
} // namespace gfx
namespace ash { namespace ash {
...@@ -18,7 +17,7 @@ class PhotoModelObserver; ...@@ -18,7 +17,7 @@ class PhotoModelObserver;
// The model belonging to AmbientController which tracks photo state and // The model belonging to AmbientController which tracks photo state and
// notifies a pool of observers. // notifies a pool of observers.
class PhotoModel { class ASH_EXPORT PhotoModel {
public: public:
PhotoModel(); PhotoModel();
~PhotoModel(); ~PhotoModel();
...@@ -26,10 +25,36 @@ class PhotoModel { ...@@ -26,10 +25,36 @@ class PhotoModel {
void AddObserver(PhotoModelObserver* observer); void AddObserver(PhotoModelObserver* observer);
void RemoveObserver(PhotoModelObserver* observer); void RemoveObserver(PhotoModelObserver* observer);
// Prefetch one more image for ShowNextImage animations.
bool ShouldFetchImmediately() const;
// Show the next downloaded image.
void ShowNextImage();
// Add image to local storage.
void AddNextImage(const gfx::ImageSkia& image); void AddNextImage(const gfx::ImageSkia& image);
// Get images from local storage. Could be null image.
gfx::ImageSkia GetPrevImage() const;
gfx::ImageSkia GetCurrImage() const;
gfx::ImageSkia GetNextImage() const;
void set_buffer_length_for_testing(int length) {
buffer_length_for_testing_ = length;
}
private: private:
void NotifyImageAvailable(const gfx::ImageSkia& image); void NotifyImagesChanged();
int GetImageBufferLength() const;
// A local cache for downloaded images. This buffer is split into two equal
// length of kImageBufferLength / 2 for previous seen and next unseen images.
base::circular_deque<gfx::ImageSkia> images_;
// The index of currently shown image.
int current_image_index_ = 0;
int buffer_length_for_testing_ = -1;
base::ObserverList<ash::PhotoModelObserver> observers_; base::ObserverList<ash::PhotoModelObserver> observers_;
......
...@@ -8,18 +8,14 @@ ...@@ -8,18 +8,14 @@
#include "ash/public/cpp/ash_public_export.h" #include "ash/public/cpp/ash_public_export.h"
#include "base/observer_list_types.h" #include "base/observer_list_types.h"
namespace gfx {
class ImageSkia;
} // namespace gfx
namespace ash { namespace ash {
// A checked observer which receives notification of changes to the PhotoModel // A checked observer which receives notification of changes to the PhotoModel
// in ambient mode. // in ambient mode.
class ASH_PUBLIC_EXPORT PhotoModelObserver : public base::CheckedObserver { class ASH_PUBLIC_EXPORT PhotoModelObserver : public base::CheckedObserver {
public: public:
// Invoked when the requested image is available to show. // Invoked when prev/current/next images changed.
virtual void OnImageAvailable(const gfx::ImageSkia& image) = 0; virtual void OnImagesChanged() = 0;
protected: protected:
~PhotoModelObserver() override = default; ~PhotoModelObserver() override = default;
......
// Copyright 2019 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/ambient/model/photo_model.h"
#include "ash/ambient/model/photo_model_observer.h"
#include "ash/test/ash_test_base.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/views/controls/image_view.h"
namespace ash {
namespace {
// This class has a local in memory cache of downloaded photos. This is the max
// number of photos before and after currently shown image.
constexpr int kImageBufferLength = 3;
} // namespace
class PhotoModelTest : public AshTestBase {
public:
PhotoModelTest() = default;
~PhotoModelTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
model_ = std::make_unique<PhotoModel>();
model_->set_buffer_length_for_testing(kImageBufferLength);
}
void TearDown() override {
model_.reset();
AshTestBase::TearDown();
}
protected:
std::unique_ptr<PhotoModel> model_;
private:
DISALLOW_COPY_AND_ASSIGN(PhotoModelTest);
};
// Test adding the first image.
TEST_F(PhotoModelTest, AddFirstImage) {
gfx::ImageSkia first_image =
gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10);
model_->AddNextImage(first_image);
EXPECT_TRUE(model_->GetPrevImage().isNull());
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(first_image));
EXPECT_TRUE(model_->GetNextImage().isNull());
}
// Test adding the second image.
TEST_F(PhotoModelTest, AddSecondImage) {
gfx::ImageSkia first_image =
gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10);
gfx::ImageSkia second_image =
gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10);
// First |AddNextImage| will set |current_image_index_| to 0.
model_->AddNextImage(first_image);
model_->AddNextImage(second_image);
EXPECT_TRUE(model_->GetPrevImage().isNull());
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(first_image));
EXPECT_TRUE(model_->GetNextImage().BackedBySameObjectAs(second_image));
// Increment the |current_image_index_| to 1.
model_->ShowNextImage();
EXPECT_TRUE(model_->GetPrevImage().BackedBySameObjectAs(first_image));
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(second_image));
EXPECT_TRUE(model_->GetNextImage().isNull());
}
// Test adding the third image.
TEST_F(PhotoModelTest, AddThirdImage) {
gfx::ImageSkia first_image =
gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10);
gfx::ImageSkia second_image =
gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10);
gfx::ImageSkia third_image =
gfx::test::CreateImageSkia(/*width=*/10, /*height=*/10);
// The default |current_image_index_| is 0.
model_->AddNextImage(first_image);
model_->AddNextImage(second_image);
model_->AddNextImage(third_image);
EXPECT_TRUE(model_->GetPrevImage().isNull());
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(first_image));
EXPECT_TRUE(model_->GetNextImage().BackedBySameObjectAs(second_image));
// Increment the |current_image_index_| to 1.
model_->ShowNextImage();
EXPECT_TRUE(model_->GetPrevImage().BackedBySameObjectAs(first_image));
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(second_image));
EXPECT_TRUE(model_->GetNextImage().BackedBySameObjectAs(third_image));
// Pop the |images_| front and keep the |current_image_index_| to 1.
model_->ShowNextImage();
EXPECT_TRUE(model_->GetPrevImage().BackedBySameObjectAs(second_image));
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(third_image));
EXPECT_TRUE(model_->GetNextImage().isNull());
// ShowNextImage() will early return.
model_->ShowNextImage();
EXPECT_TRUE(model_->GetPrevImage().BackedBySameObjectAs(second_image));
EXPECT_TRUE(model_->GetCurrImage().BackedBySameObjectAs(third_image));
EXPECT_TRUE(model_->GetNextImage().isNull());
}
} // namespace ash
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
#include "ash/public/cpp/shell_window_ids.h" #include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h" #include "ash/shell.h"
#include "ui/aura/window.h" #include "ui/aura/window.h"
#include "ui/views/layout/fill_layout.h" #include "ui/views/background.h"
#include "ui/views/view.h" #include "ui/views/view.h"
#include "ui/views/widget/widget.h" #include "ui/views/widget/widget.h"
...@@ -56,7 +56,7 @@ const char* AmbientContainerView::GetClassName() const { ...@@ -56,7 +56,7 @@ const char* AmbientContainerView::GetClassName() const {
} }
gfx::Size AmbientContainerView::CalculatePreferredSize() const { gfx::Size AmbientContainerView::CalculatePreferredSize() const {
// TODO(wutao): Handle multiple displays. // TODO(b/139953389): Handle multiple displays.
return GetWidget()->GetNativeWindow()->GetRootWindow()->bounds().size(); return GetWidget()->GetNativeWindow()->GetRootWindow()->bounds().size();
} }
...@@ -76,7 +76,9 @@ void AmbientContainerView::OnGestureEvent(ui::GestureEvent* event) { ...@@ -76,7 +76,9 @@ void AmbientContainerView::OnGestureEvent(ui::GestureEvent* event) {
void AmbientContainerView::Init() { void AmbientContainerView::Init() {
CreateWidget(this); CreateWidget(this);
SetLayoutManager(std::make_unique<views::FillLayout>()); // TODO(b/139954108): Choose a better dark mode theme color.
SetBackground(views::CreateSolidBackground(SK_ColorBLACK));
photo_view_ = new PhotoView(ambient_controller_); photo_view_ = new PhotoView(ambient_controller_);
AddChildView(photo_view_); AddChildView(photo_view_);
} }
......
...@@ -4,9 +4,16 @@ ...@@ -4,9 +4,16 @@
#include "ash/ambient/ui/photo_view.h" #include "ash/ambient/ui/photo_view.h"
#include <algorithm>
#include "ash/ambient/ambient_controller.h" #include "ash/ambient/ambient_controller.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/image/image_skia_operations.h" #include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/controls/image_view.h" #include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
namespace ash { namespace ash {
...@@ -24,22 +31,75 @@ const char* PhotoView::GetClassName() const { ...@@ -24,22 +31,75 @@ const char* PhotoView::GetClassName() const {
return "PhotoView"; return "PhotoView";
} }
void PhotoView::OnBoundsChanged(const gfx::Rect& prev_bounds) { void PhotoView::AddedToWidget() {
image_view_->SetBoundsRect(GetLocalBounds()); // Set the bounds to show |image_view_curr_| for the first time.
// TODO(b/140066694): Handle display configuration changes, e.g. resolution,
// rotation, etc.
const gfx::Size widget_size = GetWidget()->GetRootView()->size();
image_view_prev_->SetImageSize(widget_size);
image_view_curr_->SetImageSize(widget_size);
image_view_next_->SetImageSize(widget_size);
gfx::Rect view_bounds = gfx::Rect(GetPreferredSize());
const int width = widget_size.width();
view_bounds.set_x(-width);
SetBoundsRect(view_bounds);
} }
void PhotoView::OnImageAvailable(const gfx::ImageSkia& image) { void PhotoView::OnImagesChanged() {
// TODO(wutao): Options to choose how to layout image, such as STRETCH, UpdateImages();
// CENTER, CENTER_CROPPED etc. StartSlideAnimation();
image_view_->SetImage(image);
} }
void PhotoView::Init() { void PhotoView::Init() {
image_view_ = new views::ImageView(); SetPaintToLayer();
AddChildView(image_view_); layer()->SetFillsBoundsOpaquely(false);
auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
image_view_prev_ = AddChildView(std::make_unique<views::ImageView>());
image_view_curr_ = AddChildView(std::make_unique<views::ImageView>());
image_view_next_ = AddChildView(std::make_unique<views::ImageView>());
// |ambient_controller_| outlives this view. // |ambient_controller_| outlives this view.
ambient_controller_->AddPhotoModelObserver(this); ambient_controller_->AddPhotoModelObserver(this);
} }
void PhotoView::UpdateImages() {
// TODO(b/140193766): Investigate a more efficient way to update images and do
// layer animation.
auto& model = ambient_controller_->model();
image_view_prev_->SetImage(model.GetPrevImage());
image_view_curr_->SetImage(model.GetCurrImage());
image_view_next_->SetImage(model.GetNextImage());
}
void PhotoView::StartSlideAnimation() {
if (!CanAnimate())
return;
ui::Layer* layer = this->layer();
const int x_offset = image_view_prev_->GetPreferredSize().width();
gfx::Transform transform;
transform.Translate(x_offset, 0);
layer->SetTransform(transform);
{
constexpr base::TimeDelta kDuration =
base::TimeDelta::FromMilliseconds(250);
ui::ScopedLayerAnimationSettings animation(layer->GetAnimator());
animation.SetTransitionDuration(kDuration);
animation.SetTweenType(gfx::Tween::EASE_OUT);
animation.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
layer->SetTransform(gfx::Transform());
}
}
bool PhotoView::CanAnimate() const {
// Cannot do slide animation from previous to current image.
return !image_view_prev_->GetImage().isNull();
}
} // namespace ash } // namespace ash
...@@ -26,16 +26,23 @@ class ASH_EXPORT PhotoView : public views::View, public PhotoModelObserver { ...@@ -26,16 +26,23 @@ class ASH_EXPORT PhotoView : public views::View, public PhotoModelObserver {
// views::View: // views::View:
const char* GetClassName() const override; const char* GetClassName() const override;
void OnBoundsChanged(const gfx::Rect& prev_bounds) override; void AddedToWidget() override;
// PhotoModelObserver: // PhotoModelObserver:
void OnImageAvailable(const gfx::ImageSkia& image) override; void OnImagesChanged() override;
private: private:
void Init(); void Init();
void UpdateImages();
void StartSlideAnimation();
bool CanAnimate() const;
AmbientController* ambient_controller_ = nullptr; AmbientController* ambient_controller_ = nullptr;
views::ImageView* image_view_; // Owned by view hierarchy.
// Image containers. Used for layer animation.
views::ImageView* image_view_prev_ = nullptr; // Owned by view hierarchy.
views::ImageView* image_view_curr_ = nullptr; // Owned by view hierarchy.
views::ImageView* image_view_next_ = nullptr; // Owned by view hierarchy.
DISALLOW_COPY_AND_ASSIGN(PhotoView); DISALLOW_COPY_AND_ASSIGN(PhotoView);
}; };
......
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