1/**
2 * Copyright (C) 2015 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.launcher3.folder;
18
19import android.annotation.SuppressLint;
20import android.content.Context;
21import android.graphics.Canvas;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.view.Gravity;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewDebug;
28import android.view.animation.DecelerateInterpolator;
29
30import com.android.launcher3.BubbleTextView;
31import com.android.launcher3.CellLayout;
32import com.android.launcher3.DeviceProfile;
33import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
34import com.android.launcher3.InvariantDeviceProfile;
35import com.android.launcher3.ItemInfo;
36import com.android.launcher3.Launcher;
37import com.android.launcher3.LauncherAppState;
38import com.android.launcher3.LauncherModel;
39import com.android.launcher3.PagedView;
40import com.android.launcher3.R;
41import com.android.launcher3.ShortcutAndWidgetContainer;
42import com.android.launcher3.ShortcutInfo;
43import com.android.launcher3.Utilities;
44import com.android.launcher3.Workspace.ItemOperator;
45import com.android.launcher3.dragndrop.DragController;
46import com.android.launcher3.keyboard.ViewGroupFocusHelper;
47import com.android.launcher3.pageindicators.PageIndicator;
48import com.android.launcher3.util.Themes;
49import com.android.launcher3.util.Thunk;
50
51import java.util.ArrayList;
52import java.util.HashMap;
53import java.util.Iterator;
54import java.util.Map;
55
56public class FolderPagedView extends PagedView {
57
58    private static final String TAG = "FolderPagedView";
59
60    private static final boolean ALLOW_FOLDER_SCROLL = true;
61
62    private static final int REORDER_ANIMATION_DURATION = 230;
63    private static final int START_VIEW_REORDER_DELAY = 30;
64    private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
65
66    /**
67     * Fraction of the width to scroll when showing the next page hint.
68     */
69    private static final float SCROLL_HINT_FRACTION = 0.07f;
70
71    private static final int[] sTempPosArray = new int[2];
72
73    public final boolean mIsRtl;
74
75    private final LayoutInflater mInflater;
76    private final ViewGroupFocusHelper mFocusIndicatorHelper;
77
78    @Thunk final HashMap<View, Runnable> mPendingAnimations = new HashMap<>();
79
80    @ViewDebug.ExportedProperty(category = "launcher")
81    private final int mMaxCountX;
82    @ViewDebug.ExportedProperty(category = "launcher")
83    private final int mMaxCountY;
84    @ViewDebug.ExportedProperty(category = "launcher")
85    private final int mMaxItemsPerPage;
86
87    private int mAllocatedContentSize;
88    @ViewDebug.ExportedProperty(category = "launcher")
89    private int mGridCountX;
90    @ViewDebug.ExportedProperty(category = "launcher")
91    private int mGridCountY;
92
93    private Folder mFolder;
94    private PagedFolderKeyEventListener mKeyListener;
95
96    private PageIndicator mPageIndicator;
97
98    public FolderPagedView(Context context, AttributeSet attrs) {
99        super(context, attrs);
100        InvariantDeviceProfile profile = LauncherAppState.getIDP(context);
101        mMaxCountX = profile.numFolderColumns;
102        mMaxCountY = profile.numFolderRows;
103
104        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
105
106        mInflater = LayoutInflater.from(context);
107
108        mIsRtl = Utilities.isRtl(getResources());
109        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
110
111        setEdgeGlowColor(Themes.getAttrColor(context, android.R.attr.colorEdgeEffect));
112        mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
113    }
114
115    public void setFolder(Folder folder) {
116        mFolder = folder;
117        mKeyListener = new PagedFolderKeyEventListener(folder);
118        mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator);
119        initParentViews(folder);
120    }
121
122    /**
123     * Sets up the grid size such that {@param count} items can fit in the grid.
124     * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
125     * maintaining the restrictions of {@link #mMaxCountX} &amp; {@link #mMaxCountY}.
126     */
127    private void setupContentDimensions(int count) {
128        mAllocatedContentSize = count;
129        boolean done;
130        if (count >= mMaxItemsPerPage) {
131            mGridCountX = mMaxCountX;
132            mGridCountY = mMaxCountY;
133            done = true;
134        } else {
135            done = false;
136        }
137
138        while (!done) {
139            int oldCountX = mGridCountX;
140            int oldCountY = mGridCountY;
141            if (mGridCountX * mGridCountY < count) {
142                // Current grid is too small, expand it
143                if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) {
144                    mGridCountX++;
145                } else if (mGridCountY < mMaxCountY) {
146                    mGridCountY++;
147                }
148                if (mGridCountY == 0) mGridCountY++;
149            } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) {
150                mGridCountY = Math.max(0, mGridCountY - 1);
151            } else if ((mGridCountX - 1) * mGridCountY >= count) {
152                mGridCountX = Math.max(0, mGridCountX - 1);
153            }
154            done = mGridCountX == oldCountX && mGridCountY == oldCountY;
155        }
156
157        // Update grid size
158        for (int i = getPageCount() - 1; i >= 0; i--) {
159            getPageAt(i).setGridSize(mGridCountX, mGridCountY);
160        }
161    }
162
163    @Override
164    protected void dispatchDraw(Canvas canvas) {
165        mFocusIndicatorHelper.draw(canvas);
166        super.dispatchDraw(canvas);
167    }
168
169    /**
170     * Binds items to the layout.
171     * @return list of items that could not be bound, probably because we hit the max size limit.
172     */
173    public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
174        ArrayList<View> icons = new ArrayList<View>();
175        ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>();
176
177        for (ShortcutInfo item : items) {
178            if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) {
179                extra.add(item);
180            } else {
181                icons.add(createNewView(item));
182            }
183        }
184        arrangeChildren(icons, icons.size(), false);
185        return extra;
186    }
187
188    /**
189     * Create space for a new item at the end, and returns the rank for that item.
190     * Also sets the current page to the last page.
191     */
192    public int allocateRankForNewItem() {
193        int rank = getItemCount();
194        ArrayList<View> views = new ArrayList<>(mFolder.getItemsInReadingOrder());
195        views.add(rank, null);
196        arrangeChildren(views, views.size(), false);
197        setCurrentPage(rank / mMaxItemsPerPage);
198        return rank;
199    }
200
201    public View createAndAddViewForRank(ShortcutInfo item, int rank) {
202        View icon = createNewView(item);
203        addViewForRank(icon, item, rank);
204        return icon;
205    }
206
207    /**
208     * Adds the {@param view} to the layout based on {@param rank} and updated the position
209     * related attributes. It assumes that {@param item} is already attached to the view.
210     */
211    public void addViewForRank(View view, ShortcutInfo item, int rank) {
212        int pagePos = rank % mMaxItemsPerPage;
213        int pageNo = rank / mMaxItemsPerPage;
214
215        item.rank = rank;
216        item.cellX = pagePos % mGridCountX;
217        item.cellY = pagePos / mGridCountX;
218
219        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
220        lp.cellX = item.cellX;
221        lp.cellY = item.cellY;
222        getPageAt(pageNo).addViewToCellLayout(
223                view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
224    }
225
226    @SuppressLint("InflateParams")
227    public View createNewView(ShortcutInfo item) {
228        final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
229                R.layout.folder_application, null, false);
230        textView.applyFromShortcutInfo(item);
231        textView.setOnClickListener(mFolder);
232        textView.setOnLongClickListener(mFolder);
233        textView.setOnFocusChangeListener(mFocusIndicatorHelper);
234        textView.setOnKeyListener(mKeyListener);
235
236        textView.setLayoutParams(new CellLayout.LayoutParams(
237                item.cellX, item.cellY, item.spanX, item.spanY));
238        return textView;
239    }
240
241    @Override
242    public CellLayout getPageAt(int index) {
243        return (CellLayout) getChildAt(index);
244    }
245
246    public CellLayout getCurrentCellLayout() {
247        return getPageAt(getNextPage());
248    }
249
250    private CellLayout createAndAddNewPage() {
251        DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
252        CellLayout page = (CellLayout) mInflater.inflate(R.layout.folder_page, this, false);
253        page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
254        page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
255        page.setInvertIfRtl(true);
256        page.setGridSize(mGridCountX, mGridCountY);
257
258        addView(page, -1, generateDefaultLayoutParams());
259        return page;
260    }
261
262    @Override
263    protected int getChildGap() {
264        return getPaddingLeft() + getPaddingRight();
265    }
266
267    public void setFixedSize(int width, int height) {
268        width -= (getPaddingLeft() + getPaddingRight());
269        height -= (getPaddingTop() + getPaddingBottom());
270        for (int i = getChildCount() - 1; i >= 0; i --) {
271            ((CellLayout) getChildAt(i)).setFixedSize(width, height);
272        }
273    }
274
275    public void removeItem(View v) {
276        for (int i = getChildCount() - 1; i >= 0; i --) {
277            getPageAt(i).removeView(v);
278        }
279    }
280
281    @Override
282    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
283        super.onScrollChanged(l, t, oldl, oldt);
284        mPageIndicator.setScroll(l, mMaxScrollX);
285    }
286
287    /**
288     * Updates position and rank of all the children in the view.
289     * It essentially removes all views from all the pages and then adds them again in appropriate
290     * page.
291     *
292     * @param list the ordered list of children.
293     * @param itemCount if greater than the total children count, empty spaces are left
294     * at the end, otherwise it is ignored.
295     *
296     */
297    public void arrangeChildren(ArrayList<View> list, int itemCount) {
298        arrangeChildren(list, itemCount, true);
299    }
300
301    @SuppressLint("RtlHardcoded")
302    private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
303        ArrayList<CellLayout> pages = new ArrayList<CellLayout>();
304        for (int i = 0; i < getChildCount(); i++) {
305            CellLayout page = (CellLayout) getChildAt(i);
306            page.removeAllViews();
307            pages.add(page);
308        }
309        setupContentDimensions(itemCount);
310
311        Iterator<CellLayout> pageItr = pages.iterator();
312        CellLayout currentPage = null;
313
314        int position = 0;
315        int newX, newY, rank;
316
317        rank = 0;
318        for (int i = 0; i < itemCount; i++) {
319            View v = list.size() > i ? list.get(i) : null;
320            if (currentPage == null || position >= mMaxItemsPerPage) {
321                // Next page
322                if (pageItr.hasNext()) {
323                    currentPage = pageItr.next();
324                } else {
325                    currentPage = createAndAddNewPage();
326                }
327                position = 0;
328            }
329
330            if (v != null) {
331                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
332                newX = position % mGridCountX;
333                newY = position / mGridCountX;
334                ItemInfo info = (ItemInfo) v.getTag();
335                if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
336                    info.cellX = newX;
337                    info.cellY = newY;
338                    info.rank = rank;
339                    if (saveChanges) {
340                        mFolder.mLauncher.getModelWriter().addOrMoveItemInDatabase(info,
341                                mFolder.mInfo.id, 0, info.cellX, info.cellY);
342                    }
343                }
344                lp.cellX = info.cellX;
345                lp.cellY = info.cellY;
346                currentPage.addViewToCellLayout(
347                        v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
348
349                if (rank < FolderIcon.NUM_ITEMS_IN_PREVIEW && v instanceof BubbleTextView) {
350                    ((BubbleTextView) v).verifyHighRes();
351                }
352            }
353
354            rank ++;
355            position++;
356        }
357
358        // Remove extra views.
359        boolean removed = false;
360        while (pageItr.hasNext()) {
361            removeView(pageItr.next());
362            removed = true;
363        }
364        if (removed) {
365            setCurrentPage(0);
366        }
367
368        setEnableOverscroll(getPageCount() > 1);
369
370        // Update footer
371        mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
372        // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
373        mFolder.mFolderName.setGravity(getPageCount() > 1 ?
374                (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
375    }
376
377    public int getDesiredWidth() {
378        return getPageCount() > 0 ?
379                (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0;
380    }
381
382    public int getDesiredHeight()  {
383        return  getPageCount() > 0 ?
384                (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0;
385    }
386
387    public int getItemCount() {
388        int lastPageIndex = getChildCount() - 1;
389        if (lastPageIndex < 0) {
390            // If there are no pages, nothing has yet been added to the folder.
391            return 0;
392        }
393        return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount()
394                + lastPageIndex * mMaxItemsPerPage;
395    }
396
397    /**
398     * @return the rank of the cell nearest to the provided pixel position.
399     */
400    public int findNearestArea(int pixelX, int pixelY) {
401        int pageIndex = getNextPage();
402        CellLayout page = getPageAt(pageIndex);
403        page.findNearestArea(pixelX, pixelY, 1, 1, sTempPosArray);
404        if (mFolder.isLayoutRtl()) {
405            sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1;
406        }
407        return Math.min(mAllocatedContentSize - 1,
408                pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]);
409    }
410
411    public boolean isFull() {
412        return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage;
413    }
414
415    public View getFirstItem() {
416        if (getChildCount() < 1) {
417            return null;
418        }
419        ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets();
420        if (mGridCountX > 0) {
421            return currContainer.getChildAt(0, 0);
422        } else {
423            return currContainer.getChildAt(0);
424        }
425    }
426
427    public View getLastItem() {
428        if (getChildCount() < 1) {
429            return null;
430        }
431        ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets();
432        int lastRank = currContainer.getChildCount() - 1;
433        if (mGridCountX > 0) {
434            return currContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX);
435        } else {
436            return currContainer.getChildAt(lastRank);
437        }
438    }
439
440    /**
441     * Iterates over all its items in a reading order.
442     * @return the view for which the operator returned true.
443     */
444    public View iterateOverItems(ItemOperator op) {
445        for (int k = 0 ; k < getChildCount(); k++) {
446            CellLayout page = getPageAt(k);
447            for (int j = 0; j < page.getCountY(); j++) {
448                for (int i = 0; i < page.getCountX(); i++) {
449                    View v = page.getChildAt(i, j);
450                    if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v)) {
451                        return v;
452                    }
453                }
454            }
455        }
456        return null;
457    }
458
459    public String getAccessibilityDescription() {
460        return getContext().getString(R.string.folder_opened, mGridCountX, mGridCountY);
461    }
462
463    /**
464     * Sets the focus on the first visible child.
465     */
466    public void setFocusOnFirstChild() {
467        View firstChild = getCurrentCellLayout().getChildAt(0, 0);
468        if (firstChild != null) {
469            firstChild.requestFocus();
470        }
471    }
472
473    @Override
474    protected void notifyPageSwitchListener() {
475        super.notifyPageSwitchListener();
476        if (mFolder != null) {
477            mFolder.updateTextViewFocus();
478        }
479    }
480
481    /**
482     * Scrolls the current view by a fraction
483     */
484    public void showScrollHint(int direction) {
485        float fraction = (direction == Folder.SCROLL_LEFT) ^ mIsRtl
486                ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION;
487        int hint = (int) (fraction * getWidth());
488        int scroll = getScrollForPage(getNextPage()) + hint;
489        int delta = scroll - getScrollX();
490        if (delta != 0) {
491            mScroller.setInterpolator(new DecelerateInterpolator());
492            mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION);
493            invalidate();
494        }
495    }
496
497    public void clearScrollHint() {
498        if (getScrollX() != getScrollForPage(getNextPage())) {
499            snapToPage(getNextPage());
500        }
501    }
502
503    /**
504     * Finish animation all the views which are animating across pages
505     */
506    public void completePendingPageChanges() {
507        if (!mPendingAnimations.isEmpty()) {
508            HashMap<View, Runnable> pendingViews = new HashMap<>(mPendingAnimations);
509            for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) {
510                e.getKey().animate().cancel();
511                e.getValue().run();
512            }
513        }
514    }
515
516    public boolean rankOnCurrentPage(int rank) {
517        int p = rank / mMaxItemsPerPage;
518        return p == getNextPage();
519    }
520
521    @Override
522    protected void onPageBeginTransition() {
523        super.onPageBeginTransition();
524        // Ensure that adjacent pages have high resolution icons
525        verifyVisibleHighResIcons(getCurrentPage() - 1);
526        verifyVisibleHighResIcons(getCurrentPage() + 1);
527    }
528
529    /**
530     * Ensures that all the icons on the given page are of high-res
531     */
532    public void verifyVisibleHighResIcons(int pageNo) {
533        CellLayout page = getPageAt(pageNo);
534        if (page != null) {
535            ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
536            for (int i = parent.getChildCount() - 1; i >= 0; i--) {
537                ((BubbleTextView) parent.getChildAt(i)).verifyHighRes();
538            }
539        }
540    }
541
542    public int getAllocatedContentSize() {
543        return mAllocatedContentSize;
544    }
545
546    /**
547     * Reorders the items such that the {@param empty} spot moves to {@param target}
548     */
549    public void realTimeReorder(int empty, int target) {
550        completePendingPageChanges();
551        int delay = 0;
552        float delayAmount = START_VIEW_REORDER_DELAY;
553
554        // Animation only happens on the current page.
555        int pageToAnimate = getNextPage();
556
557        int pageT = target / mMaxItemsPerPage;
558        int pagePosT = target % mMaxItemsPerPage;
559
560        if (pageT != pageToAnimate) {
561            Log.e(TAG, "Cannot animate when the target cell is invisible");
562        }
563        int pagePosE = empty % mMaxItemsPerPage;
564        int pageE = empty / mMaxItemsPerPage;
565
566        int startPos, endPos;
567        int moveStart, moveEnd;
568        int direction;
569
570        if (target == empty) {
571            // No animation
572            return;
573        } else if (target > empty) {
574            // Items will move backwards to make room for the empty cell.
575            direction = 1;
576
577            // If empty cell is in a different page, move them instantly.
578            if (pageE < pageToAnimate) {
579                moveStart = empty;
580                // Instantly move the first item in the current page.
581                moveEnd = pageToAnimate * mMaxItemsPerPage;
582                // Animate the 2nd item in the current page, as the first item was already moved to
583                // the last page.
584                startPos = 0;
585            } else {
586                moveStart = moveEnd = -1;
587                startPos = pagePosE;
588            }
589
590            endPos = pagePosT;
591        } else {
592            // The items will move forward.
593            direction = -1;
594
595            if (pageE > pageToAnimate) {
596                // Move the items immediately.
597                moveStart = empty;
598                // Instantly move the last item in the current page.
599                moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1;
600
601                // Animations start with the second last item in the page
602                startPos = mMaxItemsPerPage - 1;
603            } else {
604                moveStart = moveEnd = -1;
605                startPos = pagePosE;
606            }
607
608            endPos = pagePosT;
609        }
610
611        // Instant moving views.
612        while (moveStart != moveEnd) {
613            int rankToMove = moveStart + direction;
614            int p = rankToMove / mMaxItemsPerPage;
615            int pagePos = rankToMove % mMaxItemsPerPage;
616            int x = pagePos % mGridCountX;
617            int y = pagePos / mGridCountX;
618
619            final CellLayout page = getPageAt(p);
620            final View v = page.getChildAt(x, y);
621            if (v != null) {
622                if (pageToAnimate != p) {
623                    page.removeView(v);
624                    addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart);
625                } else {
626                    // Do a fake animation before removing it.
627                    final int newRank = moveStart;
628                    final float oldTranslateX = v.getTranslationX();
629
630                    Runnable endAction = new Runnable() {
631
632                        @Override
633                        public void run() {
634                            mPendingAnimations.remove(v);
635                            v.setTranslationX(oldTranslateX);
636                            ((CellLayout) v.getParent().getParent()).removeView(v);
637                            addViewForRank(v, (ShortcutInfo) v.getTag(), newRank);
638                        }
639                    };
640                    v.animate()
641                        .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth())
642                        .setDuration(REORDER_ANIMATION_DURATION)
643                        .setStartDelay(0)
644                        .withEndAction(endAction);
645                    mPendingAnimations.put(v, endAction);
646                }
647            }
648            moveStart = rankToMove;
649        }
650
651        if ((endPos - startPos) * direction <= 0) {
652            // No animation
653            return;
654        }
655
656        CellLayout page = getPageAt(pageToAnimate);
657        for (int i = startPos; i != endPos; i += direction) {
658            int nextPos = i + direction;
659            View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
660            if (v != null) {
661                ((ItemInfo) v.getTag()).rank -= direction;
662            }
663            if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
664                    REORDER_ANIMATION_DURATION, delay, true, true)) {
665                delay += delayAmount;
666                delayAmount *= VIEW_REORDER_DELAY_FACTOR;
667            }
668        }
669    }
670
671    public int itemsPerPage() {
672        return mMaxItemsPerPage;
673    }
674
675    @Override
676    protected void getEdgeVerticalPosition(int[] pos) {
677        pos[0] = 0;
678        pos[1] = getViewportHeight();
679    }
680}
681