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