// Copyright 2020 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/ambient/ui/ambient_background_image_view.h"
#include <memory>
#include "ash/ambient/ambient_constants.h"
#include "ash/ambient/ui/ambient_slideshow_peripheral_ui.h"
#include "ash/ambient/ui/ambient_view_delegate.h"
#include "ash/ambient/ui/ambient_view_ids.h"
#include "ash/ambient/util/ambient_util.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/skbitmap_operations.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
gfx::ImageSkia ResizeImage(const gfx::ImageSkia& image,
const gfx::Size& view_size,
const bool force_resize_to_fit) {
if (image.isNull())
return gfx::ImageSkia();
const double image_width = image.width();
const double image_height = image.height();
const double view_width = view_size.width();
const double view_height = view_size.height();
const double horizontal_ratio = view_width / image_width;
const double vertical_ratio = view_height / image_height;
const double image_ratio = image_height / image_width;
const double view_ratio = view_height / view_width;
double scale = 1.0;
// If force fitting is enabled, we will always scale to the smaller ratio to
// ensure that no part of the image is cropped out and the whole image is
// shown on the screen with possible black bars.
if (force_resize_to_fit) {
scale = std::min(horizontal_ratio, vertical_ratio);
} else {
// If the image and the container view has the same orientation, e.g. both
// portrait, the |scale| will make the image filled the whole view with
// possible cropping on one direction. If they are in different orientation,
// the |scale| will display the image in the view without any cropping, but
// with empty background.
scale = (image_ratio - 1) * (view_ratio - 1) > 0
? std::max(horizontal_ratio, vertical_ratio)
: std::min(horizontal_ratio, vertical_ratio);
}
const gfx::Size& resized = gfx::ScaleToCeiledSize(image.size(), scale);
return gfx::ImageSkiaOperations::CreateResizedImage(
image, skia::ImageOperations::RESIZE_BEST, resized);
}
gfx::ImageSkia MaybeRotateImage(const gfx::ImageSkia& image,
const gfx::Size& view_size,
views::Widget* widget) {
if (image.isNull())
return image;
const double image_width = image.width();
const double image_height = image.height();
const double view_width = view_size.width();
const double view_height = view_size.height();
const double image_ratio = image_height / image_width;
const double view_ratio = view_height / view_width;
// Rotate the image to have the same orientation as the display.
// Keep the relative orientation between the image and the display in portrait
// mode.
if ((image_ratio - 1) * (view_ratio - 1) < 0) {
bool should_rotate = false;
SkBitmapOperations::RotationAmount rotation_amount;
const int64_t display_id =
display::Screen::GetScreen()
->GetDisplayNearestWindow(widget->GetNativeWindow())
.id();
const auto active_rotation = Shell::Get()
->display_manager()
->GetDisplayInfo(display_id)
.GetActiveRotation();
switch (active_rotation) {
case display::Display::ROTATE_90:
should_rotate = true;
rotation_amount = SkBitmapOperations::RotationAmount::ROTATION_270_CW;
break;
case display::Display::ROTATE_270:
should_rotate = true;
rotation_amount = SkBitmapOperations::RotationAmount::ROTATION_90_CW;
break;
default:
// No action.
break;
}
if (should_rotate) {
return gfx::ImageSkiaOperations::CreateRotatedImage(image,
rotation_amount);
}
}
return image;
}
} // namespace
AmbientBackgroundImageView::AmbientBackgroundImageView(
AmbientViewDelegate* delegate)
: delegate_(delegate) {
DCHECK(delegate_);
SetID(AmbientViewID::kAmbientBackgroundImageView);
InitLayout();
}
AmbientBackgroundImageView::~AmbientBackgroundImageView() = default;
void AmbientBackgroundImageView::OnBoundsChanged(
const gfx::Rect& previous_bounds) {
if (!GetVisible())
return;
if (width() == 0)
return;
UpdateLayout();
// When bounds changes, recalculate the visibility of related image view.
UpdateRelatedImageViewVisibility();
UpdateImageDetails(details_, related_details_);
}
void AmbientBackgroundImageView::OnViewBoundsChanged(
views::View* observed_view) {
if (observed_view == image_view_)
SetResizedImage(image_view_, image_unscaled_);
else
SetResizedImage(related_image_view_, related_image_unscaled_);
}
void AmbientBackgroundImageView::UpdateImage(
const gfx::ImageSkia& image,
const gfx::ImageSkia& related_image,
bool is_portrait,
::ambient::TopicType type) {
image_unscaled_ = image;
related_image_unscaled_ = related_image;
is_portrait_ = is_portrait;
topic_type_ = type;
ambient_peripheral_ui_->UpdateGlanceableInfoPosition();
const bool has_change = UpdateRelatedImageViewVisibility();
// If there is no change in the visibility of related image view, call
// SetResizedImages() directly. Otherwise it will be called from
// OnViewBoundsChanged().
if (!has_change) {
SetResizedImage(image_view_, image_unscaled_);
SetResizedImage(related_image_view_, related_image_unscaled_);
}
}
void AmbientBackgroundImageView::UpdateImageDetails(
const std::u16string& details,
const std::u16string& related_details) {
details_ = details;
related_details_ = related_details;
ambient_peripheral_ui_->UpdateImageDetails(
details, MustShowPairs() ? related_details : std::u16string());
}
gfx::ImageSkia AmbientBackgroundImageView::GetCurrentImage() {
return image_view_->GetImage();
}
gfx::Rect AmbientBackgroundImageView::GetImageBoundsInScreenForTesting() const {
gfx::Rect rect = image_view_->GetImageBounds();
views::View::ConvertRectToScreen(image_view_, &rect);
return rect;
}
gfx::Rect AmbientBackgroundImageView::GetRelatedImageBoundsInScreenForTesting()
const {
if (!related_image_view_->GetVisible())
return gfx::Rect();
gfx::Rect rect = related_image_view_->GetImageBounds();
views::View::ConvertRectToScreen(related_image_view_, &rect);
return rect;
}
void AmbientBackgroundImageView::ResetRelatedImageForTesting() {
related_image_unscaled_ = gfx::ImageSkia();
UpdateRelatedImageViewVisibility();
}
void AmbientBackgroundImageView::InitLayout() {
static const views::FlexSpecification kUnboundedScaleToZero(
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded);
SetLayoutManager(std::make_unique<views::FillLayout>());
// Inits container for images.
image_container_ = AddChildView(std::make_unique<views::View>());
image_layout_ =
image_container_->SetLayoutManager(std::make_unique<views::FlexLayout>());
image_view_ =
image_container_->AddChildView(std::make_unique<views::ImageView>());
// Set a place holder size for Flex layout to assign bounds.
image_view_->SetPreferredSize(gfx::Size(1, 1));
image_view_->SetProperty(views::kFlexBehaviorKey, kUnboundedScaleToZero);
observed_views_.AddObservation(image_view_.get());
related_image_view_ =
image_container_->AddChildView(std::make_unique<views::ImageView>());
// Set a place holder size for Flex layout to assign bounds.
related_image_view_->SetPreferredSize(gfx::Size(1, 1));
related_image_view_->SetProperty(views::kFlexBehaviorKey,
kUnboundedScaleToZero);
observed_views_.AddObservation(related_image_view_.get());
ambient_peripheral_ui_ =
AddChildView(std::make_unique<AmbientSlideshowPeripheralUi>(delegate_));
}
void AmbientBackgroundImageView::UpdateLayout() {
if (width() > height()) {
image_layout_->SetOrientation(views::LayoutOrientation::kHorizontal);
// Set spacing between two images.
related_image_view_->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(0, kMarginLeftOfRelatedImageDip, 0, 0));
} else {
image_layout_->SetOrientation(views::LayoutOrientation::kVertical);
// Set spacing between two images.
related_image_view_->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(kMarginLeftOfRelatedImageDip, 0, 0, 0));
}
image_layout_->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
image_layout_->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
}
bool AmbientBackgroundImageView::UpdateRelatedImageViewVisibility() {
const bool did_show_pair = related_image_view_->GetVisible();
const bool show_pair = MustShowPairs() && HasPairedImages();
related_image_view_->SetVisible(show_pair);
return did_show_pair != show_pair;
}
void AmbientBackgroundImageView::SetResizedImage(
views::ImageView* image_view,
const gfx::ImageSkia& image_unscaled) {
if (!image_view->GetVisible())
return;
if (image_unscaled.isNull())
return;
gfx::ImageSkia image_rotated =
topic_type_ == ::ambient::TopicType::kGeo
? MaybeRotateImage(image_unscaled, image_view->size(), GetWidget())
: image_unscaled;
image_view->SetImage(
ResizeImage(image_rotated, image_view->size(), force_resize_to_fit_));
// Intend to update the image origin in image view.
// There is no bounds change or preferred size change when updating image from
// landscape to portrait when device is in portrait orientation because we
// only show one photo. Call ResetImageSize() to trigger UpdateImageOrigin().
image_view->ResetImageSize();
}
void AmbientBackgroundImageView::SetPeripheralUiVisibility(bool visible) {
ambient_peripheral_ui_->SetVisible(visible);
}
void AmbientBackgroundImageView::SetForceResizeToFit(bool force_resize_to_fit) {
force_resize_to_fit_ = force_resize_to_fit;
}
bool AmbientBackgroundImageView::MustShowPairs() const {
const bool landscape_mode_portrait_image = width() > height() && is_portrait_;
const bool portrait_mode_landscape_image =
width() < height() && !is_portrait_;
return landscape_mode_portrait_image || portrait_mode_landscape_image;
}
bool AmbientBackgroundImageView::HasPairedImages() const {
return !image_unscaled_.isNull() && !related_image_unscaled_.isNull();
}
BEGIN_METADATA(AmbientBackgroundImageView)
END_METADATA
} // namespace ash