RowsFragment.java revision b10ba3b01290ce801180a3d5dc992825af8cb3ab
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import android.animation.TimeAnimator;
17import android.animation.TimeAnimator.TimeListener;
18import android.os.Bundle;
19import android.support.v17.leanback.R;
20import android.support.v17.leanback.widget.HorizontalGridView;
21import android.support.v17.leanback.widget.ItemBridgeAdapter;
22import android.support.v17.leanback.widget.ListRowPresenter;
23import android.support.v17.leanback.widget.ObjectAdapter;
24import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
25import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
26import android.support.v17.leanback.widget.OnItemViewClickedListener;
27import android.support.v17.leanback.widget.OnItemViewSelectedListener;
28import android.support.v17.leanback.widget.Presenter;
29import android.support.v17.leanback.widget.PresenterSelector;
30import android.support.v17.leanback.widget.RowPresenter;
31import android.support.v17.leanback.widget.VerticalGridView;
32import android.support.v17.leanback.widget.ViewHolderTask;
33import android.support.v7.widget.RecyclerView;
34import android.util.Log;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.animation.DecelerateInterpolator;
39import android.view.animation.Interpolator;
40
41import java.util.ArrayList;
42
43/**
44 * An ordered set of rows of leanback widgets.
45 * <p>
46 * A RowsFragment renders the elements of its
47 * {@link android.support.v17.leanback.widget.ObjectAdapter} as a set
48 * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses
49 * of {@link RowPresenter}.
50 * </p>
51 */
52public class RowsFragment extends BaseRowFragment implements Adaptable {
53
54    private MainFragmentAdapter mMainFragmentAdapter;
55    private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
56
57    @Override
58    public Object getAdapter(Class clazz) {
59        if (clazz == BrowseFragment.MainFragmentAdapter.class) {
60            if (mMainFragmentAdapter == null) {
61                mMainFragmentAdapter = new MainFragmentAdapter(this);
62            }
63            return mMainFragmentAdapter;
64        } else if (clazz == BrowseFragment.MainFragmentRowsAdapter.class) {
65            if (mMainFragmentRowsAdapter == null) {
66                mMainFragmentRowsAdapter = new MainFragmentRowsAdapter(this);
67            }
68            return mMainFragmentRowsAdapter;
69        }
70        return null;
71    }
72
73    /**
74     * Internal helper class that manages row select animation and apply a default
75     * dim to each row.
76     */
77    final class RowViewHolderExtra implements TimeListener {
78        final RowPresenter mRowPresenter;
79        final Presenter.ViewHolder mRowViewHolder;
80
81        final TimeAnimator mSelectAnimator = new TimeAnimator();
82
83        int mSelectAnimatorDurationInUse;
84        Interpolator mSelectAnimatorInterpolatorInUse;
85        float mSelectLevelAnimStart;
86        float mSelectLevelAnimDelta;
87
88        RowViewHolderExtra(ItemBridgeAdapter.ViewHolder ibvh) {
89            mRowPresenter = (RowPresenter) ibvh.getPresenter();
90            mRowViewHolder = ibvh.getViewHolder();
91            mSelectAnimator.setTimeListener(this);
92        }
93
94        @Override
95        public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
96            if (mSelectAnimator.isRunning()) {
97                updateSelect(totalTime, deltaTime);
98            }
99        }
100
101        void updateSelect(long totalTime, long deltaTime) {
102            float fraction;
103            if (totalTime >= mSelectAnimatorDurationInUse) {
104                fraction = 1;
105                mSelectAnimator.end();
106            } else {
107                fraction = (float) (totalTime / (double) mSelectAnimatorDurationInUse);
108            }
109            if (mSelectAnimatorInterpolatorInUse != null) {
110                fraction = mSelectAnimatorInterpolatorInUse.getInterpolation(fraction);
111            }
112            float level = mSelectLevelAnimStart + fraction * mSelectLevelAnimDelta;
113            mRowPresenter.setSelectLevel(mRowViewHolder, level);
114        }
115
116        void animateSelect(boolean select, boolean immediate) {
117            mSelectAnimator.end();
118            final float end = select ? 1 : 0;
119            if (immediate) {
120                mRowPresenter.setSelectLevel(mRowViewHolder, end);
121            } else if (mRowPresenter.getSelectLevel(mRowViewHolder) != end) {
122                mSelectAnimatorDurationInUse = mSelectAnimatorDuration;
123                mSelectAnimatorInterpolatorInUse = mSelectAnimatorInterpolator;
124                mSelectLevelAnimStart = mRowPresenter.getSelectLevel(mRowViewHolder);
125                mSelectLevelAnimDelta = end - mSelectLevelAnimStart;
126                mSelectAnimator.start();
127            }
128        }
129
130    }
131
132    private static final String TAG = "RowsFragment";
133    private static final boolean DEBUG = false;
134
135    private ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
136    private int mSubPosition;
137    private boolean mExpand = true;
138    private boolean mViewsCreated;
139    private int mAlignedTop;
140    private boolean mAfterEntranceTransition = true;
141
142    private BaseOnItemViewSelectedListener mOnItemViewSelectedListener;
143    private BaseOnItemViewClickedListener mOnItemViewClickedListener;
144
145    // Select animation and interpolator are not intended to be
146    // exposed at this moment. They might be synced with vertical scroll
147    // animation later.
148    int mSelectAnimatorDuration;
149    Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2);
150
151    private RecyclerView.RecycledViewPool mRecycledViewPool;
152    private ArrayList<Presenter> mPresenterMapper;
153
154    private ItemBridgeAdapter.AdapterListener mExternalAdapterListener;
155
156    @Override
157    protected VerticalGridView findGridViewFromRoot(View view) {
158        return (VerticalGridView) view.findViewById(R.id.container_list);
159    }
160
161    /**
162     * Sets an item clicked listener on the fragment.
163     * OnItemViewClickedListener will override {@link View.OnClickListener} that
164     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
165     * So in general, developer should choose one of the listeners but not both.
166     */
167    public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) {
168        mOnItemViewClickedListener = listener;
169        if (mViewsCreated) {
170            throw new IllegalStateException(
171                    "Item clicked listener must be set before views are created");
172        }
173    }
174
175    /**
176     * Returns the item clicked listener.
177     */
178    public BaseOnItemViewClickedListener getOnItemViewClickedListener() {
179        return mOnItemViewClickedListener;
180    }
181
182    /**
183     * @deprecated use {@link BrowseFragment#enableRowScaling(boolean)} instead.
184     *
185     * @param enable true to enable row scaling
186     */
187    public void enableRowScaling(boolean enable) {
188    }
189
190    /**
191     * Set the visibility of titles/hovercard of browse rows.
192     */
193    public void setExpand(boolean expand) {
194        mExpand = expand;
195        VerticalGridView listView = getVerticalGridView();
196        if (listView != null) {
197            final int count = listView.getChildCount();
198            if (DEBUG) Log.v(TAG, "setExpand " + expand + " count " + count);
199            for (int i = 0; i < count; i++) {
200                View view = listView.getChildAt(i);
201                ItemBridgeAdapter.ViewHolder vh
202                        = (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
203                setRowViewExpanded(vh, mExpand);
204            }
205        }
206    }
207
208    /**
209     * Sets an item selection listener.
210     */
211    public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) {
212        mOnItemViewSelectedListener = listener;
213        VerticalGridView listView = getVerticalGridView();
214        if (listView != null) {
215            final int count = listView.getChildCount();
216            for (int i = 0; i < count; i++) {
217                View view = listView.getChildAt(i);
218                ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
219                        listView.getChildViewHolder(view);
220                getRowViewHolder(ibvh).setOnItemViewSelectedListener(mOnItemViewSelectedListener);
221            }
222        }
223    }
224
225    /**
226     * Returns an item selection listener.
227     */
228    public BaseOnItemViewSelectedListener getOnItemViewSelectedListener() {
229        return mOnItemViewSelectedListener;
230    }
231
232    @Override
233    void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder,
234            int position, int subposition) {
235        if (mSelectedViewHolder != viewHolder || mSubPosition != subposition) {
236            if (DEBUG) Log.v(TAG, "new row selected position " + position + " subposition "
237                    + subposition + " view " + viewHolder.itemView);
238            mSubPosition = subposition;
239            if (mSelectedViewHolder != null) {
240                setRowViewSelected(mSelectedViewHolder, false, false);
241            }
242            mSelectedViewHolder = (ItemBridgeAdapter.ViewHolder) viewHolder;
243            if (mSelectedViewHolder != null) {
244                setRowViewSelected(mSelectedViewHolder, true, false);
245            }
246        }
247        // When RowsFragment is embedded inside a page fragment, we want to show
248        // the title view only when we're on the first row or there is no data.
249        if (mMainFragmentAdapter != null) {
250            mMainFragmentAdapter.getFragmentHost().showTitleView(position <= 0);
251        }
252    }
253
254    /**
255     * Get row ViewHolder at adapter position.  Returns null if the row object is not in adapter or
256     * the row object has not been bound to a row view.
257     *
258     * @param position Position of row in adapter.
259     * @return Row ViewHolder at a given adapter position.
260     */
261    public RowPresenter.ViewHolder getRowViewHolder(int position) {
262        VerticalGridView verticalView = getVerticalGridView();
263        if (verticalView == null) {
264            return null;
265        }
266        return getRowViewHolder((ItemBridgeAdapter.ViewHolder)
267                verticalView.findViewHolderForAdapterPosition(position));
268    }
269
270    @Override
271    int getLayoutResourceId() {
272        return R.layout.lb_rows_fragment;
273    }
274
275    @Override
276    public void onCreate(Bundle savedInstanceState) {
277        super.onCreate(savedInstanceState);
278        mSelectAnimatorDuration = getResources().getInteger(
279                R.integer.lb_browse_rows_anim_duration);
280    }
281
282    @Override
283    public void onViewCreated(View view, Bundle savedInstanceState) {
284        if (DEBUG) Log.v(TAG, "onViewCreated");
285        super.onViewCreated(view, savedInstanceState);
286        // Align the top edge of child with id row_content.
287        // Need set this for directly using RowsFragment.
288        getVerticalGridView().setItemAlignmentViewId(R.id.row_content);
289        getVerticalGridView().setSaveChildrenPolicy(VerticalGridView.SAVE_LIMITED_CHILD);
290
291        setAlignment(mAlignedTop);
292
293        mRecycledViewPool = null;
294        mPresenterMapper = null;
295        if (mMainFragmentAdapter != null) {
296            mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
297        }
298
299    }
300
301    @Override
302    public void onDestroyView() {
303        mViewsCreated = false;
304        super.onDestroyView();
305    }
306
307    void setExternalAdapterListener(ItemBridgeAdapter.AdapterListener listener) {
308        mExternalAdapterListener = listener;
309    }
310
311    private static void setRowViewExpanded(ItemBridgeAdapter.ViewHolder vh, boolean expanded) {
312        ((RowPresenter) vh.getPresenter()).setRowViewExpanded(vh.getViewHolder(), expanded);
313    }
314
315    private static void setRowViewSelected(ItemBridgeAdapter.ViewHolder vh, boolean selected,
316            boolean immediate) {
317        RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
318        extra.animateSelect(selected, immediate);
319        ((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
320    }
321
322    private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
323            new ItemBridgeAdapter.AdapterListener() {
324        @Override
325        public void onAddPresenter(Presenter presenter, int type) {
326            if (mExternalAdapterListener != null) {
327                mExternalAdapterListener.onAddPresenter(presenter, type);
328            }
329        }
330
331        @Override
332        public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
333            VerticalGridView listView = getVerticalGridView();
334            if (listView != null) {
335                // set clip children false for slide animation
336                listView.setClipChildren(false);
337            }
338            setupSharedViewPool(vh);
339            mViewsCreated = true;
340            vh.setExtraObject(new RowViewHolderExtra(vh));
341            // selected state is initialized to false, then driven by grid view onChildSelected
342            // events.  When there is rebind, grid view fires onChildSelected event properly.
343            // So we don't need do anything special later in onBind or onAttachedToWindow.
344            setRowViewSelected(vh, false, true);
345            if (mExternalAdapterListener != null) {
346                mExternalAdapterListener.onCreate(vh);
347            }
348        }
349
350        @Override
351        public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
352            if (DEBUG) Log.v(TAG, "onAttachToWindow");
353            // All views share the same mExpand value.  When we attach a view to grid view,
354            // we should make sure it pick up the latest mExpand value we set early on other
355            // attached views.  For no-structure-change update,  the view is rebound to new data,
356            // but again it should use the unchanged mExpand value,  so we don't need do any
357            // thing in onBind.
358            setRowViewExpanded(vh, mExpand);
359            RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
360            RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
361            rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
362            rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
363            rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
364            if (mExternalAdapterListener != null) {
365                mExternalAdapterListener.onAttachedToWindow(vh);
366            }
367        }
368
369        @Override
370        public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
371            if (mSelectedViewHolder == vh) {
372                setRowViewSelected(mSelectedViewHolder, false, true);
373                mSelectedViewHolder = null;
374            }
375            if (mExternalAdapterListener != null) {
376                mExternalAdapterListener.onDetachedFromWindow(vh);
377            }
378        }
379
380        @Override
381        public void onBind(ItemBridgeAdapter.ViewHolder vh) {
382            if (mExternalAdapterListener != null) {
383                mExternalAdapterListener.onBind(vh);
384            }
385        }
386
387        @Override
388        public void onUnbind(ItemBridgeAdapter.ViewHolder vh) {
389            setRowViewSelected(vh, false, true);
390            if (mExternalAdapterListener != null) {
391                mExternalAdapterListener.onUnbind(vh);
392            }
393        }
394    };
395
396    private void setupSharedViewPool(ItemBridgeAdapter.ViewHolder bridgeVh) {
397        RowPresenter rowPresenter = (RowPresenter) bridgeVh.getPresenter();
398        RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(bridgeVh.getViewHolder());
399
400        if (rowVh instanceof ListRowPresenter.ViewHolder) {
401            HorizontalGridView view = ((ListRowPresenter.ViewHolder) rowVh).getGridView();
402            // Recycled view pool is shared between all list rows
403            if (mRecycledViewPool == null) {
404                mRecycledViewPool = view.getRecycledViewPool();
405            } else {
406                view.setRecycledViewPool(mRecycledViewPool);
407            }
408
409            ItemBridgeAdapter bridgeAdapter =
410                    ((ListRowPresenter.ViewHolder) rowVh).getBridgeAdapter();
411            if (mPresenterMapper == null) {
412                mPresenterMapper = bridgeAdapter.getPresenterMapper();
413            } else {
414                bridgeAdapter.setPresenterMapper(mPresenterMapper);
415            }
416        }
417    }
418
419    @Override
420    void updateAdapter() {
421        super.updateAdapter();
422        mSelectedViewHolder = null;
423        mViewsCreated = false;
424
425        ItemBridgeAdapter adapter = getBridgeAdapter();
426        if (adapter != null) {
427            adapter.setAdapterListener(mBridgeAdapterListener);
428        }
429    }
430
431    @Override
432    public boolean onTransitionPrepare() {
433        boolean prepared = super.onTransitionPrepare();
434        if (prepared) {
435            freezeRows(true);
436        }
437        return prepared;
438    }
439
440    @Override
441    public void onTransitionEnd() {
442        super.onTransitionEnd();
443        freezeRows(false);
444    }
445
446    private void freezeRows(boolean freeze) {
447        VerticalGridView verticalView = getVerticalGridView();
448        if (verticalView != null) {
449            final int count = verticalView.getChildCount();
450            for (int i = 0; i < count; i++) {
451                ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
452                        verticalView.getChildViewHolder(verticalView.getChildAt(i));
453                RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
454                RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
455                rowPresenter.freeze(vh, freeze);
456            }
457        }
458    }
459
460    /**
461     * For rows that willing to participate entrance transition,  this function
462     * hide views if afterTransition is true,  show views if afterTransition is false.
463     */
464    public void setEntranceTransitionState(boolean afterTransition) {
465        mAfterEntranceTransition = afterTransition;
466        VerticalGridView verticalView = getVerticalGridView();
467        if (verticalView != null) {
468            final int count = verticalView.getChildCount();
469            for (int i = 0; i < count; i++) {
470                ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
471                        verticalView.getChildViewHolder(verticalView.getChildAt(i));
472                RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
473                RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
474                rowPresenter.setEntranceTransitionState(vh, mAfterEntranceTransition);
475            }
476        }
477    }
478
479    /**
480     * Selects a Row and perform an optional task on the Row. For example
481     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
482     * Scroll to 11th row and selects 6th item on that row.  The method will be ignored if
483     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
484     * ViewGroup, Bundle)}).
485     *
486     * @param rowPosition Which row to select.
487     * @param smooth True to scroll to the row, false for no animation.
488     * @param rowHolderTask Task to perform on the Row.
489     */
490    public void setSelectedPosition(int rowPosition, boolean smooth,
491            final Presenter.ViewHolderTask rowHolderTask) {
492        VerticalGridView verticalView = getVerticalGridView();
493        if (verticalView == null) {
494            return;
495        }
496        ViewHolderTask task = null;
497        if (rowHolderTask != null) {
498            task = new ViewHolderTask() {
499                @Override
500                public void run(RecyclerView.ViewHolder rvh) {
501                    rowHolderTask.run(getRowViewHolder((ItemBridgeAdapter.ViewHolder) rvh));
502                }
503            };
504        }
505        if (smooth) {
506            verticalView.setSelectedPositionSmooth(rowPosition, task);
507        } else {
508            verticalView.setSelectedPosition(rowPosition, task);
509        }
510    }
511
512    static RowPresenter.ViewHolder getRowViewHolder(ItemBridgeAdapter.ViewHolder ibvh) {
513        if (ibvh == null) {
514            return null;
515        }
516        RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
517        return rowPresenter.getRowViewHolder(ibvh.getViewHolder());
518    }
519
520    public boolean isScrolling() {
521        if (getVerticalGridView() == null) {
522            return false;
523        }
524        return getVerticalGridView().getScrollState() != HorizontalGridView.SCROLL_STATE_IDLE;
525    }
526
527    @Override
528    public void setAlignment(int windowAlignOffsetFromTop) {
529        mAlignedTop = windowAlignOffsetFromTop;
530        final VerticalGridView gridView = getVerticalGridView();
531
532        if (gridView != null) {
533            gridView.setItemAlignmentOffset(0);
534            gridView.setItemAlignmentOffsetPercent(
535                    VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
536            gridView.setItemAlignmentOffsetWithPadding(true);
537            gridView.setWindowAlignmentOffset(mAlignedTop);
538            // align to a fixed position from top
539            gridView.setWindowAlignmentOffsetPercent(
540                    VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
541            gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
542        }
543    }
544
545    public static class MainFragmentAdapter extends BrowseFragment.MainFragmentAdapter<RowsFragment> {
546
547        public MainFragmentAdapter(RowsFragment fragment) {
548            super(fragment);
549            setScalingEnabled(true);
550        }
551
552        @Override
553        public boolean isScrolling() {
554            return getFragment().isScrolling();
555        }
556
557        @Override
558        public void setExpand(boolean expand) {
559            getFragment().setExpand(expand);
560        }
561
562        @Override
563        public void setEntranceTransitionState(boolean state) {
564            getFragment().setEntranceTransitionState(state);
565        }
566
567        @Override
568        public void setAlignment(int windowAlignOffsetFromTop) {
569            getFragment().setAlignment(windowAlignOffsetFromTop);
570        }
571
572        @Override
573        public boolean onTransitionPrepare() {
574            return getFragment().onTransitionPrepare();
575        }
576
577        @Override
578        public void onTransitionStart() {
579            getFragment().onTransitionStart();
580        }
581
582        @Override
583        public void onTransitionEnd() {
584            getFragment().onTransitionEnd();
585        }
586
587    }
588
589    public static class MainFragmentRowsAdapter
590            extends BrowseFragment.MainFragmentRowsAdapter<RowsFragment> {
591
592        public MainFragmentRowsAdapter(RowsFragment fragment) {
593            super(fragment);
594        }
595
596        @Override
597        public void setAdapter(ObjectAdapter adapter) {
598            getFragment().setAdapter(adapter);
599        }
600
601        /**
602         * Sets an item clicked listener on the fragment.
603         */
604        @Override
605        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
606            getFragment().setOnItemViewClickedListener(listener);
607        }
608
609        @Override
610        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
611            getFragment().setOnItemViewSelectedListener(listener);
612        }
613
614        @Override
615        public void setSelectedPosition(int rowPosition,
616                                        boolean smooth,
617                                        final Presenter.ViewHolderTask rowHolderTask) {
618            getFragment().setSelectedPosition(rowPosition, smooth, rowHolderTask);
619        }
620
621        @Override
622        public void setSelectedPosition(int rowPosition, boolean smooth) {
623            getFragment().setSelectedPosition(rowPosition, smooth);
624        }
625
626        @Override
627        public int getSelectedPosition() {
628            return getFragment().getSelectedPosition();
629        }
630    }
631}
632