1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.TimeInterpolator;
22import android.animation.ValueAnimator;
23import android.annotation.NonNull;
24import android.view.View;
25import android.view.ViewPropertyAnimator;
26
27import com.android.internal.widget.RecyclerView.ViewHolder;
28
29import java.util.ArrayList;
30import java.util.List;
31
32/**
33 * This implementation of {@link RecyclerView.ItemAnimator} provides basic
34 * animations on remove, add, and move events that happen to the items in
35 * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
36 *
37 * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
38 */
39public class DefaultItemAnimator extends SimpleItemAnimator {
40    private static final boolean DEBUG = false;
41
42    private static TimeInterpolator sDefaultInterpolator;
43
44    private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
45    private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
46    private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
47    private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
48
49    ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
50    ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
51    ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
52
53    ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
54    ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
55    ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
56    ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
57
58    private static class MoveInfo {
59        public ViewHolder holder;
60        public int fromX, fromY, toX, toY;
61
62        MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
63            this.holder = holder;
64            this.fromX = fromX;
65            this.fromY = fromY;
66            this.toX = toX;
67            this.toY = toY;
68        }
69    }
70
71    private static class ChangeInfo {
72        public ViewHolder oldHolder, newHolder;
73        public int fromX, fromY, toX, toY;
74        private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
75            this.oldHolder = oldHolder;
76            this.newHolder = newHolder;
77        }
78
79        ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
80                int fromX, int fromY, int toX, int toY) {
81            this(oldHolder, newHolder);
82            this.fromX = fromX;
83            this.fromY = fromY;
84            this.toX = toX;
85            this.toY = toY;
86        }
87
88        @Override
89        public String toString() {
90            return "ChangeInfo{"
91                    + "oldHolder=" + oldHolder
92                    + ", newHolder=" + newHolder
93                    + ", fromX=" + fromX
94                    + ", fromY=" + fromY
95                    + ", toX=" + toX
96                    + ", toY=" + toY
97                    + '}';
98        }
99    }
100
101    @Override
102    public void runPendingAnimations() {
103        boolean removalsPending = !mPendingRemovals.isEmpty();
104        boolean movesPending = !mPendingMoves.isEmpty();
105        boolean changesPending = !mPendingChanges.isEmpty();
106        boolean additionsPending = !mPendingAdditions.isEmpty();
107        if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
108            // nothing to animate
109            return;
110        }
111        // First, remove stuff
112        for (ViewHolder holder : mPendingRemovals) {
113            animateRemoveImpl(holder);
114        }
115        mPendingRemovals.clear();
116        // Next, move stuff
117        if (movesPending) {
118            final ArrayList<MoveInfo> moves = new ArrayList<>();
119            moves.addAll(mPendingMoves);
120            mMovesList.add(moves);
121            mPendingMoves.clear();
122            Runnable mover = new Runnable() {
123                @Override
124                public void run() {
125                    for (MoveInfo moveInfo : moves) {
126                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
127                                moveInfo.toX, moveInfo.toY);
128                    }
129                    moves.clear();
130                    mMovesList.remove(moves);
131                }
132            };
133            if (removalsPending) {
134                View view = moves.get(0).holder.itemView;
135                view.postOnAnimationDelayed(mover, getRemoveDuration());
136            } else {
137                mover.run();
138            }
139        }
140        // Next, change stuff, to run in parallel with move animations
141        if (changesPending) {
142            final ArrayList<ChangeInfo> changes = new ArrayList<>();
143            changes.addAll(mPendingChanges);
144            mChangesList.add(changes);
145            mPendingChanges.clear();
146            Runnable changer = new Runnable() {
147                @Override
148                public void run() {
149                    for (ChangeInfo change : changes) {
150                        animateChangeImpl(change);
151                    }
152                    changes.clear();
153                    mChangesList.remove(changes);
154                }
155            };
156            if (removalsPending) {
157                ViewHolder holder = changes.get(0).oldHolder;
158                holder.itemView.postOnAnimationDelayed(changer, getRemoveDuration());
159            } else {
160                changer.run();
161            }
162        }
163        // Next, add stuff
164        if (additionsPending) {
165            final ArrayList<ViewHolder> additions = new ArrayList<>();
166            additions.addAll(mPendingAdditions);
167            mAdditionsList.add(additions);
168            mPendingAdditions.clear();
169            Runnable adder = new Runnable() {
170                @Override
171                public void run() {
172                    for (ViewHolder holder : additions) {
173                        animateAddImpl(holder);
174                    }
175                    additions.clear();
176                    mAdditionsList.remove(additions);
177                }
178            };
179            if (removalsPending || movesPending || changesPending) {
180                long removeDuration = removalsPending ? getRemoveDuration() : 0;
181                long moveDuration = movesPending ? getMoveDuration() : 0;
182                long changeDuration = changesPending ? getChangeDuration() : 0;
183                long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
184                View view = additions.get(0).itemView;
185                view.postOnAnimationDelayed(adder, totalDelay);
186            } else {
187                adder.run();
188            }
189        }
190    }
191
192    @Override
193    public boolean animateRemove(final ViewHolder holder) {
194        resetAnimation(holder);
195        mPendingRemovals.add(holder);
196        return true;
197    }
198
199    private void animateRemoveImpl(final ViewHolder holder) {
200        final View view = holder.itemView;
201        final ViewPropertyAnimator animation = view.animate();
202        mRemoveAnimations.add(holder);
203        animation.setDuration(getRemoveDuration()).alpha(0).setListener(
204                new AnimatorListenerAdapter() {
205                    @Override
206                    public void onAnimationStart(Animator animator) {
207                        dispatchRemoveStarting(holder);
208                    }
209
210                    @Override
211                    public void onAnimationEnd(Animator animator) {
212                        animation.setListener(null);
213                        view.setAlpha(1);
214                        dispatchRemoveFinished(holder);
215                        mRemoveAnimations.remove(holder);
216                        dispatchFinishedWhenDone();
217                    }
218                }).start();
219    }
220
221    @Override
222    public boolean animateAdd(final ViewHolder holder) {
223        resetAnimation(holder);
224        holder.itemView.setAlpha(0);
225        mPendingAdditions.add(holder);
226        return true;
227    }
228
229    void animateAddImpl(final ViewHolder holder) {
230        final View view = holder.itemView;
231        final ViewPropertyAnimator animation = view.animate();
232        mAddAnimations.add(holder);
233        animation.alpha(1).setDuration(getAddDuration())
234                .setListener(new AnimatorListenerAdapter() {
235                    @Override
236                    public void onAnimationStart(Animator animator) {
237                        dispatchAddStarting(holder);
238                    }
239
240                    @Override
241                    public void onAnimationCancel(Animator animator) {
242                        view.setAlpha(1);
243                    }
244
245                    @Override
246                    public void onAnimationEnd(Animator animator) {
247                        animation.setListener(null);
248                        dispatchAddFinished(holder);
249                        mAddAnimations.remove(holder);
250                        dispatchFinishedWhenDone();
251                    }
252                }).start();
253    }
254
255    @Override
256    public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
257            int toX, int toY) {
258        final View view = holder.itemView;
259        fromX += holder.itemView.getTranslationX();
260        fromY += holder.itemView.getTranslationY();
261        resetAnimation(holder);
262        int deltaX = toX - fromX;
263        int deltaY = toY - fromY;
264        if (deltaX == 0 && deltaY == 0) {
265            dispatchMoveFinished(holder);
266            return false;
267        }
268        if (deltaX != 0) {
269            view.setTranslationX(-deltaX);
270        }
271        if (deltaY != 0) {
272            view.setTranslationY(-deltaY);
273        }
274        mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
275        return true;
276    }
277
278    void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
279        final View view = holder.itemView;
280        final int deltaX = toX - fromX;
281        final int deltaY = toY - fromY;
282        if (deltaX != 0) {
283            view.animate().translationX(0);
284        }
285        if (deltaY != 0) {
286            view.animate().translationY(0);
287        }
288        // TODO: make EndActions end listeners instead, since end actions aren't called when
289        // vpas are canceled (and can't end them. why?)
290        // need listener functionality in VPACompat for this. Ick.
291        final ViewPropertyAnimator animation = view.animate();
292        mMoveAnimations.add(holder);
293        animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
294            @Override
295            public void onAnimationStart(Animator animator) {
296                dispatchMoveStarting(holder);
297            }
298
299            @Override
300            public void onAnimationCancel(Animator animator) {
301                if (deltaX != 0) {
302                    view.setTranslationX(0);
303                }
304                if (deltaY != 0) {
305                    view.setTranslationY(0);
306                }
307            }
308
309            @Override
310            public void onAnimationEnd(Animator animator) {
311                animation.setListener(null);
312                dispatchMoveFinished(holder);
313                mMoveAnimations.remove(holder);
314                dispatchFinishedWhenDone();
315            }
316        }).start();
317    }
318
319    @Override
320    public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
321            int fromX, int fromY, int toX, int toY) {
322        if (oldHolder == newHolder) {
323            // Don't know how to run change animations when the same view holder is re-used.
324            // run a move animation to handle position changes.
325            return animateMove(oldHolder, fromX, fromY, toX, toY);
326        }
327        final float prevTranslationX = oldHolder.itemView.getTranslationX();
328        final float prevTranslationY = oldHolder.itemView.getTranslationY();
329        final float prevAlpha = oldHolder.itemView.getAlpha();
330        resetAnimation(oldHolder);
331        int deltaX = (int) (toX - fromX - prevTranslationX);
332        int deltaY = (int) (toY - fromY - prevTranslationY);
333        // recover prev translation state after ending animation
334        oldHolder.itemView.setTranslationX(prevTranslationX);
335        oldHolder.itemView.setTranslationY(prevTranslationY);
336        oldHolder.itemView.setAlpha(prevAlpha);
337        if (newHolder != null) {
338            // carry over translation values
339            resetAnimation(newHolder);
340            newHolder.itemView.setTranslationX(-deltaX);
341            newHolder.itemView.setTranslationY(-deltaY);
342            newHolder.itemView.setAlpha(0);
343        }
344        mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
345        return true;
346    }
347
348    void animateChangeImpl(final ChangeInfo changeInfo) {
349        final ViewHolder holder = changeInfo.oldHolder;
350        final View view = holder == null ? null : holder.itemView;
351        final ViewHolder newHolder = changeInfo.newHolder;
352        final View newView = newHolder != null ? newHolder.itemView : null;
353        if (view != null) {
354            final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
355                    getChangeDuration());
356            mChangeAnimations.add(changeInfo.oldHolder);
357            oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
358            oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
359            oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
360                @Override
361                public void onAnimationStart(Animator animator) {
362                    dispatchChangeStarting(changeInfo.oldHolder, true);
363                }
364
365                @Override
366                public void onAnimationEnd(Animator animator) {
367                    oldViewAnim.setListener(null);
368                    view.setAlpha(1);
369                    view.setTranslationX(0);
370                    view.setTranslationY(0);
371                    dispatchChangeFinished(changeInfo.oldHolder, true);
372                    mChangeAnimations.remove(changeInfo.oldHolder);
373                    dispatchFinishedWhenDone();
374                }
375            }).start();
376        }
377        if (newView != null) {
378            final ViewPropertyAnimator newViewAnimation = newView.animate();
379            mChangeAnimations.add(changeInfo.newHolder);
380            newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
381                    .alpha(1).setListener(new AnimatorListenerAdapter() {
382                        @Override
383                        public void onAnimationStart(Animator animator) {
384                            dispatchChangeStarting(changeInfo.newHolder, false);
385                        }
386                        @Override
387                        public void onAnimationEnd(Animator animator) {
388                            newViewAnimation.setListener(null);
389                            newView.setAlpha(1);
390                            newView.setTranslationX(0);
391                            newView.setTranslationY(0);
392                            dispatchChangeFinished(changeInfo.newHolder, false);
393                            mChangeAnimations.remove(changeInfo.newHolder);
394                            dispatchFinishedWhenDone();
395                        }
396                    }).start();
397        }
398    }
399
400    private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
401        for (int i = infoList.size() - 1; i >= 0; i--) {
402            ChangeInfo changeInfo = infoList.get(i);
403            if (endChangeAnimationIfNecessary(changeInfo, item)) {
404                if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
405                    infoList.remove(changeInfo);
406                }
407            }
408        }
409    }
410
411    private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
412        if (changeInfo.oldHolder != null) {
413            endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
414        }
415        if (changeInfo.newHolder != null) {
416            endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
417        }
418    }
419    private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
420        boolean oldItem = false;
421        if (changeInfo.newHolder == item) {
422            changeInfo.newHolder = null;
423        } else if (changeInfo.oldHolder == item) {
424            changeInfo.oldHolder = null;
425            oldItem = true;
426        } else {
427            return false;
428        }
429        item.itemView.setAlpha(1);
430        item.itemView.setTranslationX(0);
431        item.itemView.setTranslationY(0);
432        dispatchChangeFinished(item, oldItem);
433        return true;
434    }
435
436    @Override
437    public void endAnimation(ViewHolder item) {
438        final View view = item.itemView;
439        // this will trigger end callback which should set properties to their target values.
440        view.animate().cancel();
441        // TODO if some other animations are chained to end, how do we cancel them as well?
442        for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
443            MoveInfo moveInfo = mPendingMoves.get(i);
444            if (moveInfo.holder == item) {
445                view.setTranslationY(0);
446                view.setTranslationX(0);
447                dispatchMoveFinished(item);
448                mPendingMoves.remove(i);
449            }
450        }
451        endChangeAnimation(mPendingChanges, item);
452        if (mPendingRemovals.remove(item)) {
453            view.setAlpha(1);
454            dispatchRemoveFinished(item);
455        }
456        if (mPendingAdditions.remove(item)) {
457            view.setAlpha(1);
458            dispatchAddFinished(item);
459        }
460
461        for (int i = mChangesList.size() - 1; i >= 0; i--) {
462            ArrayList<ChangeInfo> changes = mChangesList.get(i);
463            endChangeAnimation(changes, item);
464            if (changes.isEmpty()) {
465                mChangesList.remove(i);
466            }
467        }
468        for (int i = mMovesList.size() - 1; i >= 0; i--) {
469            ArrayList<MoveInfo> moves = mMovesList.get(i);
470            for (int j = moves.size() - 1; j >= 0; j--) {
471                MoveInfo moveInfo = moves.get(j);
472                if (moveInfo.holder == item) {
473                    view.setTranslationY(0);
474                    view.setTranslationX(0);
475                    dispatchMoveFinished(item);
476                    moves.remove(j);
477                    if (moves.isEmpty()) {
478                        mMovesList.remove(i);
479                    }
480                    break;
481                }
482            }
483        }
484        for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
485            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
486            if (additions.remove(item)) {
487                view.setAlpha(1);
488                dispatchAddFinished(item);
489                if (additions.isEmpty()) {
490                    mAdditionsList.remove(i);
491                }
492            }
493        }
494
495        // animations should be ended by the cancel above.
496        //noinspection PointlessBooleanExpression,ConstantConditions
497        if (mRemoveAnimations.remove(item) && DEBUG) {
498            throw new IllegalStateException("after animation is cancelled, item should not be in "
499                    + "mRemoveAnimations list");
500        }
501
502        //noinspection PointlessBooleanExpression,ConstantConditions
503        if (mAddAnimations.remove(item) && DEBUG) {
504            throw new IllegalStateException("after animation is cancelled, item should not be in "
505                    + "mAddAnimations list");
506        }
507
508        //noinspection PointlessBooleanExpression,ConstantConditions
509        if (mChangeAnimations.remove(item) && DEBUG) {
510            throw new IllegalStateException("after animation is cancelled, item should not be in "
511                    + "mChangeAnimations list");
512        }
513
514        //noinspection PointlessBooleanExpression,ConstantConditions
515        if (mMoveAnimations.remove(item) && DEBUG) {
516            throw new IllegalStateException("after animation is cancelled, item should not be in "
517                    + "mMoveAnimations list");
518        }
519        dispatchFinishedWhenDone();
520    }
521
522    private void resetAnimation(ViewHolder holder) {
523        if (sDefaultInterpolator == null) {
524            sDefaultInterpolator = new ValueAnimator().getInterpolator();
525        }
526        holder.itemView.animate().setInterpolator(sDefaultInterpolator);
527        endAnimation(holder);
528    }
529
530    @Override
531    public boolean isRunning() {
532        return (!mPendingAdditions.isEmpty()
533                || !mPendingChanges.isEmpty()
534                || !mPendingMoves.isEmpty()
535                || !mPendingRemovals.isEmpty()
536                || !mMoveAnimations.isEmpty()
537                || !mRemoveAnimations.isEmpty()
538                || !mAddAnimations.isEmpty()
539                || !mChangeAnimations.isEmpty()
540                || !mMovesList.isEmpty()
541                || !mAdditionsList.isEmpty()
542                || !mChangesList.isEmpty());
543    }
544
545    /**
546     * Check the state of currently pending and running animations. If there are none
547     * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
548     * listeners.
549     */
550    void dispatchFinishedWhenDone() {
551        if (!isRunning()) {
552            dispatchAnimationsFinished();
553        }
554    }
555
556    @Override
557    public void endAnimations() {
558        int count = mPendingMoves.size();
559        for (int i = count - 1; i >= 0; i--) {
560            MoveInfo item = mPendingMoves.get(i);
561            View view = item.holder.itemView;
562            view.setTranslationY(0);
563            view.setTranslationX(0);
564            dispatchMoveFinished(item.holder);
565            mPendingMoves.remove(i);
566        }
567        count = mPendingRemovals.size();
568        for (int i = count - 1; i >= 0; i--) {
569            ViewHolder item = mPendingRemovals.get(i);
570            dispatchRemoveFinished(item);
571            mPendingRemovals.remove(i);
572        }
573        count = mPendingAdditions.size();
574        for (int i = count - 1; i >= 0; i--) {
575            ViewHolder item = mPendingAdditions.get(i);
576            item.itemView.setAlpha(1);
577            dispatchAddFinished(item);
578            mPendingAdditions.remove(i);
579        }
580        count = mPendingChanges.size();
581        for (int i = count - 1; i >= 0; i--) {
582            endChangeAnimationIfNecessary(mPendingChanges.get(i));
583        }
584        mPendingChanges.clear();
585        if (!isRunning()) {
586            return;
587        }
588
589        int listCount = mMovesList.size();
590        for (int i = listCount - 1; i >= 0; i--) {
591            ArrayList<MoveInfo> moves = mMovesList.get(i);
592            count = moves.size();
593            for (int j = count - 1; j >= 0; j--) {
594                MoveInfo moveInfo = moves.get(j);
595                ViewHolder item = moveInfo.holder;
596                View view = item.itemView;
597                view.setTranslationY(0);
598                view.setTranslationX(0);
599                dispatchMoveFinished(moveInfo.holder);
600                moves.remove(j);
601                if (moves.isEmpty()) {
602                    mMovesList.remove(moves);
603                }
604            }
605        }
606        listCount = mAdditionsList.size();
607        for (int i = listCount - 1; i >= 0; i--) {
608            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
609            count = additions.size();
610            for (int j = count - 1; j >= 0; j--) {
611                ViewHolder item = additions.get(j);
612                View view = item.itemView;
613                view.setAlpha(1);
614                dispatchAddFinished(item);
615                additions.remove(j);
616                if (additions.isEmpty()) {
617                    mAdditionsList.remove(additions);
618                }
619            }
620        }
621        listCount = mChangesList.size();
622        for (int i = listCount - 1; i >= 0; i--) {
623            ArrayList<ChangeInfo> changes = mChangesList.get(i);
624            count = changes.size();
625            for (int j = count - 1; j >= 0; j--) {
626                endChangeAnimationIfNecessary(changes.get(j));
627                if (changes.isEmpty()) {
628                    mChangesList.remove(changes);
629                }
630            }
631        }
632
633        cancelAll(mRemoveAnimations);
634        cancelAll(mMoveAnimations);
635        cancelAll(mAddAnimations);
636        cancelAll(mChangeAnimations);
637
638        dispatchAnimationsFinished();
639    }
640
641    void cancelAll(List<ViewHolder> viewHolders) {
642        for (int i = viewHolders.size() - 1; i >= 0; i--) {
643            viewHolders.get(i).itemView.animate().cancel();
644        }
645    }
646
647    /**
648     * {@inheritDoc}
649     * <p>
650     * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
651     * When this is the case:
652     * <ul>
653     * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both
654     * ViewHolder arguments will be the same instance.
655     * </li>
656     * <li>
657     * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
658     * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
659     * run a move animation instead.
660     * </li>
661     * </ul>
662     */
663    @Override
664    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
665            @NonNull List<Object> payloads) {
666        return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
667    }
668}
669