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