-
Notifications
You must be signed in to change notification settings - Fork 6.6k
/
MessageAnimationCoordinator.java
385 lines (355 loc) · 18.2 KB
/
MessageAnimationCoordinator.java
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.messages;
import static org.chromium.components.messages.MessagesMetrics.recordStackingAnimationType;
import static org.chromium.components.messages.MessagesMetrics.recordThreeStackedScenario;
import android.animation.Animator;
import android.animation.AnimatorSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.components.browser_ui.widget.animation.CancelAwareAnimatorListener;
import org.chromium.components.messages.MessageQueueManager.MessageState;
import org.chromium.components.messages.MessageStateHandler.Position;
import org.chromium.components.messages.MessagesMetrics.StackingAnimationAction;
import org.chromium.components.messages.MessagesMetrics.StackingAnimationType;
import org.chromium.components.messages.MessagesMetrics.ThreeStackedScenario;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Coordinator for toggling animation when message is about to show or hide.
*/
public class MessageAnimationCoordinator implements SwipeAnimationHandler {
private static final String TAG = MessageQueueManager.TAG;
// Animation start delay for the back message for MessageBannerMediator.ENTER_DURATION_MS amount
// of time, required to show the front message from Position.INVISIBLE to Position.FRONT.
private static final int BACK_MESSAGE_START_DELAY_MS = 600;
/**
* mCurrentDisplayedMessage refers to the message which is currently visible on the screen
* including situations in which the message is already dismissed and hide animation is running.
*/
@Nullable
private MessageState mCurrentDisplayedMessage;
@NonNull
private List<MessageState> mCurrentDisplayedMessages = Arrays.asList(null, null);
private MessageState mLastShownMessage;
private MessageQueueDelegate mMessageQueueDelegate;
private AnimatorSet mAnimatorSet = new AnimatorSet();
private Animator mFrontAnimator;
private Animator mBackAnimator;
private final MessageContainer mContainer;
private final Callback<Animator> mAnimatorStartCallback;
public MessageAnimationCoordinator(
MessageContainer messageContainer, Callback<Animator> animatorStartCallback) {
mContainer = messageContainer;
mAnimatorStartCallback = animatorStartCallback;
}
public void updateWithoutStacking(
@Nullable MessageState candidate, boolean suspended, Runnable onFinished) {
if (mCurrentDisplayedMessage == candidate) return;
if (!suspended && mAnimatorSet.isStarted()) {
return;
}
if (mCurrentDisplayedMessage == null) {
mCurrentDisplayedMessage = candidate;
mMessageQueueDelegate.onStartShowing(() -> {
if (mCurrentDisplayedMessage == null) {
return;
}
Log.w(TAG,
"MessageStateHandler#shouldShow for message with ID %s and key %s in "
+ "MessageQueueManager#updateCurrentDisplayedMessage "
+ "returned %s.",
candidate.handler.getMessageIdentifier(), candidate.messageKey,
candidate.handler.shouldShow());
final var animator =
mCurrentDisplayedMessage.handler.show(Position.INVISIBLE, Position.FRONT);
// Wait until the message and the container are measured before showing the message.
// This is required in case the animation set-up requires the height of the
// container, e.g. showing messages without the top controls visible.
mContainer.runAfterInitialMessageLayout(() -> {
mAnimatorSet.cancel();
mAnimatorSet.removeAllListeners();
mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(animator);
mAnimatorSet.addListener(new MessageAnimationListener(() -> {
mMessageQueueDelegate.onAnimationEnd();
onFinished.run();
}));
mMessageQueueDelegate.onAnimationStart();
mAnimatorStartCallback.onResult(mAnimatorSet);
});
mLastShownMessage = mCurrentDisplayedMessage;
});
} else {
Runnable runnable = () -> {
mMessageQueueDelegate.onFinishHiding();
mMessageQueueDelegate.onAnimationEnd();
mCurrentDisplayedMessage = mLastShownMessage = null;
onFinished.run();
};
if (mLastShownMessage != mCurrentDisplayedMessage) {
runnable.run();
return;
}
mAnimatorSet.cancel();
mAnimatorSet.removeAllListeners();
Animator animator = mCurrentDisplayedMessage.handler.hide(
Position.FRONT, Position.INVISIBLE, !suspended);
if (animator == null) {
runnable.run();
} else {
mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(animator);
mMessageQueueDelegate.onAnimationStart();
mAnimatorSet.addListener(new MessageAnimationListener(runnable));
mAnimatorStartCallback.onResult(mAnimatorSet);
}
}
}
// TODO(crbug.com/1200974): Compare current shown messages with last shown ones.
/**
* cf: Current front message.
* cb: Current back message.
* nf: Next front message.
* nb: Next back message.
* Null represents no view at that position.
* 1. If candidates and current displayed messages are internally equal, do nothing.
* 2. If cf is null, which implies cb is also null, show candidates.
* 3. If cf is not found in candidates, it must be hidden.
* In the meantime, if current back message is displayed, check if it should be hidden or
* moved to front.
* 4. If only back message is changed:
* Hide current back message if possible; otherwise, show the candidate.
* 5. The current front message must be moved back and a new message is moved to front.
*
* Note: Assume current displayed messages are [m1, m2]; Then the candidates won't be [m3, m2].
* If m3 is a higher priority message, then the candidates should be [m3, m1].
* Otherwise, m1 is usually hidden because of dismissing or inactive scope, the candidates
* should be [m2, null/m3].
*
* [m1, m2] -> [m3, m4] should also be impossible, because message is designed to be dismissed
* one by one. If both are hiding by queue suspending, it should look like:
* [m1, m2] -> [null, null] -> [m3, m4]
*
* @param candidates The candidates supposed to be displayed next. Not all candidates are
* guaranteed to be displayed after update. The content may be changed to
* reflect the actual change in this update.
* @param isSuspended Whether the queue is suspended.
* @param onFinished Runnable triggered after animation is finished.
*/
public void updateWithStacking(
@NonNull List<MessageState> candidates, boolean isSuspended, Runnable onFinished) {
// Wait until the current animation is done, unless we need to hide them immediately.
if (mAnimatorSet.isStarted()) {
if (isSuspended) {
// crbug.com/1405389: Force animation to end in order to trigger callbacks.
mAnimatorSet.end();
onFinished.run();
}
return;
}
var currentFront = mCurrentDisplayedMessages.get(0); // Currently front.
var currentBack = mCurrentDisplayedMessages.get(1); // Currently back.
var nextFront = candidates.get(0); // Next front.
var nextBack = candidates.get(1); // Next back.
// If both animators will be modified, modify FrontAnimator first, because the back message
// relies on the first message in order to adjust its size.
mFrontAnimator = mBackAnimator = null;
boolean animate = !isSuspended;
// If front message is null, then the back one is definitely null.
assert !(nextFront == null && nextBack != null);
assert !(currentFront == null && currentBack != null);
if (currentFront == nextFront && currentBack == nextBack) return;
if (currentFront == null) { // Implies that currently back is also null.
recordAnimationAction(StackingAnimationAction.INSERT_AT_FRONT, nextFront);
mFrontAnimator = nextFront.handler.show(Position.INVISIBLE, Position.FRONT);
if (nextBack != null) {
recordAnimationAction(StackingAnimationAction.INSERT_AT_BACK, nextBack);
recordStackingAnimationType(StackingAnimationType.SHOW_ALL);
mBackAnimator = nextBack.handler.show(Position.FRONT, Position.BACK);
if (mBackAnimator != null) {
mBackAnimator.setStartDelay(BACK_MESSAGE_START_DELAY_MS);
}
} else {
recordStackingAnimationType(StackingAnimationType.SHOW_FRONT_ONLY);
}
} else if (currentFront != nextFront && currentFront != nextBack) {
// Current displayed front message will be hidden.
recordAnimationAction(StackingAnimationAction.REMOVE_FRONT, currentFront);
mFrontAnimator = currentFront.handler.hide(Position.FRONT, Position.INVISIBLE, animate);
if (currentBack != null) {
if (currentBack == nextFront) { // Visible front will be dismissed and back one is
// moved to front.
recordAnimationAction(StackingAnimationAction.PUSH_TO_FRONT, currentBack);
recordStackingAnimationType(StackingAnimationType.REMOVE_FRONT_AND_SHOW_BACK);
mBackAnimator = currentBack.handler.show(Position.BACK, Position.FRONT);
if (nextBack != null) {
recordThreeStackedScenario(ThreeStackedScenario.IN_SEQUENCE);
}
// Show nb in the next round.
nextBack = null;
candidates.set(1, null);
} else { // Both visible front and back messages will be replaced.
recordAnimationAction(StackingAnimationAction.REMOVE_BACK, currentBack);
recordStackingAnimationType(StackingAnimationType.REMOVE_ALL);
mBackAnimator =
currentBack.handler.hide(Position.BACK, Position.FRONT, animate);
// Hide current displayed two messages and then show other messages
// in the next round.
nextFront = nextBack = null;
candidates.set(0, null);
candidates.set(1, null);
}
} else {
// TODO(crbug.com/1382275): simplify this into one step.
// Split the transition: [m1, null] -> [m2, null] into two steps:
// [m1, null] -> [null, null] -> [m2, null]
nextFront = nextBack = null;
candidates.set(0, null);
candidates.set(1, null);
recordStackingAnimationType(StackingAnimationType.REMOVE_FRONT_ONLY);
}
} else if (currentFront == nextFront) {
if (currentBack != null) { // Hide the current back one.
recordAnimationAction(StackingAnimationAction.REMOVE_BACK, currentBack);
recordStackingAnimationType(StackingAnimationType.REMOVE_BACK_ONLY);
mBackAnimator = currentBack.handler.hide(Position.BACK, Position.FRONT, animate);
} else {
recordAnimationAction(StackingAnimationAction.INSERT_AT_BACK, nextBack);
recordStackingAnimationType(StackingAnimationType.SHOW_BACK_ONLY);
// If nb is null, it means candidates and current displayed messages are equal.
assert nextBack != null;
mBackAnimator = nextBack.handler.show(Position.FRONT, Position.BACK);
}
} else {
assert currentFront == nextBack;
if (currentBack != null) {
recordAnimationAction(StackingAnimationAction.REMOVE_BACK, currentBack);
recordStackingAnimationType(StackingAnimationType.REMOVE_BACK_ONLY);
recordThreeStackedScenario(ThreeStackedScenario.HIGH_PRIORITY);
mBackAnimator = currentBack.handler.hide(Position.BACK, Position.FRONT, animate);
// [m1, m2] -> [m1, null] -> [m3, m1]
// In this case, we complete this in 2 steps to avoid manipulating 3 handlers
// at any single moment.
candidates.set(0, currentFront);
candidates.set(1, null);
} else { // Moved the current front to back and show a new front view.
recordAnimationAction(StackingAnimationAction.PUSH_TO_BACK, currentFront);
recordAnimationAction(StackingAnimationAction.INSERT_AT_FRONT, nextFront);
recordStackingAnimationType(StackingAnimationType.INSERT_AT_FRONT);
mFrontAnimator = nextFront.handler.show(Position.INVISIBLE, Position.FRONT);
mBackAnimator = currentFront.handler.show(Position.FRONT, Position.BACK);
}
}
if (currentFront == null) {
// No message is being displayed now: trigger #onStartShowing.
mCurrentDisplayedMessages = new ArrayList<>(candidates);
// Use ref because when startShowing is finished, other animation might have been
// triggered such that those two member variables have been mutated.
var frontAnimator = mFrontAnimator;
var backAnimator = mBackAnimator;
mMessageQueueDelegate.onStartShowing(() -> {
if (candidates.get(0) == mCurrentDisplayedMessages.get(0)
&& candidates.get(1) == mCurrentDisplayedMessages.get(1)) {
triggerStackingAnimation(candidates, onFinished, frontAnimator, backAnimator);
}
});
} else if (nextFront == null) {
// All messages will be hidden: trigger #onFinishHiding.
Runnable runnable = () -> {
mMessageQueueDelegate.onFinishHiding();
mCurrentDisplayedMessages = new ArrayList<>(candidates);
onFinished.run();
};
triggerStackingAnimation(candidates, runnable, mFrontAnimator, mBackAnimator);
} else {
mCurrentDisplayedMessages = new ArrayList<>(candidates);
triggerStackingAnimation(candidates, onFinished, mFrontAnimator, mBackAnimator);
}
}
private void triggerStackingAnimation(List<MessageState> candidates, Runnable onFinished,
Animator frontAnimator, Animator backAnimator) {
Runnable runnable = () -> {
// While the runnable is waiting to be triggered, hiding animation might be triggered:
// while the hiding animation is running, declare this runnable as obsolete so that
// it won't cancel the hiding animation.
if (isAnimatorExpired(frontAnimator, backAnimator)) {
return;
}
mAnimatorSet.cancel();
mAnimatorSet.removeAllListeners();
mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(frontAnimator);
mAnimatorSet.play(backAnimator);
mAnimatorSet.addListener(new MessageAnimationListener(() -> {
mMessageQueueDelegate.onAnimationEnd();
onFinished.run();
}));
mMessageQueueDelegate.onAnimationStart();
mAnimatorStartCallback.onResult(mAnimatorSet);
};
if (candidates.get(0) == null) {
runnable.run();
} else {
mContainer.runAfterInitialMessageLayout(runnable);
}
}
private boolean isAnimatorExpired(Animator frontAnimator, Animator backAnimator) {
return mFrontAnimator != frontAnimator || mBackAnimator != backAnimator;
}
@Override
public void onSwipeStart() {
// Message shouldn't consume swipe for now because animation is running, e.g.:
// the front message should not be swiped when back message is running showing animation.
assert isSwipeEnabled();
mMessageQueueDelegate.onAnimationStart();
}
@Override
public boolean isSwipeEnabled() {
return !mAnimatorSet.isStarted();
}
@Override
public void onSwipeEnd(@Nullable Animator animator) {
if (animator == null) {
mMessageQueueDelegate.onAnimationEnd();
return;
}
animator.addListener(new MessageAnimationListener(mMessageQueueDelegate::onAnimationEnd));
mAnimatorStartCallback.onResult(animator);
}
void setMessageQueueDelegate(MessageQueueDelegate delegate) {
mMessageQueueDelegate = delegate;
}
@Nullable
MessageState getCurrentDisplayedMessage() {
return mCurrentDisplayedMessage;
}
// Return a list of two messages which should be displayed when stacking animation is enabled.
@NonNull
List<MessageState> getCurrentDisplayedMessages() {
return mCurrentDisplayedMessages;
}
private void recordAnimationAction(
@StackingAnimationAction int action, @NonNull MessageState messageState) {
MessagesMetrics.recordStackingAnimationAction(
action, messageState.handler.getMessageIdentifier());
}
class MessageAnimationListener extends CancelAwareAnimatorListener {
private final Runnable mOnFinished;
public MessageAnimationListener(Runnable onFinished) {
mOnFinished = onFinished;
}
@Override
public void onEnd(Animator animator) {
super.onEnd(animator);
mOnFinished.run();
}
}
AnimatorSet getAnimatorSetForTesting() {
return mAnimatorSet;
}
}