chromium/ash/assistant/ui/logo_view/logo_view_impl.cc

// Copyright 2018 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/assistant/ui/logo_view/logo_view_impl.h"

#include <algorithm>

#include "ash/assistant/ui/logo_view/shape/shape.h"
#include "base/check.h"
#include "base/containers/adapters.h"
#include "base/notreached.h"
#include "chromeos/assistant/internal/logo_view/logo_model/dot.h"
#include "chromeos/assistant/internal/logo_view/logo_view_constants.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/vector2d.h"

namespace ash {

namespace {

int64_t TimeTicksToMs(const base::TimeTicks& timestamp) {
  return (timestamp - base::TimeTicks()).InMilliseconds();
}

}  // namespace

LogoViewImpl::LogoViewImpl()
    : mic_part_shape_(chromeos::assistant::kDotDefaultSize),
      state_animator_(&logo_, &state_model_, StateModel::State::kUndefined) {
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  state_animator_.SetStateAnimatorTimerDelegate(this);
  state_animator_.SetLogoInputValueProvider(StateModel::State::kUserSpeaks,
                                            &sound_level_input_value_provider_);
}

LogoViewImpl::~LogoViewImpl() {
  state_animator_.StopAnimator();
}

void LogoViewImpl::SetState(LogoView::State state, bool animate) {
  StateModel::State animator_state;
  switch (state) {
    case LogoView::State::kUndefined:
      animator_state = StateModel::State::kUndefined;
      break;
    case LogoView::State::kListening:
      animator_state = StateModel::State::kListening;
      break;
    case LogoView::State::kMic:
      animator_state = StateModel::State::kMic;
      break;
    case LogoView::State::kUserSpeaks:
      animator_state = StateModel::State::kUserSpeaks;
      break;
  }
  state_animator_.SwitchStateTo(animator_state, !animate);
}

void LogoViewImpl::SetSpeechLevel(float speech_level) {
  sound_level_input_value_provider_.SetSpeechLevel(speech_level);
}

int64_t LogoViewImpl::StartTimer() {
  // Remove animation observer from previous |StartTimer| if exists.
  StopTimer();

  ui::Compositor* compositor = layer()->GetCompositor();
  if (compositor && !compositor->HasAnimationObserver(this)) {
    animating_compositor_ = compositor;
    animating_compositor_->AddAnimationObserver(this);
  }
  return TimeTicksToMs(base::TimeTicks::Now());
}

void LogoViewImpl::StopTimer() {
  if (animating_compositor_ &&
      animating_compositor_->HasAnimationObserver(this)) {
    animating_compositor_->RemoveAnimationObserver(this);
  }
  animating_compositor_ = nullptr;
}

void LogoViewImpl::OnAnimationStep(base::TimeTicks timestamp) {
  const int64_t current_time_ms = TimeTicksToMs(timestamp);
  state_animator_.OnTimeUpdate(current_time_ms);
  SchedulePaint();
}

void LogoViewImpl::OnCompositingShuttingDown(ui::Compositor* compositor) {
  DCHECK(compositor);
  if (animating_compositor_ == compositor) {
    StopTimer();
  }
}

void LogoViewImpl::DrawDots(gfx::Canvas* canvas) {
  // TODO: The Green Mic parts seems overlapped on the Red Mic part. Draw dots
  // in reverse order so that the Red Mic part is on top of Green Mic parts. But
  // we need to find out why the Mic parts are overlapping in the first place.
  for (const auto& dot : base::Reversed(logo_.dots())) {
    DrawDot(canvas, dot.get());
  }
}

void LogoViewImpl::DrawDot(gfx::Canvas* canvas, Dot* dot) {
  const float radius = dot->GetRadius();
  const float angle = logo_.GetRotation() + dot->GetAngle();
  const float x = radius * std::cos(angle) + dot->GetOffsetX();
  const float y = radius * std::sin(angle) + dot->GetOffsetY();

  if (dot->IsMic()) {
    DrawMicPart(canvas, dot, x, y);
  } else if (dot->IsLetter()) {
    // TODO(b/79579731): Implement the letter animation.
    NOTIMPLEMENTED();
  } else if (dot->IsLine()) {
    DrawLine(canvas, dot, x, y);
  } else {
    DrawCircle(canvas, dot, x, y);
  }
}

void LogoViewImpl::DrawMicPart(gfx::Canvas* canvas,
                               Dot* dot,
                               float x,
                               float y) {
  const float progress = dot->GetMicMorph();
  mic_part_shape_.Reset();
  mic_part_shape_.ToMicPart(progress, dot->dot_color());
  mic_part_shape_.Transform(x, y, dots_scale_);
  DrawShape(canvas, &mic_part_shape_, dot->color());
}

void LogoViewImpl::DrawShape(gfx::Canvas* canvas, Shape* shape, SkColor color) {
  cc::PaintFlags paint_flags;
  paint_flags.setAntiAlias(true);
  paint_flags.setColor(color);
  paint_flags.setAlphaf(logo_.GetAlpha());
  paint_flags.setStyle(cc::PaintFlags::kStroke_Style);
  paint_flags.setStrokeCap(shape->cap());

  paint_flags.setStrokeWidth(shape->first_stroke_width());
  canvas->DrawPath(*shape->first_path(), paint_flags);

  paint_flags.setStrokeWidth(shape->second_stroke_width());
  canvas->DrawPath(*shape->second_path(), paint_flags);
}

void LogoViewImpl::DrawLine(gfx::Canvas* canvas, Dot* dot, float x, float y) {
  const float stroke_width = dot->GetSize() * dots_scale_;
  cc::PaintFlags paint_flags;
  paint_flags.setAntiAlias(true);
  paint_flags.setColor(dot->color());
  paint_flags.setAlphaf(logo_.GetAlpha());
  paint_flags.setStrokeWidth(stroke_width);
  paint_flags.setStyle(cc::PaintFlags::kStroke_Style);
  paint_flags.setStrokeCap(cc::PaintFlags::kRound_Cap);

  const float line_length = dot->GetLineLength();
  const float line_x = x * dots_scale_;
  const float line_top_y = (y - line_length) * dots_scale_;
  const float line_bottom_y = (y + line_length) * dots_scale_;
  canvas->DrawLine(gfx::PointF(line_x, line_top_y),
                   gfx::PointF(line_x, line_bottom_y), paint_flags);
}

void LogoViewImpl::DrawCircle(gfx::Canvas* canvas, Dot* dot, float x, float y) {
  const float radius = dot->GetSize() * dot->GetVisibility() / 2.0f;
  cc::PaintFlags paint_flags;
  paint_flags.setAntiAlias(true);
  paint_flags.setColor(dot->color());
  paint_flags.setAlphaf(logo_.GetAlpha());
  paint_flags.setStyle(cc::PaintFlags::kFill_Style);
  canvas->DrawCircle(gfx::PointF(x * dots_scale_, y * dots_scale_),
                     radius * dots_scale_, paint_flags);
}

void LogoViewImpl::OnPaint(gfx::Canvas* canvas) {
  View::OnPaint(canvas);

  canvas->Save();
  gfx::RectF content_bounds(GetContentsBounds());
  gfx::InsetsF insets(GetInsets());
  const float offset_x = insets.left() + content_bounds.width() / 2.0f;
  const float offset_y = insets.top() + content_bounds.height() / 2.0f;
  canvas->Translate(gfx::Vector2d(offset_x, offset_y));
  DrawDots(canvas);
  canvas->Restore();
}

void LogoViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  gfx::Rect content_bounds(GetContentsBounds());
  if (content_bounds.IsEmpty()) {
    return;
  }

  // Sets a scale such that an object of the specified width and height will
  // fill the view while keeping the aspect ratio if drawn at that scale.
  constexpr float kDefaultWidth = 28.0f;
  constexpr float kDefaultHeight = 25.0f;
  const float x_scale = content_bounds.width() / kDefaultWidth;
  const float y_scale = content_bounds.height() / kDefaultHeight;
  dots_scale_ = std::fmin(x_scale, y_scale);
}

void LogoViewImpl::VisibilityChanged(views::View* starting_from,
                                     bool is_visible) {
  if (IsDrawn()) {
    state_animator_.StartAnimator();
  } else {
    state_animator_.StopAnimator();
  }
}

BEGIN_METADATA(LogoViewImpl)
END_METADATA

}  // namespace ash