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