/** * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.OvershootInterpolator; import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; import com.android.launcher3.PageIndicator.PageMarkerResources; import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class FolderPagedView extends PagedView { private static final String TAG = "FolderPagedView"; private static final boolean ALLOW_FOLDER_SCROLL = true; private static final int REORDER_ANIMATION_DURATION = 230; private static final int START_VIEW_REORDER_DELAY = 30; private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f; private static final int PAGE_INDICATOR_ANIMATION_START_DELAY = 300; private static final int PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY = 150; private static final int PAGE_INDICATOR_ANIMATION_DURATION = 400; // This value approximately overshoots to 1.5 times the original size. private static final float PAGE_INDICATOR_OVERSHOOT_TENSION = 4.9f; /** * Fraction of the width to scroll when showing the next page hint. */ private static final float SCROLL_HINT_FRACTION = 0.07f; private static final int[] sTempPosArray = new int[2]; public final boolean mIsRtl; private final LayoutInflater mInflater; private final IconCache mIconCache; @Thunk final HashMap mPendingAnimations = new HashMap<>(); private final int mMaxCountX; private final int mMaxCountY; private final int mMaxItemsPerPage; private int mAllocatedContentSize; private int mGridCountX; private int mGridCountY; private Folder mFolder; private FocusIndicatorView mFocusIndicatorView; private PagedFolderKeyEventListener mKeyListener; private PageIndicator mPageIndicator; public FolderPagedView(Context context, AttributeSet attrs) { super(context, attrs); LauncherAppState app = LauncherAppState.getInstance(); InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); mMaxCountX = (int) profile.numFolderColumns; mMaxCountY = (int) profile.numFolderRows; mMaxItemsPerPage = mMaxCountX * mMaxCountY; mInflater = LayoutInflater.from(context); mIconCache = app.getIconCache(); mIsRtl = Utilities.isRtl(getResources()); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } public void setFolder(Folder folder) { mFolder = folder; mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator); mKeyListener = new PagedFolderKeyEventListener(folder); mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator); } /** * Sets up the grid size such that {@param count} items can fit in the grid. * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}. */ private void setupContentDimensions(int count) { mAllocatedContentSize = count; boolean done; if (count >= mMaxItemsPerPage) { mGridCountX = mMaxCountX; mGridCountY = mMaxCountY; done = true; } else { done = false; } while (!done) { int oldCountX = mGridCountX; int oldCountY = mGridCountY; if (mGridCountX * mGridCountY < count) { // Current grid is too small, expand it if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) { mGridCountX++; } else if (mGridCountY < mMaxCountY) { mGridCountY++; } if (mGridCountY == 0) mGridCountY++; } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) { mGridCountY = Math.max(0, mGridCountY - 1); } else if ((mGridCountX - 1) * mGridCountY >= count) { mGridCountX = Math.max(0, mGridCountX - 1); } done = mGridCountX == oldCountX && mGridCountY == oldCountY; } // Update grid size for (int i = getPageCount() - 1; i >= 0; i--) { getPageAt(i).setGridSize(mGridCountX, mGridCountY); } } /** * Binds items to the layout. * @return list of items that could not be bound, probably because we hit the max size limit. */ public ArrayList bindItems(ArrayList items) { ArrayList icons = new ArrayList(); ArrayList extra = new ArrayList(); for (ShortcutInfo item : items) { if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) { extra.add(item); } else { icons.add(createNewView(item)); } } arrangeChildren(icons, icons.size(), false); return extra; } /** * Create space for a new item at the end, and returns the rank for that item. * Also sets the current page to the last page. */ public int allocateRankForNewItem(ShortcutInfo info) { int rank = getItemCount(); ArrayList views = new ArrayList(mFolder.getItemsInReadingOrder()); views.add(rank, null); arrangeChildren(views, views.size(), false); setCurrentPage(rank / mMaxItemsPerPage); return rank; } public View createAndAddViewForRank(ShortcutInfo item, int rank) { View icon = createNewView(item); addViewForRank(icon, item, rank); return icon; } /** * Adds the {@param view} to the layout based on {@param rank} and updated the position * related attributes. It assumes that {@param item} is already attached to the view. */ public void addViewForRank(View view, ShortcutInfo item, int rank) { int pagePos = rank % mMaxItemsPerPage; int pageNo = rank / mMaxItemsPerPage; item.rank = rank; item.cellX = pagePos % mGridCountX; item.cellY = pagePos / mGridCountX; CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); lp.cellX = item.cellX; lp.cellY = item.cellY; getPageAt(pageNo).addViewToCellLayout( view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true); } @SuppressLint("InflateParams") public View createNewView(ShortcutInfo item) { final BubbleTextView textView = (BubbleTextView) mInflater.inflate( R.layout.folder_application, null, false); textView.applyFromShortcutInfo(item, mIconCache); textView.setOnClickListener(mFolder); textView.setOnLongClickListener(mFolder); textView.setOnFocusChangeListener(mFocusIndicatorView); textView.setOnKeyListener(mKeyListener); textView.setLayoutParams(new CellLayout.LayoutParams( item.cellX, item.cellY, item.spanX, item.spanY)); return textView; } @Override public CellLayout getPageAt(int index) { return (CellLayout) getChildAt(index); } public void removeCellLayoutView(View view) { for (int i = getChildCount() - 1; i >= 0; i --) { getPageAt(i).removeView(view); } } public CellLayout getCurrentCellLayout() { return getPageAt(getNextPage()); } private CellLayout createAndAddNewPage() { DeviceProfile grid = ((Launcher) getContext()).getDeviceProfile(); CellLayout page = new CellLayout(getContext()); page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); page.setInvertIfRtl(true); page.setGridSize(mGridCountX, mGridCountY); addView(page, -1, generateDefaultLayoutParams()); return page; } @Override protected int getChildGap() { return getPaddingLeft() + getPaddingRight(); } public void setFixedSize(int width, int height) { width -= (getPaddingLeft() + getPaddingRight()); height -= (getPaddingTop() + getPaddingBottom()); for (int i = getChildCount() - 1; i >= 0; i --) { ((CellLayout) getChildAt(i)).setFixedSize(width, height); } } public void removeItem(View v) { for (int i = getChildCount() - 1; i >= 0; i --) { getPageAt(i).removeView(v); } } /** * Updates position and rank of all the children in the view. * It essentially removes all views from all the pages and then adds them again in appropriate * page. * * @param list the ordered list of children. * @param itemCount if greater than the total children count, empty spaces are left * at the end, otherwise it is ignored. * */ public void arrangeChildren(ArrayList list, int itemCount) { arrangeChildren(list, itemCount, true); } @SuppressLint("RtlHardcoded") private void arrangeChildren(ArrayList list, int itemCount, boolean saveChanges) { ArrayList pages = new ArrayList(); for (int i = 0; i < getChildCount(); i++) { CellLayout page = (CellLayout) getChildAt(i); page.removeAllViews(); pages.add(page); } setupContentDimensions(itemCount); Iterator pageItr = pages.iterator(); CellLayout currentPage = null; int position = 0; int newX, newY, rank; rank = 0; for (int i = 0; i < itemCount; i++) { View v = list.size() > i ? list.get(i) : null; if (currentPage == null || position >= mMaxItemsPerPage) { // Next page if (pageItr.hasNext()) { currentPage = pageItr.next(); } else { currentPage = createAndAddNewPage(); } position = 0; } if (v != null) { CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); newX = position % mGridCountX; newY = position / mGridCountX; ItemInfo info = (ItemInfo) v.getTag(); if (info.cellX != newX || info.cellY != newY || info.rank != rank) { info.cellX = newX; info.cellY = newY; info.rank = rank; if (saveChanges) { LauncherModel.addOrMoveItemInDatabase(getContext(), info, mFolder.mInfo.id, 0, info.cellX, info.cellY); } } lp.cellX = info.cellX; lp.cellY = info.cellY; currentPage.addViewToCellLayout( v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); } rank ++; position++; } // Remove extra views. boolean removed = false; while (pageItr.hasNext()) { removeView(pageItr.next()); removed = true; } if (removed) { setCurrentPage(0); } setEnableOverscroll(getPageCount() > 1); // Update footer mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text. mFolder.mFolderName.setGravity(getPageCount() > 1 ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL); } public int getDesiredWidth() { return getPageCount() > 0 ? (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0; } public int getDesiredHeight() { return getPageCount() > 0 ? (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0; } public int getItemCount() { int lastPageIndex = getChildCount() - 1; if (lastPageIndex < 0) { // If there are no pages, nothing has yet been added to the folder. return 0; } return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount() + lastPageIndex * mMaxItemsPerPage; } /** * @return the rank of the cell nearest to the provided pixel position. */ public int findNearestArea(int pixelX, int pixelY) { int pageIndex = getNextPage(); CellLayout page = getPageAt(pageIndex); page.findNearestArea(pixelX, pixelY, 1, 1, sTempPosArray); if (mFolder.isLayoutRtl()) { sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1; } return Math.min(mAllocatedContentSize - 1, pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]); } @Override protected PageMarkerResources getPageIndicatorMarker(int pageIndex) { return new PageMarkerResources(R.drawable.ic_pageindicator_current_folder, R.drawable.ic_pageindicator_default_folder); } public boolean isFull() { return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage; } public View getLastItem() { if (getChildCount() < 1) { return null; } ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets(); int lastRank = lastContainer.getChildCount() - 1; if (mGridCountX > 0) { return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX); } else { return lastContainer.getChildAt(lastRank); } } /** * Iterates over all its items in a reading order. * @return the view for which the operator returned true. */ public View iterateOverItems(ItemOperator op) { for (int k = 0 ; k < getChildCount(); k++) { CellLayout page = getPageAt(k); for (int j = 0; j < page.getCountY(); j++) { for (int i = 0; i < page.getCountX(); i++) { View v = page.getChildAt(i, j); if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) { return v; } } } } return null; } public String getAccessibilityDescription() { return String.format(getContext().getString(R.string.folder_opened), mGridCountX, mGridCountY); } /** * Sets the focus on the first visible child. */ public void setFocusOnFirstChild() { View firstChild = getCurrentCellLayout().getChildAt(0, 0); if (firstChild != null) { firstChild.requestFocus(); } } @Override protected void notifyPageSwitchListener() { super.notifyPageSwitchListener(); if (mFolder != null) { mFolder.updateTextViewFocus(); } } /** * Scrolls the current view by a fraction */ public void showScrollHint(int direction) { float fraction = (direction == DragController.SCROLL_LEFT) ^ mIsRtl ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION; int hint = (int) (fraction * getWidth()); int scroll = getScrollForPage(getNextPage()) + hint; int delta = scroll - mUnboundedScrollX; if (delta != 0) { mScroller.setInterpolator(new DecelerateInterpolator()); mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, Folder.SCROLL_HINT_DURATION); invalidate(); } } public void clearScrollHint() { if (mUnboundedScrollX != getScrollForPage(getNextPage())) { snapToPage(getNextPage()); } } /** * Finish animation all the views which are animating across pages */ public void completePendingPageChanges() { if (!mPendingAnimations.isEmpty()) { HashMap pendingViews = new HashMap<>(mPendingAnimations); for (Map.Entry e : pendingViews.entrySet()) { e.getKey().animate().cancel(); e.getValue().run(); } } } public boolean rankOnCurrentPage(int rank) { int p = rank / mMaxItemsPerPage; return p == getNextPage(); } @Override protected void onPageBeginMoving() { super.onPageBeginMoving(); getVisiblePages(sTempPosArray); for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) { verifyVisibleHighResIcons(i); } } /** * Ensures that all the icons on the given page are of high-res */ public void verifyVisibleHighResIcons(int pageNo) { CellLayout page = getPageAt(pageNo); if (page != null) { ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets(); for (int i = parent.getChildCount() - 1; i >= 0; i--) { ((BubbleTextView) parent.getChildAt(i)).verifyHighRes(); } } } public int getAllocatedContentSize() { return mAllocatedContentSize; } /** * Reorders the items such that the {@param empty} spot moves to {@param target} */ public void realTimeReorder(int empty, int target) { completePendingPageChanges(); int delay = 0; float delayAmount = START_VIEW_REORDER_DELAY; // Animation only happens on the current page. int pageToAnimate = getNextPage(); int pageT = target / mMaxItemsPerPage; int pagePosT = target % mMaxItemsPerPage; if (pageT != pageToAnimate) { Log.e(TAG, "Cannot animate when the target cell is invisible"); } int pagePosE = empty % mMaxItemsPerPage; int pageE = empty / mMaxItemsPerPage; int startPos, endPos; int moveStart, moveEnd; int direction; if (target == empty) { // No animation return; } else if (target > empty) { // Items will move backwards to make room for the empty cell. direction = 1; // If empty cell is in a different page, move them instantly. if (pageE < pageToAnimate) { moveStart = empty; // Instantly move the first item in the current page. moveEnd = pageToAnimate * mMaxItemsPerPage; // Animate the 2nd item in the current page, as the first item was already moved to // the last page. startPos = 0; } else { moveStart = moveEnd = -1; startPos = pagePosE; } endPos = pagePosT; } else { // The items will move forward. direction = -1; if (pageE > pageToAnimate) { // Move the items immediately. moveStart = empty; // Instantly move the last item in the current page. moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1; // Animations start with the second last item in the page startPos = mMaxItemsPerPage - 1; } else { moveStart = moveEnd = -1; startPos = pagePosE; } endPos = pagePosT; } // Instant moving views. while (moveStart != moveEnd) { int rankToMove = moveStart + direction; int p = rankToMove / mMaxItemsPerPage; int pagePos = rankToMove % mMaxItemsPerPage; int x = pagePos % mGridCountX; int y = pagePos / mGridCountX; final CellLayout page = getPageAt(p); final View v = page.getChildAt(x, y); if (v != null) { if (pageToAnimate != p) { page.removeView(v); addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart); } else { // Do a fake animation before removing it. final int newRank = moveStart; final float oldTranslateX = v.getTranslationX(); Runnable endAction = new Runnable() { @Override public void run() { mPendingAnimations.remove(v); v.setTranslationX(oldTranslateX); ((CellLayout) v.getParent().getParent()).removeView(v); addViewForRank(v, (ShortcutInfo) v.getTag(), newRank); } }; v.animate() .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth()) .setDuration(REORDER_ANIMATION_DURATION) .setStartDelay(0) .withEndAction(endAction); mPendingAnimations.put(v, endAction); } } moveStart = rankToMove; } if ((endPos - startPos) * direction <= 0) { // No animation return; } CellLayout page = getPageAt(pageToAnimate); for (int i = startPos; i != endPos; i += direction) { int nextPos = i + direction; View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX); if (v != null) { ((ItemInfo) v.getTag()).rank -= direction; } if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX, REORDER_ANIMATION_DURATION, delay, true, true)) { delay += delayAmount; delayAmount *= VIEW_REORDER_DELAY_FACTOR; } } } public void setMarkerScale(float scale) { int count = mPageIndicator.getChildCount(); for (int i = 0; i < count; i++) { View marker = mPageIndicator.getChildAt(i); marker.animate().cancel(); marker.setScaleX(scale); marker.setScaleY(scale); } } public void animateMarkers() { int count = mPageIndicator.getChildCount(); Interpolator interpolator = new OvershootInterpolator(PAGE_INDICATOR_OVERSHOOT_TENSION); for (int i = 0; i < count; i++) { mPageIndicator.getChildAt(i).animate().scaleX(1).scaleY(1) .setInterpolator(interpolator) .setDuration(PAGE_INDICATOR_ANIMATION_DURATION) .setStartDelay(PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY * i + PAGE_INDICATOR_ANIMATION_START_DELAY); } } public int itemsPerPage() { return mMaxItemsPerPage; } }