1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
|
// Copyright (c) 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gfx/paint_throbber.h"
#include <algorithm>
#include "base/numerics/safe_conversions.h"
#include "base/time/time.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/skia_util.h"
namespace gfx {
namespace {
// The maximum size of the "spinning" state arc, in degrees.
constexpr int64_t kMaxArcSize = 270;
// The amount of time it takes to grow the "spinning" arc from 0 to 270 degrees.
constexpr auto kArcTime = base::TimeDelta::FromSecondsD(2.0 / 3.0);
// The amount of time it takes for the "spinning" throbber to make a full
// rotation.
constexpr auto kRotationTime = base::TimeDelta::FromMilliseconds(1568);
void PaintArc(Canvas* canvas,
const Rect& bounds,
SkColor color,
SkScalar start_angle,
SkScalar sweep,
base::Optional<SkScalar> stroke_width) {
if (!stroke_width) {
// Stroke width depends on size.
// . For size < 28: 3 - (28 - size) / 16
// . For 28 <= size: (8 + size) / 12
stroke_width = bounds.width() < 28
? 3.0 - SkIntToScalar(28 - bounds.width()) / 16.0
: SkIntToScalar(bounds.width() + 8) / 12.0;
}
Rect oval = bounds;
// Inset by half the stroke width to make sure the whole arc is inside
// the visible rect.
const int inset = SkScalarCeilToInt(*stroke_width / 2.0);
oval.Inset(inset, inset);
SkPath path;
path.arcTo(RectToSkRect(oval), start_angle, sweep, true);
cc::PaintFlags flags;
flags.setColor(color);
flags.setStrokeCap(cc::PaintFlags::kRound_Cap);
flags.setStrokeWidth(*stroke_width);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setAntiAlias(true);
canvas->DrawPath(path, flags);
}
void CalculateWaitingAngles(const base::TimeDelta& elapsed_time,
int64_t* start_angle,
int64_t* sweep) {
// Calculate start and end points. The angles are counter-clockwise because
// the throbber spins counter-clockwise. The finish angle starts at 12 o'clock
// (90 degrees) and rotates steadily. The start angle trails 180 degrees
// behind, except for the first half revolution, when it stays at 12 o'clock.
constexpr auto kRevolutionTime = base::TimeDelta::FromMilliseconds(1320);
int64_t twelve_oclock = 90;
int64_t finish_angle_cc =
twelve_oclock +
base::ClampRound<int64_t>(elapsed_time / kRevolutionTime * 360);
int64_t start_angle_cc = std::max(finish_angle_cc - 180, twelve_oclock);
// Negate the angles to convert to the clockwise numbers Skia expects.
if (start_angle)
*start_angle = -finish_angle_cc;
if (sweep)
*sweep = finish_angle_cc - start_angle_cc;
}
// This is a Skia port of the MD spinner SVG. The |start_angle| rotation
// here corresponds to the 'rotate' animation.
void PaintThrobberSpinningWithStartAngle(
Canvas* canvas,
const Rect& bounds,
SkColor color,
const base::TimeDelta& elapsed_time,
int64_t start_angle,
base::Optional<SkScalar> stroke_width) {
// The sweep angle ranges from -270 to 270 over 1333ms. CSS
// animation timing functions apply in between key frames, so we have to
// break up the 1333ms into two keyframes (-270 to 0, then 0 to 270).
const double elapsed_ratio = elapsed_time / kArcTime;
const int64_t sweep_frame = base::ClampFloor<int64_t>(elapsed_ratio);
const double arc_progress = elapsed_ratio - sweep_frame;
// This tween is equivalent to cubic-bezier(0.4, 0.0, 0.2, 1).
double sweep = kMaxArcSize *
Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN, arc_progress);
if (sweep_frame % 2 == 0)
sweep -= kMaxArcSize;
// This part makes sure the sweep is at least 5 degrees long. Roughly
// equivalent to the "magic constants" in SVG's fillunfill animation.
constexpr double kMinSweepLength = 5.0;
if (sweep >= 0.0 && sweep < kMinSweepLength) {
start_angle -= (kMinSweepLength - sweep);
sweep = kMinSweepLength;
} else if (sweep <= 0.0 && sweep > -kMinSweepLength) {
start_angle += (-kMinSweepLength - sweep);
sweep = -kMinSweepLength;
}
// To keep the sweep smooth, we have an additional rotation after each
// arc period has elapsed. See SVG's 'rot' animation.
const int64_t rot_keyframe = (sweep_frame / 2) % 4;
PaintArc(canvas, bounds, color, start_angle + rot_keyframe * kMaxArcSize,
sweep, stroke_width);
}
} // namespace
void PaintThrobberSpinning(Canvas* canvas,
const Rect& bounds,
SkColor color,
const base::TimeDelta& elapsed_time,
base::Optional<SkScalar> stroke_width) {
const int64_t start_angle =
270 + base::ClampRound<int64_t>(elapsed_time / kRotationTime * 360);
PaintThrobberSpinningWithStartAngle(canvas, bounds, color, elapsed_time,
start_angle, stroke_width);
}
void PaintThrobberWaiting(Canvas* canvas,
const Rect& bounds,
SkColor color,
const base::TimeDelta& elapsed_time,
base::Optional<SkScalar> stroke_width) {
int64_t start_angle = 0, sweep = 0;
CalculateWaitingAngles(elapsed_time, &start_angle, &sweep);
PaintArc(canvas, bounds, color, start_angle, sweep, stroke_width);
}
void PaintThrobberSpinningAfterWaiting(Canvas* canvas,
const Rect& bounds,
SkColor color,
const base::TimeDelta& elapsed_time,
ThrobberWaitingState* waiting_state,
base::Optional<SkScalar> stroke_width) {
int64_t waiting_start_angle = 0, waiting_sweep = 0;
CalculateWaitingAngles(waiting_state->elapsed_time, &waiting_start_angle,
&waiting_sweep);
// |arc_time_offset| is the effective amount of time one would have to wait
// for the "spinning" sweep to match |waiting_sweep|. Brute force calculation.
if (waiting_state->arc_time_offset.is_zero()) {
for (int64_t arc_ms = 0; arc_ms <= kArcTime.InMillisecondsRoundedUp();
++arc_ms) {
const base::TimeDelta arc_time =
std::min(base::TimeDelta::FromMilliseconds(arc_ms), kArcTime);
if (kMaxArcSize * Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN,
arc_time / kArcTime) >=
waiting_sweep) {
// Add kArcTime to sidestep the |sweep_keyframe == 0| offset below.
waiting_state->arc_time_offset = kArcTime + arc_time;
break;
}
}
}
// Blend the color between "waiting" and "spinning" states.
constexpr auto kColorFadeTime = base::TimeDelta::FromMilliseconds(900);
const float color_progress = (float)Tween::CalculateValue(
Tween::LINEAR_OUT_SLOW_IN, std::min(elapsed_time / kColorFadeTime, 1.0));
const SkColor blend_color =
color_utils::AlphaBlend(color, waiting_state->color, color_progress);
const int64_t start_angle =
waiting_start_angle +
base::ClampRound<int64_t>(elapsed_time / kRotationTime * 360);
const base::TimeDelta effective_elapsed_time =
elapsed_time + waiting_state->arc_time_offset;
PaintThrobberSpinningWithStartAngle(canvas, bounds, blend_color,
effective_elapsed_time, start_angle,
stroke_width);
}
GFX_EXPORT void PaintNewThrobberWaiting(Canvas* canvas,
const RectF& throbber_container_bounds,
SkColor color,
const base::TimeDelta& elapsed_time) {
// Cycle time for the waiting throbber.
constexpr auto kNewThrobberWaitingCycleTime = base::TimeDelta::FromSeconds(1);
// The throbber bounces back and forth. We map the elapsed time to 0->2. Time
// 0->1 represents when the throbber moves left to right, time 1->2 represents
// right to left.
float time = 2.0f * (elapsed_time % kNewThrobberWaitingCycleTime) /
kNewThrobberWaitingCycleTime;
// 1 -> 2 values mirror back to 1 -> 0 values to represent right-to-left.
const bool going_back = time > 1.0f;
if (going_back)
time = 2.0f - time;
// This animation should be fast in the middle and slow at the edges.
time = Tween::CalculateValue(Tween::EASE_IN_OUT, time);
const float min_width = throbber_container_bounds.height();
// The throbber animation stretches longer when moving in (left to right) than
// when going back.
const float throbber_width =
(going_back ? 0.75f : 1.0f) * throbber_container_bounds.width();
// These bounds keep at least |min_width| of the throbber visible (inside the
// throbber bounds).
const float min_x =
throbber_container_bounds.x() - throbber_width + min_width;
const float max_x = throbber_container_bounds.right() - min_width;
RectF bounds = throbber_container_bounds;
// Linear interpolation between |min_x| and |max_x|.
bounds.set_x(time * (max_x - min_x) + min_x);
bounds.set_width(throbber_width);
// The throbber is designed to go out of bounds, but it should not be rendered
// outside |throbber_container_bounds|. This clips the throbber to the edges,
// which gives a smooth bouncing effect.
bounds.Intersect(throbber_container_bounds);
cc::PaintFlags flags;
flags.setColor(color);
flags.setStyle(cc::PaintFlags::kFill_Style);
// Draw with circular end caps.
canvas->DrawRoundRect(bounds, bounds.height() / 2, flags);
}
} // namespace gfx
|