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