From 4846193300245c8c0a1f9bde3175f273df044309 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 9 Mar 2015 17:41:09 -0700 Subject: Scrolling folder during drag-drop > Show a scroll hint (partial scroll) when the icon is over the last icon (some fraction) > Automatically scroll the folder if the user stays in that position for some time > Rearrance the icons on the new page only after the scroll animaiton is complete Change-Id: I7a2dd85ab23802d647801686df069975d197cd39 --- src/com/android/launcher3/DragController.java | 4 +- src/com/android/launcher3/Folder.java | 192 ++++++++++++++++++++++++- src/com/android/launcher3/FolderPagedView.java | 100 +++++++++---- 3 files changed, 261 insertions(+), 35 deletions(-) diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 09c59a057..8dc6e185c 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -49,8 +49,8 @@ public class DragController { /** Indicates the drag is a copy. */ public static int DRAG_ACTION_COPY = 1; - private static final int SCROLL_DELAY = 500; - private static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; + public static final int SCROLL_DELAY = 500; + public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index bef1f0da4..7ff60de4f 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -48,6 +48,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; import android.widget.TextView; + import com.android.launcher3.FolderInfo.FolderListener; import com.android.launcher3.Workspace.ItemOperator; @@ -74,6 +75,21 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList static final int STATE_ANIMATING = 1; static final int STATE_OPEN = 2; + /** + * Fraction of the width to scroll when showing the next page hint. + */ + private static final float SCROLL_HINT_FRACTION = 0.07f; + + /** + * Time for which the scroll hint is shown before automatically changing page. + */ + public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; + + /** + * Fraction of icon width which behave as scroll region. + */ + private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; + private static final int REORDER_DELAY = 250; private static final int ON_EXIT_CLOSE_DELAY = 400; private static final Rect sTempRect = new Rect(); @@ -129,6 +145,17 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; + // Folder scrolling + private int mScrollAreaOffset; + private Alarm mOnScrollHintAlarm; + private Alarm mScrollPauseAlarm; + + // TODO: Use {@link #mContent} once {@link #ALLOW_FOLDER_SCROLL} is removed. + private FolderPagedView mPagedView; + + private int mScrollHintDir = DragController.SCROLL_NONE; + private int mCurrentScrollDir = DragController.SCROLL_NONE; + /** * Used to inflate the Workspace from XML. * @@ -157,6 +184,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // name is complete, we have something to focus on, thus hiding the cursor and giving // reliable behavior when clicking the text field (since it will always gain focus on click). setFocusableInTouchMode(true); + + if (ALLOW_FOLDER_SCROLL) { + mOnScrollHintAlarm = new Alarm(); + mScrollPauseAlarm = new Alarm(); + } } @Override @@ -183,6 +215,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList int measureSpec = MeasureSpec.UNSPECIFIED; mFooter.measure(measureSpec, measureSpec); mFooterHeight = mFooter.getMeasuredHeight(); + + if (ALLOW_FOLDER_SCROLL) { + mPagedView = (FolderPagedView) mContent; + } } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -386,6 +422,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void animateOpen() { if (!(getParent() instanceof DragLayer)) return; + if (ALLOW_FOLDER_SCROLL) { + // Always open on the first page. + mPagedView.snapToPageImmediately(0); + } + Animator openFolderAnim = null; final Runnable onCompleteRunnable; if (!Utilities.isLmpOrAbove()) { @@ -544,6 +585,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void onDragEnter(DragObject d) { mPrevTargetRank = -1; mOnExitAlarm.cancelAlarm(); + if (ALLOW_FOLDER_SCROLL) { + // Get the area offset such that the folder only closes if half the drag icon width + // is outside the folder area + mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset; + } } OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { @@ -558,18 +604,80 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); } + @Override public void onDragOver(DragObject d) { - final float[] r = d.getVisualCenter(null); - r[0] -= getPaddingLeft(); - r[1] -= getPaddingTop(); + onDragOver(d, REORDER_DELAY); + } + + private int getTargetRank(DragObject d, float[] recycle) { + recycle = d.getVisualCenter(recycle); + return mContent.findNearestArea( + (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop()); + } + + private void onDragOver(DragObject d, int reorderDelay) { + if (ALLOW_FOLDER_SCROLL && mScrollPauseAlarm.alarmPending()) { + return; + } + final float[] r = new float[2]; + mTargetRank = getTargetRank(d, r); - mTargetRank = mContent.findNearestArea((int) r[0], (int) r[1]); if (mTargetRank != mPrevTargetRank) { mReorderAlarm.cancelAlarm(); mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); mReorderAlarm.setAlarm(REORDER_DELAY); mPrevTargetRank = mTargetRank; } + + if (!ALLOW_FOLDER_SCROLL) { + return; + } + + float x = r[0]; + int currentPage = mPagedView.getNextPage(); + int cellWidth = mPagedView.getCurrentCellLayout().getCellWidth(); + if (currentPage > 0 && x < cellWidth * ICON_OVERSCROLL_WIDTH_FACTOR) { + // Show scroll hint on the left + if (mScrollHintDir != DragController.SCROLL_LEFT) { + mPagedView.showScrollHint(-SCROLL_HINT_FRACTION); + mScrollHintDir = DragController.SCROLL_LEFT; + } + + // Set alarm for when the hint is complete + if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != DragController.SCROLL_LEFT) { + mCurrentScrollDir = DragController.SCROLL_LEFT; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); + + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } + } else if (currentPage < (mPagedView.getPageCount() - 1) && + (x > (getWidth() - cellWidth * ICON_OVERSCROLL_WIDTH_FACTOR))) { + // Show scroll hint on the right + if (mScrollHintDir != DragController.SCROLL_RIGHT) { + mPagedView.showScrollHint(SCROLL_HINT_FRACTION); + mScrollHintDir = DragController.SCROLL_RIGHT; + } + + // Set alarm for when the hint is complete + if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != DragController.SCROLL_RIGHT) { + mCurrentScrollDir = DragController.SCROLL_RIGHT; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); + + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } + } else { + mOnScrollHintAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mPagedView.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } + } } OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { @@ -595,6 +703,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); } mReorderAlarm.cancelAlarm(); + + if (ALLOW_FOLDER_SCROLL) { + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mPagedView.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } + } } public void onDropCompleted(final View target, final DragObject d, @@ -960,6 +1077,22 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; } + if (ALLOW_FOLDER_SCROLL) { + // If the icon was dropped while the page was being scrolled, we need to compute + // the target location again such that the icon is placed of the final page. + if (!mPagedView.rankOnCurrentPage(mEmptyCellRank)) { + // Reorder again. + mTargetRank = getTargetRank(d, null); + + // Rearrange items immediately. + mReorderAlarmListener.onAlarm(mReorderAlarm); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + } + mPagedView.completePendingPageChanges(); + } + View currentDragView; ShortcutInfo si = mCurrentDragInfo; if (mIsExternalDrag) { @@ -1090,6 +1223,57 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void getHitRectRelativeToDragLayer(Rect outRect) { getHitRect(outRect); + outRect.left -= mScrollAreaOffset; + outRect.right += mScrollAreaOffset; + } + + private class OnScrollHintListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollHintListener(DragObject object) { + mDragObject = object; + } + + /** + * Scroll hint has been shown long enough. Now scroll to appropriate page. + */ + @Override + public void onAlarm(Alarm alarm) { + if (mCurrentScrollDir == DragController.SCROLL_LEFT) { + mPagedView.scrollLeft(); + mScrollHintDir = DragController.SCROLL_NONE; + } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) { + mPagedView.scrollRight(); + mScrollHintDir = DragController.SCROLL_NONE; + } else { + // This should not happen + return; + } + mCurrentScrollDir = DragController.SCROLL_NONE; + + // Pause drag event until the scrolling is finished + mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); + mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY); + } + } + + private class OnScrollFinishedListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollFinishedListener(DragObject object) { + mDragObject = object; + } + + /** + * Page scroll is complete. + */ + @Override + public void onAlarm(Alarm alarm) { + // Reorder immediately on page change. + onDragOver(mDragObject, 1); + } } public static interface FolderContent { diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index c9e825aa3..b4a7a7546 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -22,6 +22,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.animation.DecelerateInterpolator; import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; import com.android.launcher3.PageIndicator.PageMarkerResources; @@ -30,12 +31,15 @@ import com.android.launcher3.Workspace.ItemOperator; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.Map; public class FolderPagedView extends PagedView implements Folder.FolderContent { private static final String TAG = "FolderPagedView"; 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[] sTempPosArray = new int[2]; // TODO: Remove this restriction @@ -137,18 +141,9 @@ public class FolderPagedView extends PagedView implements Folder.FolderContent { public int allocateNewLastItemRank() { int rank = getItemCount(); int total = rank + 1; - if (rank < mMaxItemsPerPage) { - // Rearrange the items as the grid size might change. - mFolder.rearrangeChildren(total); - } else { - setupContentDimensions(total); - } + // Rearrange the items as the grid size might change. + mFolder.rearrangeChildren(total); - // Add a new page if last page is full - if (getPageAt(getChildCount() - 1).getShortcutsAndWidgets().getChildCount() - >= mMaxItemsPerPage) { - createAndAddNewPage(); - } setCurrentPage(getChildCount() - 1); return rank; } @@ -265,7 +260,8 @@ public class FolderPagedView extends PagedView implements Folder.FolderContent { int newX, newY, rank; rank = 0; - for (View v : list) { + 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()) { @@ -276,25 +272,28 @@ public class FolderPagedView extends PagedView implements Folder.FolderContent { position = 0; } - 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); + 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); } - lp.cellX = info.cellX; - lp.cellY = info.cellY; + rank ++; position++; - currentPage.addViewToCellLayout( - v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); } // Remove extra views. @@ -328,6 +327,10 @@ public class FolderPagedView extends PagedView implements Folder.FolderContent { @Override public int getItemCount() { int lastPage = getChildCount() - 1; + if (lastPage < 0) { + // If there are no pages, there must be only one icon in the folder. + return 1; + } return getPageAt(lastPage).getShortcutsAndWidgets().getChildCount() + lastPage * mMaxItemsPerPage; } @@ -406,10 +409,49 @@ public class FolderPagedView extends PagedView implements Folder.FolderContent { } } + /** + * Scrolls the current view by a fraction + */ + public void showScrollHint(float 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 (!mPageChangingViews.isEmpty()) { + HashMap pendingViews = new HashMap<>(mPageChangingViews); + 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 public void realTimeReorder(int empty, int target) { + completePendingPageChanges(); int delay = 0; - float delayAmount = 30; + float delayAmount = START_VIEW_REORDER_DELAY; // Animation only happens on the current page. int pageToAnimate = getNextPage(); @@ -523,7 +565,7 @@ public class FolderPagedView extends PagedView implements Folder.FolderContent { if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX, REORDER_ANIMATION_DURATION, delay, true, true)) { delay += delayAmount; - delayAmount *= 0.9; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; } } } -- cgit v1.2.3