chromium/ash/public/cpp/pagination/pagination_model.cc

// 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 "ash/public/cpp/pagination/pagination_model.h"

#include <algorithm>

#include "ash/public/cpp/pagination/pagination_model_observer.h"
#include "ui/gfx/animation/slide_animation.h"

namespace ash {

namespace {
// Dampening value for PaginationModel's SlideAnimation.
constexpr int kPageTransitionDurationDampening = 3;
}  // namespace

PaginationModel::PaginationModel(views::View* view)
    : views::AnimationDelegateViews(view),
      total_pages_(-1),
      selected_page_(-1),
      transition_(-1, 0),
      pending_selected_page_(-1) {}

PaginationModel::~PaginationModel() {}

void PaginationModel::SetTotalPages(int total_pages) {
  if (total_pages == total_pages_)
    return;

  int previous_pages = total_pages_;
  total_pages_ = total_pages;
  for (auto& observer : observers_) {
    observer.TotalPagesChanged(previous_pages, total_pages);
  }

  // The selected page may need to change due to total pages changing.
  if (selected_page_ < 0)
    SelectPage(0, false /* animate */);
  if (selected_page_ >= total_pages_)
    SelectPage(std::max(total_pages_ - 1, 0), false /* animate */);
}

void PaginationModel::SelectPage(int page, bool animate) {
  if (animate) {
    // -1 and |total_pages_| are valid target page for animation.
    DCHECK(page >= -1 && page <= total_pages_);

    if (!transition_animation_) {
      if (page == selected_page_)
        return;

      // Creates an animation if there is not one.
      StartTransitionAnimation(Transition(page, 0));
      return;
    } else {
      const bool showing = transition_animation_->IsShowing();
      const int from_page = showing ? selected_page_ : transition_.target_page;
      const int to_page = showing ? transition_.target_page : selected_page_;

      if (from_page == page) {
        if (showing)
          transition_animation_->Hide();
        else
          transition_animation_->Show();
        pending_selected_page_ = -1;
      } else if (to_page != page) {
        pending_selected_page_ = page;
      } else {
        pending_selected_page_ = -1;
      }
    }
  } else {
    DCHECK(total_pages_ == 0 || (page >= 0 && page < total_pages_));

    if (page == selected_page_)
      return;

    ResetTransitionAnimation();

    int old_selected = selected_page_;
    selected_page_ = page;
    NotifySelectedPageChanged(old_selected, selected_page_);
  }
}

void PaginationModel::SelectPageRelative(int delta, bool animate) {
  SelectPage(CalculateTargetPage(delta), animate);
}

bool PaginationModel::IsValidPageRelative(int delta) const {
  if (total_pages_ <= 0)
    return false;

  const int target_page = SelectedTargetPage() + delta;

  return target_page >= 0 && target_page <= (total_pages_ - 1);
}

void PaginationModel::FinishAnimation() {
  SelectPage(SelectedTargetPage(), false);
}

void PaginationModel::SetTransition(const Transition& transition) {
  // -1 and |total_pages_| is a valid target page, which means user is at
  // the end and there is no target page for this scroll.
  DCHECK(transition.target_page >= -1 &&
         transition.target_page <= total_pages_);
  DCHECK(transition.progress >= 0 && transition.progress <= 1);

  if (transition_.Equals(transition))
    return;

  transition_ = transition;
  NotifyTransitionChanged();
}

void PaginationModel::SetTransitionDurations(
    base::TimeDelta duration,
    base::TimeDelta overscroll_duration) {
  transition_duration_ = duration;
  overscroll_transition_duration_ = overscroll_duration;
}

void PaginationModel::StartScroll() {
  NotifyScrollStarted();

  // Cancels current transition animation (if any).
  transition_animation_.reset();
}

void PaginationModel::UpdateScroll(double delta) {
  // Translates scroll delta to desired page change direction.
  int page_change_dir = delta > 0 ? -1 : 1;

  // Initializes a transition if there is none.
  if (!has_transition())
    transition_.target_page = CalculateTargetPage(page_change_dir);

  // Updates transition progress.
  int transition_dir = transition_.target_page > selected_page_ ? 1 : -1;
  double progress =
      transition_.progress + fabs(delta) * page_change_dir * transition_dir;

  if (progress < 0) {
    if (transition_.progress) {
      transition_.progress = 0;
      NotifyTransitionChanged();
    }
    clear_transition();
  } else if (progress > 1) {
    if (is_valid_page(transition_.target_page)) {
      SelectPage(transition_.target_page, false);
      clear_transition();
    }
  } else {
    transition_.progress = progress;
    NotifyTransitionChanged();
  }
}

void PaginationModel::EndScroll(bool cancel) {
  NotifyScrollEnded();

  if (!has_transition())
    return;

  StartTransitionAnimation(transition_);

  if (cancel)
    transition_animation_->Hide();
}

bool PaginationModel::IsRevertingCurrentTransition() const {
  // Use !IsShowing() so that we return true at the end of hide animation.
  return transition_animation_ && !transition_animation_->IsShowing();
}

void PaginationModel::AddObserver(PaginationModelObserver* observer) {
  observers_.AddObserver(observer);
}

void PaginationModel::RemoveObserver(PaginationModelObserver* observer) {
  observers_.RemoveObserver(observer);
}

int PaginationModel::SelectedTargetPage() const {
  // If no animation, or animation is in reverse, just the selected page.
  if (!transition_animation_ || !transition_animation_->IsShowing())
    return selected_page_;

  // If, at the end of the current animation, we will animate to another page,
  // return that eventual page.
  if (pending_selected_page_ >= 0)
    return pending_selected_page_;

  // Just the target of the current animation.
  return transition_.target_page;
}

void PaginationModel::NotifySelectedPageChanged(int old_selected,
                                                int new_selected) {
  for (auto& observer : observers_)
    observer.SelectedPageChanged(old_selected, new_selected);
}

void PaginationModel::NotifyTransitionAboutToStart() {
  for (auto& observer : observers_)
    observer.TransitionStarting();
}

void PaginationModel::NotifyTransitionStarted() {
  for (auto& observer : observers_)
    observer.TransitionStarted();
}

void PaginationModel::NotifyTransitionChanged() {
  for (auto& observer : observers_)
    observer.TransitionChanged();
}

void PaginationModel::NotifyTransitionEnded() {
  for (auto& observer : observers_)
    observer.TransitionEnded();
}

void PaginationModel::NotifyScrollStarted() {
  for (auto& observer : observers_)
    observer.ScrollStarted();
}

void PaginationModel::NotifyScrollEnded() {
  for (auto& observer : observers_)
    observer.ScrollEnded();
}

int PaginationModel::CalculateTargetPage(int delta) const {
  DCHECK_GT(total_pages_, 0);
  const int target_page = SelectedTargetPage() + delta;

  int start_page = 0;
  int end_page = total_pages_ - 1;

  // Use invalid page when |selected_page_| is at ends.
  if (target_page < start_page && selected_page_ == start_page)
    start_page = -1;
  else if (target_page > end_page && selected_page_ == end_page)
    end_page = total_pages_;

  return std::clamp(target_page, start_page, end_page);
}

base::TimeDelta PaginationModel::GetTransitionAnimationSlideDuration() const {
  return transition_animation_ ? transition_animation_->GetSlideDuration()
                               : base::TimeDelta();
}

void PaginationModel::StartTransitionAnimation(const Transition& transition) {
  DCHECK(selected_page_ != transition.target_page);

  NotifyTransitionAboutToStart();
  SetTransition(transition);

  transition_animation_ = std::make_unique<gfx::SlideAnimation>(this);
  transition_animation_->SetDampeningValue(kPageTransitionDurationDampening);
  transition_animation_->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  transition_animation_->Reset(transition_.progress);

  const base::TimeDelta duration = is_valid_page(transition_.target_page)
                                       ? transition_duration_
                                       : overscroll_transition_duration_;
  if (!duration.is_zero())
    transition_animation_->SetSlideDuration(duration);

  is_transition_started_ = true;
  NotifyTransitionStarted();
  transition_animation_->Show();
}

void PaginationModel::ResetTransitionAnimation() {
  transition_animation_.reset();
  transition_.target_page = -1;
  transition_.progress = 0;
  pending_selected_page_ = -1;
}

void PaginationModel::AnimationProgressed(const gfx::Animation* animation) {
  transition_.progress = transition_animation_->GetCurrentValue();
  NotifyTransitionChanged();
}

void PaginationModel::AnimationEnded(const gfx::Animation* animation) {
  // Ensure that each TransitionStarted() has only one TransitionEnded().
  if (is_transition_started_) {
    is_transition_started_ = false;
    NotifyTransitionEnded();
  }

  // Save |pending_selected_page_| because SelectPage resets it.
  int next_target = pending_selected_page_;

  if (transition_animation_->GetCurrentValue() == 1) {
    // Showing animation ends.
    if (!is_valid_page(transition_.target_page)) {
      // If target page is not in valid range, reverse the animation.
      transition_animation_->Hide();
      return;
    }

    // Otherwise, change page and finish the transition.
    DCHECK(selected_page_ != transition_.target_page);
    SelectPage(transition_.target_page, false /* animate */);
  } else if (transition_animation_->GetCurrentValue() == 0) {
    // Hiding animation ends. No page change should happen.
    ResetTransitionAnimation();
  }

  if (next_target >= 0)
    SelectPage(next_target, true);
}

}  // namespace ash