1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ui/app_list/pagination_model.h"
6
7#include <algorithm>
8
9#include "ui/app_list/pagination_model_observer.h"
10#include "ui/gfx/animation/slide_animation.h"
11
12namespace app_list {
13
14PaginationModel::PaginationModel()
15    : total_pages_(-1),
16      selected_page_(-1),
17      transition_(-1, 0),
18      pending_selected_page_(-1),
19      transition_duration_ms_(0),
20      overscroll_transition_duration_ms_(0),
21      last_overscroll_target_page_(0) {
22}
23
24PaginationModel::~PaginationModel() {
25}
26
27void PaginationModel::SetTotalPages(int total_pages) {
28  if (total_pages == total_pages_)
29    return;
30
31  total_pages_ = total_pages;
32  if (selected_page_ < 0)
33    SelectPage(0, false /* animate */);
34  if (selected_page_ >= total_pages_)
35    SelectPage(std::max(total_pages_ - 1, 0), false /* animate */);
36  FOR_EACH_OBSERVER(PaginationModelObserver, observers_, TotalPagesChanged());
37}
38
39void PaginationModel::SelectPage(int page, bool animate) {
40  if (animate) {
41    // -1 and |total_pages_| are valid target page for animation.
42    DCHECK(page >= -1 && page <= total_pages_);
43
44    if (!transition_animation_) {
45      if (page == selected_page_)
46        return;
47
48      // Suppress over scroll animation if the same one happens too fast.
49      if (!is_valid_page(page)) {
50        const base::TimeTicks now = base::TimeTicks::Now();
51
52        if (page == last_overscroll_target_page_) {
53          const int kMinOverScrollTimeGapInMs = 500;
54          const base::TimeDelta time_elapsed =
55               now - last_overscroll_animation_start_time_;
56          if (time_elapsed.InMilliseconds() < kMinOverScrollTimeGapInMs)
57            return;
58        }
59
60        last_overscroll_target_page_ = page;
61        last_overscroll_animation_start_time_ = now;
62      }
63
64      // Creates an animation if there is not one.
65      StartTransitionAnimation(Transition(page, 0));
66      return;
67    } else {
68      const bool showing = transition_animation_->IsShowing();
69      const int from_page = showing ? selected_page_ : transition_.target_page;
70      const int to_page = showing ? transition_.target_page : selected_page_;
71
72      if (from_page == page) {
73        if (showing)
74          transition_animation_->Hide();
75        else
76          transition_animation_->Show();
77        pending_selected_page_ = -1;
78      } else if (to_page != page) {
79        pending_selected_page_ = page;
80      } else {
81        pending_selected_page_ = -1;
82      }
83    }
84  } else {
85    DCHECK(total_pages_ == 0 || (page >= 0 && page < total_pages_));
86
87    if (page == selected_page_)
88      return;
89
90    ResetTransitionAnimation();
91
92    int old_selected = selected_page_;
93    selected_page_ = page;
94    NotifySelectedPageChanged(old_selected, selected_page_);
95  }
96}
97
98void PaginationModel::SelectPageRelative(int delta, bool animate) {
99  SelectPage(CalculateTargetPage(delta), animate);
100}
101
102void PaginationModel::FinishAnimation() {
103  SelectPage(SelectedTargetPage(), false);
104}
105
106void PaginationModel::SetTransition(const Transition& transition) {
107  // -1 and |total_pages_| is a valid target page, which means user is at
108  // the end and there is no target page for this scroll.
109  DCHECK(transition.target_page >= -1 &&
110         transition.target_page <= total_pages_);
111  DCHECK(transition.progress >= 0 && transition.progress <= 1);
112
113  if (transition_.Equals(transition))
114    return;
115
116  transition_ = transition;
117  NotifyTransitionChanged();
118}
119
120void PaginationModel::SetTransitionDurations(int duration_ms,
121                                             int overscroll_duration_ms) {
122  transition_duration_ms_ = duration_ms;
123  overscroll_transition_duration_ms_ = overscroll_duration_ms;
124}
125
126void PaginationModel::StartScroll() {
127  // Cancels current transition animation (if any).
128  transition_animation_.reset();
129}
130
131void PaginationModel::UpdateScroll(double delta) {
132  // Translates scroll delta to desired page change direction.
133  int page_change_dir = delta > 0 ? -1 : 1;
134
135  // Initializes a transition if there is none.
136  if (!has_transition())
137    transition_.target_page = CalculateTargetPage(page_change_dir);
138
139  // Updates transition progress.
140  int transition_dir = transition_.target_page > selected_page_ ? 1 : -1;
141  double progress = transition_.progress +
142      fabs(delta) * page_change_dir * transition_dir;
143
144  if (progress < 0) {
145    if (transition_.progress) {
146      transition_.progress = 0;
147      NotifyTransitionChanged();
148    }
149    clear_transition();
150  } else if (progress > 1) {
151    if (is_valid_page(transition_.target_page)) {
152      SelectPage(transition_.target_page, false);
153      clear_transition();
154    }
155  } else {
156    transition_.progress = progress;
157    NotifyTransitionChanged();
158  }
159}
160
161void PaginationModel::EndScroll(bool cancel) {
162  if (!has_transition())
163    return;
164
165  StartTransitionAnimation(transition_);
166
167  if (cancel)
168    transition_animation_->Hide();
169}
170
171bool PaginationModel::IsRevertingCurrentTransition() const {
172  // Use !IsShowing() so that we return true at the end of hide animation.
173  return transition_animation_ && !transition_animation_->IsShowing();
174}
175
176void PaginationModel::AddObserver(PaginationModelObserver* observer) {
177  observers_.AddObserver(observer);
178}
179
180void PaginationModel::RemoveObserver(PaginationModelObserver* observer) {
181  observers_.RemoveObserver(observer);
182}
183
184int PaginationModel::SelectedTargetPage() const {
185  // If no animation, or animation is in reverse, just the selected page.
186  if (!transition_animation_ || !transition_animation_->IsShowing())
187    return selected_page_;
188
189  // If, at the end of the current animation, we will animate to another page,
190  // return that eventual page.
191  if (pending_selected_page_ >= 0)
192    return pending_selected_page_;
193
194  // Just the target of the current animation.
195  return transition_.target_page;
196}
197
198void PaginationModel::NotifySelectedPageChanged(int old_selected,
199                                                int new_selected) {
200  FOR_EACH_OBSERVER(PaginationModelObserver,
201                    observers_,
202                    SelectedPageChanged(old_selected, new_selected));
203}
204
205void PaginationModel::NotifyTransitionStarted() {
206  FOR_EACH_OBSERVER(PaginationModelObserver, observers_, TransitionStarted());
207}
208
209void PaginationModel::NotifyTransitionChanged() {
210  FOR_EACH_OBSERVER(PaginationModelObserver, observers_, TransitionChanged());
211}
212
213int PaginationModel::CalculateTargetPage(int delta) const {
214  DCHECK_GT(total_pages_, 0);
215  const int target_page = SelectedTargetPage() + delta;
216
217  int start_page = 0;
218  int end_page = total_pages_ - 1;
219
220  // Use invalid page when |selected_page_| is at ends.
221  if (target_page < start_page && selected_page_ == start_page)
222    start_page = -1;
223  else if (target_page > end_page && selected_page_ == end_page)
224    end_page = total_pages_;
225
226  return std::max(start_page, std::min(end_page, target_page));
227}
228
229void PaginationModel::StartTransitionAnimation(const Transition& transition) {
230  DCHECK(selected_page_ != transition.target_page);
231
232  NotifyTransitionStarted();
233  SetTransition(transition);
234
235  transition_animation_.reset(new gfx::SlideAnimation(this));
236  transition_animation_->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
237  transition_animation_->Reset(transition_.progress);
238
239  const int duration = is_valid_page(transition_.target_page) ?
240      transition_duration_ms_ : overscroll_transition_duration_ms_;
241  if (duration)
242    transition_animation_->SetSlideDuration(duration);
243
244  transition_animation_->Show();
245}
246
247void PaginationModel::ResetTransitionAnimation() {
248  transition_animation_.reset();
249  transition_.target_page = -1;
250  transition_.progress = 0;
251  pending_selected_page_ = -1;
252}
253
254void PaginationModel::AnimationProgressed(const gfx::Animation* animation) {
255  transition_.progress = transition_animation_->GetCurrentValue();
256  NotifyTransitionChanged();
257}
258
259void PaginationModel::AnimationEnded(const gfx::Animation* animation) {
260  // Save |pending_selected_page_| because SelectPage resets it.
261  int next_target = pending_selected_page_;
262
263  if (transition_animation_->GetCurrentValue() == 1) {
264    // Showing animation ends.
265    if (!is_valid_page(transition_.target_page)) {
266      // If target page is not in valid range, reverse the animation.
267      transition_animation_->Hide();
268      return;
269    }
270
271    // Otherwise, change page and finish the transition.
272    DCHECK(selected_page_ != transition_.target_page);
273    SelectPage(transition_.target_page, false /* animate */);
274  } else if (transition_animation_->GetCurrentValue() == 0) {
275    // Hiding animation ends. No page change should happen.
276    ResetTransitionAnimation();
277  }
278
279  if (next_target >= 0)
280    SelectPage(next_target, true);
281}
282
283}  // namespace app_list
284