2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.launcher3;
19 import android.annotation.SuppressLint;
20 import android.content.Context;
21 import android.graphics.Point;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.view.Gravity;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.animation.DecelerateInterpolator;
28 import android.view.animation.Interpolator;
29 import android.view.animation.OvershootInterpolator;
31 import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
32 import com.android.launcher3.PageIndicator.PageMarkerResources;
33 import com.android.launcher3.Workspace.ItemOperator;
34 import com.android.launcher3.settings.SettingsProvider;
35 import com.android.launcher3.util.Thunk;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.Iterator;
42 public class FolderPagedView extends PagedView {
44 private static final String TAG = "FolderPagedView";
46 private static final boolean ALLOW_FOLDER_SCROLL = true;
48 private static final int REORDER_ANIMATION_DURATION = 230;
49 private static final int START_VIEW_REORDER_DELAY = 30;
50 private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
52 private static final int PAGE_INDICATOR_ANIMATION_START_DELAY = 300;
53 private static final int PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY = 150;
54 private static final int PAGE_INDICATOR_ANIMATION_DURATION = 400;
56 // This value approximately overshoots to 1.5 times the original size.
57 private static final float PAGE_INDICATOR_OVERSHOOT_TENSION = 4.9f;
60 * Fraction of the width to scroll when showing the next page hint.
62 private static final float SCROLL_HINT_FRACTION = 0.07f;
64 private static final int[] sTempPosArray = new int[2];
66 public final boolean mIsRtl;
68 private final LayoutInflater mInflater;
69 private final IconCache mIconCache;
71 @Thunk final HashMap<View, Runnable> mPendingAnimations = new HashMap<>();
73 private final int mMaxCountX;
74 private final int mMaxCountY;
75 private final int mMaxItemsPerPage;
77 private int mAllocatedContentSize;
78 private int mGridCountX;
79 private int mGridCountY;
81 private Folder mFolder;
82 private FocusIndicatorView mFocusIndicatorView;
83 private PagedFolderKeyEventListener mKeyListener;
85 private PageIndicator mPageIndicator;
87 public FolderPagedView(Context context, AttributeSet attrs) {
88 super(context, attrs);
89 LauncherAppState app = LauncherAppState.getInstance();
91 InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
92 mMaxCountX = profile.numFolderColumns;
93 mMaxCountY = profile.numFolderRows;
95 mMaxItemsPerPage = mMaxCountX * mMaxCountY;
97 mInflater = LayoutInflater.from(context);
98 mIconCache = app.getIconCache();
100 mIsRtl = Utilities.isRtl(getResources());
101 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
103 setEdgeGlowColor(getResources().getColor(R.color.folder_edge_effect_color));
106 public void setFolder(Folder folder) {
108 mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
109 mKeyListener = new PagedFolderKeyEventListener(folder);
110 mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator);
114 * Sets up the grid size such that {@param count} items can fit in the grid.
115 * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
116 * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}.
118 public void setupContentDimensions(int count) {
119 mAllocatedContentSize = count;
120 Point point = Utilities.caluclateFolderContentDimensions(count, mMaxCountX, mMaxCountY);
121 mGridCountX = point.x;
122 mGridCountY = point.y;
124 for (int i = getPageCount() - 1; i >= 0; i--) {
125 getPageAt(i).setGridSize(mGridCountX, mGridCountY);
130 * Binds items to the layout.
131 * @return list of items that could not be bound, probably because we hit the max size limit.
133 public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
134 ArrayList<View> icons = new ArrayList<View>();
135 ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>();
137 for (ShortcutInfo item : items) {
138 if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) {
141 icons.add(createNewView(item));
144 arrangeChildren(icons, icons.size(), false);
149 * Create space for a new item at the end, and returns the rank for that item.
150 * Also sets the current page to the last page.
152 public int allocateRankForNewItem(ShortcutInfo info) {
153 int rank = getItemCount();
154 ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder());
155 views.add(rank, null);
156 arrangeChildren(views, views.size(), false);
157 setCurrentPage(rank / mMaxItemsPerPage);
161 public View createAndAddViewForRank(ShortcutInfo item, int rank) {
162 View icon = createNewView(item);
163 addViewForRank(icon, item, rank);
168 * Adds the {@param view} to the layout based on {@param rank} and updated the position
169 * related attributes. It assumes that {@param item} is already attached to the view.
171 public void addViewForRank(View view, ShortcutInfo item, int rank) {
172 int pagePos = rank % mMaxItemsPerPage;
173 int pageNo = rank / mMaxItemsPerPage;
176 item.cellX = pagePos % mGridCountX;
177 item.cellY = pagePos / mGridCountX;
179 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
180 lp.cellX = item.cellX;
181 lp.cellY = item.cellY;
182 getPageAt(pageNo).addViewToCellLayout(
183 view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
186 @SuppressLint("InflateParams")
187 public View createNewView(ShortcutInfo item) {
188 final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
189 R.layout.folder_application, null, false);
190 textView.applyFromShortcutInfo(item, mIconCache);
191 textView.setOnClickListener(mFolder);
192 textView.setOnLongClickListener(mFolder);
193 textView.setOnFocusChangeListener(mFocusIndicatorView);
194 if (SettingsProvider.getBoolean(mFolder.mLauncher,
195 SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS,
196 R.bool.preferences_interface_homescreen_hide_icon_labels_default)) {
197 textView.setTextVisibility(false);
199 textView.setOnKeyListener(mKeyListener);
201 textView.setLayoutParams(new CellLayout.LayoutParams(
202 item.cellX, item.cellY, item.spanX, item.spanY));
207 public CellLayout getPageAt(int index) {
208 return (CellLayout) getChildAt(index);
211 public void removeCellLayoutView(View view) {
212 for (int i = getChildCount() - 1; i >= 0; i --) {
213 getPageAt(i).removeView(view);
217 public CellLayout getCurrentCellLayout() {
218 return getPageAt(getNextPage());
221 private CellLayout createAndAddNewPage() {
222 DeviceProfile grid = ((Launcher) getContext()).getDeviceProfile();
223 CellLayout page = new CellLayout(getContext());
224 page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
225 page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
226 page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
227 page.setInvertIfRtl(true);
228 page.setGridSize(mGridCountX, mGridCountY);
230 addView(page, -1, generateDefaultLayoutParams());
235 protected int getChildGap() {
236 return getPaddingLeft() + getPaddingRight();
239 public void setFixedSize(int width, int height) {
240 width -= (getPaddingLeft() + getPaddingRight());
241 height -= (getPaddingTop() + getPaddingBottom());
242 for (int i = getChildCount() - 1; i >= 0; i --) {
243 ((CellLayout) getChildAt(i)).setFixedSize(width, height);
247 public void removeItem(View v) {
248 for (int i = getChildCount() - 1; i >= 0; i --) {
249 getPageAt(i).removeView(v);
253 public void removeAllItems() {
254 for (int i = 0; i < getChildCount(); i++) {
255 getPageAt(i).removeAllViews();
260 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
261 if (getChildCount() == 0) {
262 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
264 // We should only be as large as our pages, so measure all of them first.
266 for (int i = 0; i < getChildCount(); i++) {
267 page = getChildAt(i);
268 page.measure(widthMeasureSpec, heightMeasureSpec);
271 // And then set ourselves to their size.
272 int width = getPaddingLeft() + page.getMeasuredWidth() + getPaddingRight();
273 int height = getPaddingTop() + page.getMeasuredHeight() + getPaddingBottom();
274 mViewport.set(0, 0, width, height);
275 setMeasuredDimension(width, height);
280 * Find the child view for the given rank.
281 * @param rank sorted index of child.
282 * @return view of child at given rank.
284 public View getChildAtRank(int rank) {
285 int pagePos = rank % mMaxItemsPerPage;
286 int pageNo = rank / mMaxItemsPerPage;
287 int cellX = pagePos % mGridCountX;
288 int cellY = pagePos / mGridCountX;
290 CellLayout page = getPageAt(pageNo);
292 return page.getChildAt(cellX, cellY);
299 * Updates position and rank of all the children in the view.
300 * It essentially removes all views from all the pages and then adds them again in appropriate
303 * @param list the ordered list of children.
304 * @param itemCount if greater than the total children count, empty spaces are left
305 * at the end, otherwise it is ignored.
308 public void arrangeChildren(ArrayList<View> list, int itemCount) {
309 arrangeChildren(list, itemCount, true);
312 @SuppressLint("RtlHardcoded")
313 private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
314 ArrayList<CellLayout> pages = new ArrayList<CellLayout>();
315 for (int i = 0; i < getChildCount(); i++) {
316 CellLayout page = (CellLayout) getChildAt(i);
317 page.removeAllViews();
320 setupContentDimensions(itemCount);
322 Iterator<CellLayout> pageItr = pages.iterator();
323 CellLayout currentPage = null;
326 int newX, newY, rank;
329 for (int i = 0; i < itemCount; i++) {
330 View v = list.size() > i ? list.get(i) : null;
331 if (currentPage == null || position >= mMaxItemsPerPage) {
333 if (pageItr.hasNext()) {
334 currentPage = pageItr.next();
336 currentPage = createAndAddNewPage();
342 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
343 newX = position % mGridCountX;
344 newY = position / mGridCountX;
345 ItemInfo info = (ItemInfo) v.getTag();
346 if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
351 LauncherModel.addOrMoveItemInDatabase(getContext(), info,
352 mFolder.mInfo.id, 0, info.cellX, info.cellY);
355 lp.cellX = info.cellX;
356 lp.cellY = info.cellY;
357 currentPage.addViewToCellLayout(
358 v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
360 if (rank < FolderIcon.NUM_ITEMS_IN_PREVIEW && v instanceof BubbleTextView) {
361 ((BubbleTextView) v).verifyHighRes();
369 // Remove extra views.
370 boolean removed = false;
371 while (pageItr.hasNext()) {
372 removeView(pageItr.next());
379 setEnableOverscroll(getPageCount() > 1);
382 mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
383 // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
384 mFolder.mFolderName.setGravity(getPageCount() > 1 ?
385 (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
388 public int getDesiredWidth() {
389 return getPageCount() > 0 ?
390 (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0;
393 public int getDesiredHeight() {
394 return getPageCount() > 0 ?
395 (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0;
398 public int getItemCount() {
399 int lastPageIndex = getChildCount() - 1;
400 if (lastPageIndex < 0) {
401 // If there are no pages, nothing has yet been added to the folder.
404 return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount()
405 + lastPageIndex * mMaxItemsPerPage;
409 * @return the rank of the cell nearest to the provided pixel position.
411 public int findNearestArea(int pixelX, int pixelY) {
412 int pageIndex = getNextPage();
413 CellLayout page = getPageAt(pageIndex);
414 page.findNearestArea(pixelX, pixelY, 1, 1, sTempPosArray);
415 if (mFolder.isLayoutRtl()) {
416 sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1;
418 return Math.min(mAllocatedContentSize - 1,
419 pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]);
423 protected PageMarkerResources getPageIndicatorMarker(int pageIndex) {
424 return new PageMarkerResources(R.drawable.ic_pageindicator_current_folder,
425 R.drawable.ic_pageindicator_default_folder);
428 public boolean isFull() {
429 return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage;
432 public View getLastItem() {
433 if (getChildCount() < 1) {
436 ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets();
437 int lastRank = lastContainer.getChildCount() - 1;
438 if (mGridCountX > 0) {
439 return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX);
441 return lastContainer.getChildAt(lastRank);
446 * Iterates over all its items in a reading order.
447 * @return the view for which the operator returned true.
449 public View iterateOverItems(ItemOperator op) {
450 for (int k = 0 ; k < getChildCount(); k++) {
451 CellLayout page = getPageAt(k);
452 for (int j = 0; j < page.getCountY(); j++) {
453 for (int i = 0; i < page.getCountX(); i++) {
454 View v = page.getChildAt(i, j);
455 if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) {
464 public String getAccessibilityDescription() {
465 return String.format(getContext().getString(R.string.folder_opened),
466 mGridCountX, mGridCountY);
470 * Sets the focus on the first visible child.
472 public void setFocusOnFirstChild() {
473 View firstChild = getCurrentCellLayout().getChildAt(0, 0);
474 if (firstChild != null) {
475 firstChild.requestFocus();
480 protected void notifyPageSwitchListener() {
481 super.notifyPageSwitchListener();
482 if (mFolder != null) {
483 mFolder.updateTextViewFocus();
488 * Scrolls the current view by a fraction
490 public void showScrollHint(int direction) {
491 float fraction = (direction == DragController.SCROLL_LEFT) ^ mIsRtl
492 ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION;
493 int hint = (int) (fraction * getWidth());
494 int scroll = getScrollForPage(getNextPage()) + hint;
495 int delta = scroll - getScrollX();
497 mScroller.setInterpolator(new DecelerateInterpolator());
498 mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION);
503 public void clearScrollHint() {
504 if (getScrollX() != getScrollForPage(getNextPage())) {
505 snapToPage(getNextPage());
510 * Finish animation all the views which are animating across pages
512 public void completePendingPageChanges() {
513 if (!mPendingAnimations.isEmpty()) {
514 HashMap<View, Runnable> pendingViews = new HashMap<>(mPendingAnimations);
515 for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) {
516 e.getKey().animate().cancel();
522 public boolean rankOnCurrentPage(int rank) {
523 int p = rank / mMaxItemsPerPage;
524 return p == getNextPage();
528 protected void onPageBeginMoving() {
529 super.onPageBeginMoving();
530 getVisiblePages(sTempPosArray);
531 for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) {
532 verifyVisibleHighResIcons(i);
537 * Ensures that all the icons on the given page are of high-res
539 public void verifyVisibleHighResIcons(int pageNo) {
540 CellLayout page = getPageAt(pageNo);
542 ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
543 for (int i = parent.getChildCount() - 1; i >= 0; i--) {
544 ((BubbleTextView) parent.getChildAt(i)).verifyHighRes();
549 public int getAllocatedContentSize() {
550 return mAllocatedContentSize;
554 * Reorders the items such that the {@param empty} spot moves to {@param target}
556 public void realTimeReorder(int empty, int target) {
557 completePendingPageChanges();
559 float delayAmount = START_VIEW_REORDER_DELAY;
561 // Animation only happens on the current page.
562 int pageToAnimate = getNextPage();
564 int pageT = target / mMaxItemsPerPage;
565 int pagePosT = target % mMaxItemsPerPage;
567 if (pageT != pageToAnimate) {
568 Log.e(TAG, "Cannot animate when the target cell is invisible");
570 int pagePosE = empty % mMaxItemsPerPage;
571 int pageE = empty / mMaxItemsPerPage;
573 int startPos, endPos;
574 int moveStart, moveEnd;
577 if (target == empty) {
580 } else if (target > empty) {
581 // Items will move backwards to make room for the empty cell.
584 // If empty cell is in a different page, move them instantly.
585 if (pageE < pageToAnimate) {
587 // Instantly move the first item in the current page.
588 moveEnd = pageToAnimate * mMaxItemsPerPage;
589 // Animate the 2nd item in the current page, as the first item was already moved to
593 moveStart = moveEnd = -1;
599 // The items will move forward.
602 if (pageE > pageToAnimate) {
603 // Move the items immediately.
605 // Instantly move the last item in the current page.
606 moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1;
608 // Animations start with the second last item in the page
609 startPos = mMaxItemsPerPage - 1;
611 moveStart = moveEnd = -1;
618 // Instant moving views.
619 while (moveStart != moveEnd) {
620 int rankToMove = moveStart + direction;
621 int p = rankToMove / mMaxItemsPerPage;
622 int pagePos = rankToMove % mMaxItemsPerPage;
623 int x = pagePos % mGridCountX;
624 int y = pagePos / mGridCountX;
626 final CellLayout page = getPageAt(p);
627 final View v = page.getChildAt(x, y);
629 if (pageToAnimate != p) {
631 addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart);
633 // Do a fake animation before removing it.
634 final int newRank = moveStart;
635 final float oldTranslateX = v.getTranslationX();
637 Runnable endAction = new Runnable() {
641 mPendingAnimations.remove(v);
642 v.setTranslationX(oldTranslateX);
643 ((CellLayout) v.getParent().getParent()).removeView(v);
644 addViewForRank(v, (ShortcutInfo) v.getTag(), newRank);
648 .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth())
649 .setDuration(REORDER_ANIMATION_DURATION)
651 .withEndAction(endAction);
652 mPendingAnimations.put(v, endAction);
655 moveStart = rankToMove;
658 if ((endPos - startPos) * direction <= 0) {
663 CellLayout page = getPageAt(pageToAnimate);
664 for (int i = startPos; i != endPos; i += direction) {
665 int nextPos = i + direction;
666 View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
668 ((ItemInfo) v.getTag()).rank -= direction;
670 if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
671 REORDER_ANIMATION_DURATION, delay, true, true)) {
672 delay += delayAmount;
673 delayAmount *= VIEW_REORDER_DELAY_FACTOR;
678 public void setMarkerScale(float scale) {
679 int count = mPageIndicator.getChildCount();
680 for (int i = 0; i < count; i++) {
681 View marker = mPageIndicator.getChildAt(i);
682 marker.animate().cancel();
683 marker.setScaleX(scale);
684 marker.setScaleY(scale);
688 public void animateMarkers() {
689 int count = mPageIndicator.getChildCount();
690 Interpolator interpolator = new OvershootInterpolator(PAGE_INDICATOR_OVERSHOOT_TENSION);
691 for (int i = 0; i < count; i++) {
692 mPageIndicator.getChildAt(i).animate().scaleX(1).scaleY(1)
693 .setInterpolator(interpolator)
694 .setDuration(PAGE_INDICATOR_ANIMATION_DURATION)
695 .setStartDelay(PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY * i
696 + PAGE_INDICATOR_ANIMATION_START_DELAY);
700 public int itemsPerPage() {
701 return mMaxItemsPerPage;
705 protected void getEdgeVerticalPostion(int[] pos) {
707 pos[1] = getViewportHeight();