StackScrollAlgorithm.java revision 9cbadd3c08a7d7dd3412743dd04aecb16c5a1595
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 */
16
17package com.android.systemui.statusbar.stack;
18
19import android.content.Context;
20import android.util.Log;
21import android.view.View;
22import android.view.ViewGroup;
23
24import com.android.systemui.R;
25import com.android.systemui.statusbar.ExpandableNotificationRow;
26import com.android.systemui.statusbar.ExpandableView;
27
28import java.util.ArrayList;
29
30/**
31 * The Algorithm of the {@link com.android.systemui.statusbar.stack
32 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
33 * .stack.StackScrollState}
34 */
35public class StackScrollAlgorithm {
36
37    private static final String LOG_TAG = "StackScrollAlgorithm";
38
39    private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
40    private static final int MAX_ITEMS_IN_TOP_STACK = 3;
41
42    private int mPaddingBetweenElements;
43    private int mCollapsedSize;
44    private int mTopStackPeekSize;
45    private int mBottomStackPeekSize;
46    private int mZDistanceBetweenElements;
47    private int mZBasicHeight;
48
49    private StackIndentationFunctor mTopStackIndentationFunctor;
50    private StackIndentationFunctor mBottomStackIndentationFunctor;
51
52    private int mLayoutHeight;
53
54    /** mLayoutHeight - mTopPadding */
55    private int mInnerHeight;
56    private int mTopPadding;
57    private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
58    private boolean mIsExpansionChanging;
59    private int mFirstChildMaxHeight;
60    private boolean mIsExpanded;
61    private ExpandableView mFirstChildWhileExpanding;
62    private boolean mExpandedOnStart;
63    private int mTopStackTotalSize;
64
65    public StackScrollAlgorithm(Context context) {
66        initConstants(context);
67    }
68
69    private void initConstants(Context context) {
70        mPaddingBetweenElements = context.getResources()
71                .getDimensionPixelSize(R.dimen.notification_padding);
72        mCollapsedSize = context.getResources()
73                .getDimensionPixelSize(R.dimen.notification_min_height);
74        mTopStackPeekSize = context.getResources()
75                .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
76        mBottomStackPeekSize = context.getResources()
77                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
78        mZDistanceBetweenElements = context.getResources()
79                .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
80        mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
81        mTopStackTotalSize = mCollapsedSize + mPaddingBetweenElements;
82        mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
83                MAX_ITEMS_IN_TOP_STACK,
84                mTopStackPeekSize,
85                mTopStackTotalSize,
86                0.5f);
87        mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
88                MAX_ITEMS_IN_BOTTOM_STACK,
89                mBottomStackPeekSize,
90                mCollapsedSize + mBottomStackPeekSize + mPaddingBetweenElements,
91                0.5f);
92    }
93
94
95    public void getStackScrollState(StackScrollState resultState) {
96        // The state of the local variables are saved in an algorithmState to easily subdivide it
97        // into multiple phases.
98        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
99
100        // First we reset the view states to their default values.
101        resultState.resetViewStates();
102
103        algorithmState.itemsInTopStack = 0.0f;
104        algorithmState.partialInTop = 0.0f;
105        algorithmState.lastTopStackIndex = 0;
106        algorithmState.scrolledPixelsTop = 0;
107        algorithmState.itemsInBottomStack = 0.0f;
108        algorithmState.partialInBottom = 0.0f;
109        algorithmState.scrollY = resultState.getScrollY() + mCollapsedSize;
110
111        updateVisibleChildren(resultState, algorithmState);
112
113        // Phase 1:
114        findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
115
116        // Phase 2:
117        updatePositionsForState(resultState, algorithmState);
118
119        // Phase 3:
120        updateZValuesForState(resultState, algorithmState);
121    }
122
123    /**
124     * Update the visible children on the state.
125     */
126    private void updateVisibleChildren(StackScrollState resultState,
127            StackScrollAlgorithmState state) {
128        ViewGroup hostView = resultState.getHostView();
129        int childCount = hostView.getChildCount();
130        state.visibleChildren.clear();
131        state.visibleChildren.ensureCapacity(childCount);
132        for (int i = 0; i < childCount; i++) {
133            ExpandableView v = (ExpandableView) hostView.getChildAt(i);
134            if (v.getVisibility() != View.GONE) {
135                state.visibleChildren.add(v);
136            }
137        }
138    }
139
140    /**
141     * Determine the positions for the views. This is the main part of the algorithm.
142     *
143     * @param resultState The result state to update if a change to the properties of a child occurs
144     * @param algorithmState The state in which the current pass of the algorithm is currently in
145     *                       and which will be updated
146     */
147    private void updatePositionsForState(StackScrollState resultState,
148            StackScrollAlgorithmState algorithmState) {
149
150        // The starting position of the bottom stack peek
151        float bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
152
153        // The position where the bottom stack starts.
154        float bottomStackStart = bottomPeekStart - mCollapsedSize;
155
156        // The y coordinate of the current child.
157        float currentYPosition = 0.0f;
158
159        // How far in is the element currently transitioning into the bottom stack.
160        float yPositionInScrollView = 0.0f;
161
162        int childCount = algorithmState.visibleChildren.size();
163        int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
164        for (int i = 0; i < childCount; i++) {
165            ExpandableView child = algorithmState.visibleChildren.get(i);
166            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
167            childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN;
168            int childHeight = getMaxAllowedChildHeight(child);
169            float yPositionInScrollViewAfterElement = yPositionInScrollView
170                    + childHeight
171                    + mPaddingBetweenElements;
172            float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize;
173
174            if (i == algorithmState.lastTopStackIndex + 1) {
175                // Normally the position of this child is the position in the regular scrollview,
176                // but if the two stacks are very close to each other,
177                // then have have to push it even more upwards to the position of the bottom
178                // stack start.
179                currentYPosition = Math.min(scrollOffset, bottomStackStart);
180            }
181            childViewState.yTranslation = currentYPosition;
182
183            // The y position after this element
184            float nextYPosition = currentYPosition + childHeight +
185                    mPaddingBetweenElements;
186
187            if (i <= algorithmState.lastTopStackIndex) {
188                // Case 1:
189                // We are in the top Stack
190                updateStateForTopStackChild(algorithmState,
191                        numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset);
192                clampYTranslation(childViewState, childHeight);
193                // check if we are overlapping with the bottom stack
194                if (childViewState.yTranslation + childHeight + mPaddingBetweenElements
195                        >= bottomStackStart && !mIsExpansionChanging && i != 0) {
196                    // TODO: handle overlapping sizes with end stack better
197                    // we just collapse this element
198                    childViewState.height = mCollapsedSize;
199                }
200            } else if (nextYPosition >= bottomStackStart) {
201                // Case 2:
202                // We are in the bottom stack.
203                if (currentYPosition >= bottomStackStart) {
204                    // According to the regular scroll view we are fully translated out of the
205                    // bottom of the screen so we are fully in the bottom stack
206                    updateStateForChildFullyInBottomStack(algorithmState,
207                            bottomStackStart, childViewState, childHeight);
208                } else {
209                    // According to the regular scroll view we are currently translating out of /
210                    // into the bottom of the screen
211                    updateStateForChildTransitioningInBottom(algorithmState,
212                            bottomStackStart, bottomPeekStart, currentYPosition,
213                            childViewState, childHeight);
214                }
215            } else {
216                // Case 3:
217                // We are in the regular scroll area.
218                childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
219                clampYTranslation(childViewState, childHeight);
220            }
221
222            // The first card is always rendered.
223            if (i == 0) {
224                childViewState.alpha = 1.0f;
225                childViewState.yTranslation = 0;
226                childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD;
227            }
228            if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) {
229                Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
230            }
231            currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
232            yPositionInScrollView = yPositionInScrollViewAfterElement;
233
234            childViewState.yTranslation += mTopPadding;
235        }
236    }
237
238    /**
239     * Clamp the yTranslation both up and down to valid positions.
240     *
241     * @param childViewState the view state of the child
242     * @param childHeight the height of this child
243     */
244    private void clampYTranslation(StackScrollState.ViewState childViewState, int childHeight) {
245        clampPositionToBottomStackStart(childViewState, childHeight);
246        clampPositionToTopStackEnd(childViewState, childHeight);
247    }
248
249    /**
250     * Clamp the yTranslation of the child down such that its end is at most on the beginning of
251     * the bottom stack.
252     *
253     * @param childViewState the view state of the child
254     * @param childHeight the height of this child
255     */
256    private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState,
257            int childHeight) {
258        childViewState.yTranslation = Math.min(childViewState.yTranslation,
259                mInnerHeight - mBottomStackPeekSize - childHeight);
260    }
261
262    /**
263     * Clamp the yTranslation of the child up such that its end is at lest on the end of the top
264     * stack.get
265     *
266     * @param childViewState the view state of the child
267     * @param childHeight the height of this child
268     */
269    private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState,
270            int childHeight) {
271        childViewState.yTranslation = Math.max(childViewState.yTranslation,
272                mCollapsedSize - childHeight);
273    }
274
275    private int getMaxAllowedChildHeight(View child) {
276        if (child instanceof ExpandableNotificationRow) {
277            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
278            return row.getIntrinsicHeight();
279        } else if (child instanceof ExpandableView) {
280            ExpandableView expandableView = (ExpandableView) child;
281            return expandableView.getActualHeight();
282        }
283        return child == null? mCollapsedSize : child.getHeight();
284    }
285
286    private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
287            float transitioningPositionStart, float bottomPeakStart, float currentYPosition,
288            StackScrollState.ViewState childViewState, int childHeight) {
289
290        // This is the transitioning element on top of bottom stack, calculate how far we are in.
291        algorithmState.partialInBottom = 1.0f - (
292                (transitioningPositionStart - currentYPosition) / (childHeight +
293                        mPaddingBetweenElements));
294
295        // the offset starting at the transitionPosition of the bottom stack
296        float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
297        algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
298        childViewState.yTranslation = transitioningPositionStart + offset - childHeight
299                - mPaddingBetweenElements;
300
301        // We want at least to be at the end of the top stack when collapsing
302        clampPositionToTopStackEnd(childViewState, childHeight);
303        childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
304    }
305
306    private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
307            float transitioningPositionStart, StackScrollState.ViewState childViewState,
308            int childHeight) {
309
310        float currentYPosition;
311        algorithmState.itemsInBottomStack += 1.0f;
312        if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
313            // We are visually entering the bottom stack
314            currentYPosition = transitioningPositionStart
315                    + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
316                    - mPaddingBetweenElements;
317            childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING;
318        } else {
319            // we are fully inside the stack
320            if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
321                childViewState.alpha = 0.0f;
322            } else if (algorithmState.itemsInBottomStack
323                    > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
324                childViewState.alpha = 1.0f - algorithmState.partialInBottom;
325            }
326            childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN;
327            currentYPosition = mInnerHeight;
328        }
329        childViewState.yTranslation = currentYPosition - childHeight;
330        clampPositionToTopStackEnd(childViewState, childHeight);
331    }
332
333    private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
334            int numberOfElementsCompletelyIn, int i, int childHeight,
335            StackScrollState.ViewState childViewState, float scrollOffset) {
336
337
338        // First we calculate the index relative to the current stack window of size at most
339        // {@link #MAX_ITEMS_IN_TOP_STACK}
340        int paddedIndex = i - 1
341                - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
342        if (paddedIndex >= 0) {
343
344            // We are currently visually entering the top stack
345            float distanceToStack = childHeight - algorithmState.scrolledPixelsTop;
346            if (i == algorithmState.lastTopStackIndex && distanceToStack > mTopStackTotalSize) {
347
348                // Child is currently translating into stack but not yet inside slow down zone.
349                // Handle it like the regular scrollview.
350                childViewState.yTranslation = scrollOffset;
351            } else {
352                // Apply stacking logic.
353                float numItemsBefore;
354                if (i == algorithmState.lastTopStackIndex) {
355                    numItemsBefore = 1.0f - (distanceToStack / mTopStackTotalSize);
356                } else {
357                    numItemsBefore = algorithmState.itemsInTopStack - i;
358                }
359                // The end position of the current child
360                float currentChildEndY = mCollapsedSize + mTopStackTotalSize -
361                        mTopStackIndentationFunctor.getValue(numItemsBefore);
362                childViewState.yTranslation = currentChildEndY - childHeight;
363            }
364            childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
365        } else {
366            if (paddedIndex == -1) {
367                childViewState.alpha = 1.0f - algorithmState.partialInTop;
368            } else {
369                // We are hidden behind the top card and faded out, so we can hide ourselves.
370                childViewState.alpha = 0.0f;
371            }
372            childViewState.yTranslation = mCollapsedSize - childHeight;
373            childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
374        }
375
376
377    }
378
379    /**
380     * Find the number of items in the top stack and update the result state if needed.
381     *
382     * @param resultState The result state to update if a height change of an child occurs
383     * @param algorithmState The state in which the current pass of the algorithm is currently in
384     *                       and which will be updated
385     */
386    private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
387            StackScrollAlgorithmState algorithmState) {
388
389        // The y Position if the element would be in a regular scrollView
390        float yPositionInScrollView = 0.0f;
391        int childCount = algorithmState.visibleChildren.size();
392
393        // find the number of elements in the top stack.
394        for (int i = 0; i < childCount; i++) {
395            ExpandableView child = algorithmState.visibleChildren.get(i);
396            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
397            int childHeight = getMaxAllowedChildHeight(child);
398            float yPositionInScrollViewAfterElement = yPositionInScrollView
399                    + childHeight
400                    + mPaddingBetweenElements;
401            if (yPositionInScrollView < algorithmState.scrollY) {
402                if (i == 0 && algorithmState.scrollY == mCollapsedSize) {
403
404                    // The starting position of the bottom stack peek
405                    int bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
406                    // Collapse and expand the first child while the shade is being expanded
407                    float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
408                            ? mFirstChildMaxHeight
409                            : childHeight;
410                    childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight),
411                            mCollapsedSize);
412                    algorithmState.itemsInTopStack = 1.0f;
413
414                } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) {
415                    // According to the regular scroll view we are fully off screen
416                    algorithmState.itemsInTopStack += 1.0f;
417                    if (i == 0) {
418                        childViewState.height = mCollapsedSize;
419                    }
420                } else {
421                    // According to the regular scroll view we are partially off screen
422                    // If it is expanded we have to collapse it to a new size
423                    float newSize = yPositionInScrollViewAfterElement
424                            - mPaddingBetweenElements
425                            - algorithmState.scrollY;
426
427                    if (i == 0) {
428                        newSize += mCollapsedSize;
429                    }
430
431                    // How much did we scroll into this child
432                    algorithmState.scrolledPixelsTop = childHeight - newSize;
433                    algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight
434                            + mPaddingBetweenElements);
435
436                    // Our element can be expanded, so this can get negative
437                    algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
438                    algorithmState.itemsInTopStack += algorithmState.partialInTop;
439                    newSize = Math.max(mCollapsedSize, newSize);
440                    if (i == 0) {
441                        childViewState.height = (int) newSize;
442                    }
443                    algorithmState.lastTopStackIndex = i;
444                    break;
445                }
446            } else {
447                algorithmState.lastTopStackIndex = i - 1;
448                // We are already past the stack so we can end the loop
449                break;
450            }
451            yPositionInScrollView = yPositionInScrollViewAfterElement;
452        }
453    }
454
455    /**
456     * Calculate the Z positions for all children based on the number of items in both stacks and
457     * save it in the resultState
458     *
459     * @param resultState The result state to update the zTranslation values
460     * @param algorithmState The state in which the current pass of the algorithm is currently in
461     */
462    private void updateZValuesForState(StackScrollState resultState,
463            StackScrollAlgorithmState algorithmState) {
464        int childCount = algorithmState.visibleChildren.size();
465        for (int i = 0; i < childCount; i++) {
466            View child = algorithmState.visibleChildren.get(i);
467            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
468            if (i < algorithmState.itemsInTopStack) {
469                float stackIndex = algorithmState.itemsInTopStack - i;
470                stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
471                childViewState.zTranslation = mZBasicHeight
472                        + stackIndex * mZDistanceBetweenElements;
473            } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
474                float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
475                float translationZ = mZBasicHeight
476                        - numItemsAbove * mZDistanceBetweenElements;
477                childViewState.zTranslation = translationZ;
478            } else {
479                childViewState.zTranslation = mZBasicHeight;
480            }
481        }
482    }
483
484    public void setLayoutHeight(int layoutHeight) {
485        this.mLayoutHeight = layoutHeight;
486        updateInnerHeight();
487    }
488
489    public void setTopPadding(int topPadding) {
490        mTopPadding = topPadding;
491        updateInnerHeight();
492    }
493
494    private void updateInnerHeight() {
495        mInnerHeight = mLayoutHeight - mTopPadding;
496    }
497
498    public void onExpansionStarted(StackScrollState currentState) {
499        mIsExpansionChanging = true;
500        mExpandedOnStart = mIsExpanded;
501        ViewGroup hostView = currentState.getHostView();
502        updateFirstChildHeightWhileExpanding(hostView);
503    }
504
505    private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
506        mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
507        if (mFirstChildWhileExpanding != null) {
508            if (mExpandedOnStart) {
509
510                // We are collapsing the shade, so the first child can get as most as high as the
511                // current height.
512                mFirstChildMaxHeight = mFirstChildWhileExpanding.getActualHeight();
513            } else {
514
515                // We are expanding the shade, expand it to its full height.
516                if (mFirstChildWhileExpanding.getWidth() == 0) {
517
518                    // This child was not layouted yet, wait for a layout pass
519                    mFirstChildWhileExpanding
520                            .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
521                                @Override
522                                public void onLayoutChange(View v, int left, int top, int right,
523                                        int bottom, int oldLeft, int oldTop, int oldRight,
524                                        int oldBottom) {
525                                    if (mFirstChildWhileExpanding != null) {
526                                        mFirstChildMaxHeight = getMaxAllowedChildHeight(
527                                                mFirstChildWhileExpanding);
528                                    } else {
529                                        mFirstChildMaxHeight = 0;
530                                    }
531                                    v.removeOnLayoutChangeListener(this);
532                                }
533                            });
534                } else {
535                    mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
536                }
537            }
538        } else {
539            mFirstChildMaxHeight = 0;
540        }
541    }
542
543    private View findFirstVisibleChild(ViewGroup container) {
544        int childCount = container.getChildCount();
545        for (int i = 0; i < childCount; i++) {
546            View child = container.getChildAt(i);
547            if (child.getVisibility() != View.GONE) {
548                return child;
549            }
550        }
551        return null;
552    }
553
554    public void onExpansionStopped() {
555        mIsExpansionChanging = false;
556        mFirstChildWhileExpanding = null;
557    }
558
559    public void setIsExpanded(boolean isExpanded) {
560        this.mIsExpanded = isExpanded;
561    }
562
563    public void notifyChildrenChanged(ViewGroup hostView) {
564        if (mIsExpansionChanging) {
565            updateFirstChildHeightWhileExpanding(hostView);
566        }
567    }
568
569    class StackScrollAlgorithmState {
570
571        /**
572         * The scroll position of the algorithm
573         */
574        public int scrollY;
575
576        /**
577         *  The quantity of items which are in the top stack.
578         */
579        public float itemsInTopStack;
580
581        /**
582         * how far in is the element currently transitioning into the top stack
583         */
584        public float partialInTop;
585
586        /**
587         * The number of pixels the last child in the top stack has scrolled in to the stack
588         */
589        public float scrolledPixelsTop;
590
591        /**
592         * The last item index which is in the top stack.
593         */
594        public int lastTopStackIndex;
595
596        /**
597         * The quantity of items which are in the bottom stack.
598         */
599        public float itemsInBottomStack;
600
601        /**
602         * how far in is the element currently transitioning into the bottom stack
603         */
604        public float partialInBottom;
605
606        /**
607         * The children from the host view which are not gone.
608         */
609        public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
610    }
611
612}
613