910e62b5创建于 1月15日历史提交
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/slider.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <utility>

#include "base/check_op.h"
#include "base/i18n/rtl.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/current_thread.h"
#include "build/build_config.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/widget/widget.h"

namespace views {

namespace {

// The thickness of the slider.
constexpr int kLineThickness = 2;

// The radius used to draw rounded slider ends.
constexpr float kSliderRoundedRadius = 2.f;

// The padding used to hide the slider underneath the thumb.
constexpr int kSliderPadding = 2;

// The radius of the highlighted thumb of the slider
constexpr float kThumbHighlightRadius = 12.f;

float GetNearestAllowedValue(const base::flat_set<float>& allowed_values,
                             float suggested_value) {
  if (allowed_values.empty()) {
    return suggested_value;
  }

  const base::flat_set<float>::const_iterator greater =
      allowed_values.upper_bound(suggested_value);
  if (greater == allowed_values.end()) {
    return *allowed_values.rbegin();
  }

  if (greater == allowed_values.begin()) {
    return *allowed_values.cbegin();
  }

  // Select a value nearest to the |suggested_value|.
  if ((*greater - suggested_value) > (suggested_value - *std::prev(greater))) {
    return *std::prev(greater);
  }

  return *greater;
}

}  // namespace

Slider::Slider(SliderListener* listener) : listener_(listener) {
  highlight_animation_.SetSlideDuration(base::Milliseconds(150));
  SetFlipCanvasOnPaintForRTLUI(true);

#if BUILDFLAG(IS_MAC)
  SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
#else
  SetFocusBehavior(FocusBehavior::ALWAYS);
#endif

  SchedulePaint();
  GetViewAccessibility().SetRole(ax::mojom::Role::kSlider);
  GetViewAccessibility().AddAction(ax::mojom::Action::kIncrement);
  GetViewAccessibility().AddAction(ax::mojom::Action::kDecrement);
}

Slider::~Slider() = default;

float Slider::GetValue() const {
  return value_;
}

void Slider::SetValue(float value) {
  SetValueInternal(value, SliderChangeReason::kByApi);
}

float Slider::GetValueIndicatorRadius() const {
  return value_indicator_radius_;
}

void Slider::SetValueIndicatorRadius(float radius) {
  value_indicator_radius_ = radius;
}

bool Slider::GetEnableAccessibilityEvents() const {
  return accessibility_events_enabled_;
}

void Slider::SetEnableAccessibilityEvents(bool enabled) {
  if (accessibility_events_enabled_ == enabled) {
    return;
  }
  accessibility_events_enabled_ = enabled;
  OnPropertyChanged(&accessibility_events_enabled_, PropertyEffects::kNone);
}

void Slider::SetRenderingStyle(RenderingStyle style) {
  style_ = style;
  SchedulePaint();
}

void Slider::SetAllowedValues(const base::flat_set<float>* allowed_values) {
  if (!allowed_values) {
    allowed_values_.clear();
    return;
  }
#if DCHECK_IS_ON()
  // Disallow empty sliders.
  DCHECK(allowed_values->size());
  for (const float v : *allowed_values) {
    // sanity check.
    DCHECK_GE(v, 0.0f);
    DCHECK_LE(v, 1.0f);
  }
#endif
  allowed_values_ = *allowed_values;

  const auto position = allowed_values_.lower_bound(value_);
  const float new_value = (position == allowed_values_.end())
                              ? *allowed_values_.cbegin()
                              : *position;
  if (new_value != value_) {
    SetValue(new_value);
  }
}

float Slider::GetAnimatingValue() const {
  return move_animation_ && move_animation_->is_animating()
             ? move_animation_->CurrentValueBetween(initial_animating_value_,
                                                    value_)
             : value_;
}

void Slider::SetHighlighted(bool is_highlighted) {
  if (is_highlighted) {
    highlight_animation_.Show();
  } else {
    highlight_animation_.Hide();
  }
}

void Slider::AnimationProgressed(const gfx::Animation* animation) {
  if (animation == &highlight_animation_) {
    thumb_highlight_radius_ =
        animation->CurrentValueBetween(kThumbRadius, kThumbHighlightRadius);
  }

  SchedulePaint();
}

void Slider::AnimationEnded(const gfx::Animation* animation) {
  if (animation == move_animation_.get()) {
    move_animation_.reset();
    return;
  }
  DCHECK_EQ(animation, &highlight_animation_);
}

void Slider::SetValueInternal(float value, SliderChangeReason reason) {
  bool old_value_valid = value_is_valid_;

  value_is_valid_ = true;
  if (value < 0.0) {
    value = 0.0;
  } else if (value > 1.0) {
    value = 1.0;
  }
  value = GetNearestAllowedValue(allowed_values_, value);
  if (value_ == value) {
    return;
  }
  float old_value = value_;
  value_ = value;
  UpdateAccessibleValue();
  if (listener_) {
    listener_->SliderValueChanged(this, value_, old_value, reason);
  }

  if (old_value_valid && base::CurrentThread::Get()) {
    // Do not animate when setting the value of the slider for the first time.
    // There is no message-loop when running tests. So we cannot animate then.
    if (!move_animation_) {
      initial_animating_value_ = old_value;
      move_animation_ = std::make_unique<gfx::SlideAnimation>(this);
      move_animation_->SetSlideDuration(base::Milliseconds(150));
      move_animation_->Show();
    }
    OnPropertyChanged(&value_, PropertyEffects::kNone);
  } else {
    OnPropertyChanged(&value_, PropertyEffects::kPaint);
  }

  if (accessibility_events_enabled_) {
    if (GetWidget() && GetWidget()->IsVisible() && GetVisible()) {
      DCHECK(!pending_accessibility_value_change_);
      NotifyAccessibilityEventDeprecated(ax::mojom::Event::kValueChanged, true);
    } else {
      pending_accessibility_value_change_ = true;
    }
  }
}

void Slider::PrepareForMove(const int new_x) {
  // Try to remember the position of the mouse cursor on the button.
  gfx::Insets inset = GetInsets();
  gfx::Rect content = GetContentsBounds();
  float value = GetAnimatingValue();

  const int thumb_x = value * (content.width() - 2 * value_indicator_radius_);
  const int candidate_x = GetMirroredXInView(new_x - inset.left()) - thumb_x;
  if (candidate_x >= value_indicator_radius_ - kThumbRadius &&
      candidate_x < value_indicator_radius_ + kThumbRadius) {
    initial_button_offset_ = candidate_x;
  } else {
    initial_button_offset_ = value_indicator_radius_;
  }
}

void Slider::MoveButtonTo(const gfx::Point& point) {
  const gfx::Insets inset = GetInsets();
  // Calculate the value.
  int amount = base::i18n::IsRTL()
                   ? width() - inset.left() - point.x() - initial_button_offset_
                   : point.x() - inset.left() - initial_button_offset_;
  SetValueInternal(static_cast<float>(amount) /
                       (width() - inset.width() - 2 * value_indicator_radius_),
                   SliderChangeReason::kByUser);
}

void Slider::OnSliderDragStarted() {
  SetHighlighted(true);
  if (listener_) {
    listener_->SliderDragStarted(this);
  }
}

void Slider::OnSliderDragEnded() {
  SetHighlighted(false);
  if (listener_) {
    listener_->SliderDragEnded(this);
  }
}

gfx::Size Slider::CalculatePreferredSize(
    const SizeBounds& available_size) const {
  constexpr int kSizeMajor = 200;
  constexpr int kSizeMinor = 40;

  return gfx::Size(std::max(available_size.width().value_or(0), kSizeMajor),
                   kSizeMinor);
}

bool Slider::OnMousePressed(const ui::MouseEvent& event) {
  if (!event.IsOnlyLeftMouseButton()) {
    return false;
  }
  OnSliderDragStarted();
  PrepareForMove(event.location().x());
  MoveButtonTo(event.location());
  return true;
}

bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
  MoveButtonTo(event.location());
  return true;
}

void Slider::OnMouseReleased(const ui::MouseEvent& event) {
  OnSliderDragEnded();
}

bool Slider::OnKeyPressed(const ui::KeyEvent& event) {
  int direction = 1;
  switch (event.key_code()) {
    case ui::VKEY_LEFT:
      direction = base::i18n::IsRTL() ? 1 : -1;
      break;
    case ui::VKEY_RIGHT:
      direction = base::i18n::IsRTL() ? -1 : 1;
      break;
    case ui::VKEY_UP:
      direction = 1;
      break;
    case ui::VKEY_DOWN:
      direction = -1;
      break;

    default:
      return false;
  }
  if (allowed_values_.empty()) {
    SetValueInternal(value_ + direction * keyboard_increment_,
                     SliderChangeReason::kByUser);
  } else {
    // discrete slider.
    if (direction > 0) {
      const base::flat_set<float>::const_iterator greater =
          allowed_values_.upper_bound(value_);
      SetValueInternal(greater == allowed_values_.cend()
                           ? *allowed_values_.crend()
                           : *greater,
                       SliderChangeReason::kByUser);
    } else {
      const base::flat_set<float>::const_iterator lesser =
          allowed_values_.lower_bound(value_);
      // Current value must be in the list of allowed values.
      DCHECK(lesser != allowed_values_.cend());
      SetValueInternal(lesser == allowed_values_.cbegin()
                           ? *allowed_values_.cbegin()
                           : *std::prev(lesser),
                       SliderChangeReason::kByUser);
    }
  }
  return true;
}

bool Slider::HandleAccessibleAction(const ui::AXActionData& action_data) {
  if (action_data.action == ax::mojom::Action::kIncrement) {
    SetValueInternal(value_ + keyboard_increment_, SliderChangeReason::kByUser);
    return true;
  } else if (action_data.action == ax::mojom::Action::kDecrement) {
    SetValueInternal(value_ - keyboard_increment_, SliderChangeReason::kByUser);
    return true;
  } else {
    return views::View::HandleAccessibleAction(action_data);
  }
}

void Slider::OnPaint(gfx::Canvas* canvas) {
  // Paint the slider.
  const gfx::Rect content = GetContentsBounds();
  const int width = content.width() - kThumbRadius * 2;
  const int full = GetAnimatingValue() * width;
  const int empty = width - full;
  const int y = content.height() / 2 - kLineThickness / 2;
  const int x = content.x() + full + kThumbRadius;

  cc::PaintFlags slider_flags;
  slider_flags.setAntiAlias(true);
  slider_flags.setColor(GetThumbColor());
  canvas->DrawRoundRect(
      gfx::Rect(content.x(), y, full - GetSliderExtraPadding(), kLineThickness),
      kSliderRoundedRadius, slider_flags);
  slider_flags.setColor(GetTroughColor());
  canvas->DrawRoundRect(
      gfx::Rect(x + kThumbRadius + GetSliderExtraPadding(), y,
                empty - GetSliderExtraPadding(), kLineThickness),
      kSliderRoundedRadius, slider_flags);

  gfx::Point thumb_center(x, content.height() / 2);

  // Paint the thumb highlight if it exists.
  const int thumb_highlight_radius =
      HasFocus() ? kThumbHighlightRadius : thumb_highlight_radius_;
  if (thumb_highlight_radius > kThumbRadius) {
    cc::PaintFlags highlight_background;
    highlight_background.setColor(GetTroughColor());
    highlight_background.setAntiAlias(true);
    canvas->DrawCircle(thumb_center, thumb_highlight_radius,
                       highlight_background);

    cc::PaintFlags highlight_border;
    highlight_border.setColor(GetThumbColor());
    highlight_border.setAntiAlias(true);
    highlight_border.setStyle(cc::PaintFlags::kStroke_Style);
    highlight_border.setStrokeWidth(kLineThickness);
    canvas->DrawCircle(thumb_center, thumb_highlight_radius, highlight_border);
  }

  // Paint the thumb of the slider.
  cc::PaintFlags flags;
  flags.setColor(GetThumbColor());
  flags.setAntiAlias(true);

  canvas->DrawCircle(thumb_center, kThumbRadius, flags);
}

void Slider::OnFocus() {
  View::OnFocus();
  SchedulePaint();
}

void Slider::OnBlur() {
  View::OnBlur();
  SchedulePaint();
}

void Slider::VisibilityChanged(View* starting_from, bool is_visible) {
  if (is_visible && GetWidget() && GetWidget()->IsVisible() && GetVisible()) {
    ApplyPendingAccessibleValueUpdate();
  }
}

void Slider::AddedToWidget() {
  if (GetWidget()->IsVisible() && GetVisible()) {
    ApplyPendingAccessibleValueUpdate();
  }
}

void Slider::ApplyPendingAccessibleValueUpdate() {
  if (!pending_accessibility_value_change_)
    return;

  NotifyAccessibilityEventDeprecated(ax::mojom::Event::kValueChanged, true);
  pending_accessibility_value_change_ = false;
}

void Slider::OnGestureEvent(ui::GestureEvent* event) {
  switch (event->type()) {
    // In a multi point gesture only the touch point will generate
    // an EventType::kGestureTapDown event.
    case ui::EventType::kGestureTapDown:
      OnSliderDragStarted();
      PrepareForMove(event->location().x());
      [[fallthrough]];
    case ui::EventType::kGestureScrollBegin:
    case ui::EventType::kGestureScrollUpdate:
      MoveButtonTo(event->location());
      event->SetHandled();
      break;
    case ui::EventType::kGestureEnd:
      MoveButtonTo(event->location());
      event->SetHandled();
      if (event->details().touch_points() <= 1) {
        OnSliderDragEnded();
      }
      break;
    default:
      break;
  }
}

SkColor Slider::GetThumbColor() const {
  switch (style_) {
    case RenderingStyle::kDefaultStyle:
      return GetColorProvider()->GetColor(ui::kColorSliderThumb);
    case RenderingStyle::kMinimalStyle:
      return GetColorProvider()->GetColor(ui::kColorSliderThumbMinimal);
  }
}

SkColor Slider::GetTroughColor() const {
  switch (style_) {
    case RenderingStyle::kDefaultStyle:
      return GetColorProvider()->GetColor(ui::kColorSliderTrack);
    case RenderingStyle::kMinimalStyle:
      return GetColorProvider()->GetColor(ui::kColorSliderTrackMinimal);
  }
}

int Slider::GetSliderExtraPadding() const {
  // Padding is negative when slider style is default so that there is no
  // separation between slider and thumb.
  switch (style_) {
    case RenderingStyle::kDefaultStyle:
      return -kSliderPadding;
    case RenderingStyle::kMinimalStyle:
      return kSliderPadding;
  }
}

void Slider::UpdateAccessibleValue() {
  views::ScopedAccessibilityEventBlocker scoped_event_blocker(
      GetViewAccessibility());
  GetViewAccessibility().SetValue(base::UTF8ToUTF16(
      base::StringPrintf("%d%%", static_cast<int>(value_ * 100 + 0.5))));
}

BEGIN_METADATA(Slider)
ADD_PROPERTY_METADATA(float, Value)
ADD_PROPERTY_METADATA(bool, EnableAccessibilityEvents)
ADD_PROPERTY_METADATA(float, ValueIndicatorRadius)
END_METADATA

}  // namespace views