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