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