diff options
Diffstat (limited to 'src/com/android/launcher3/allapps/AllAppsRecyclerView.java')
-rw-r--r-- | src/com/android/launcher3/allapps/AllAppsRecyclerView.java | 248 |
1 files changed, 145 insertions, 103 deletions
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index ff327dae3..a17f0e35d 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -22,10 +22,10 @@ import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.BaseRecyclerViewFastScrollBar; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.Stats; -import com.android.launcher3.Utilities; import java.util.List; @@ -35,13 +35,25 @@ import java.util.List; public class AllAppsRecyclerView extends BaseRecyclerView implements Stats.LaunchSourceProvider { + private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0; + private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1; + + private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0; + private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1; + private AlphabeticalAppsList mApps; private int mNumAppsPerRow; - private int mNumPredictedAppsPerRow; private int mPredictionBarHeight; - private int mLastFastscrollPosition = -1; + + private BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; + private int mPrevFastScrollFocusedPosition; + private int mFastScrollFrameIndex; + private int[] mFastScrollFrames = new int[10]; + private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON; + private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW; private Launcher mLauncher; + private ScrollPositionState mScrollPosState = new ScrollPositionState(); public AllAppsRecyclerView(Context context) { this(context, null); @@ -59,6 +71,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView int defStyleRes) { super(context, attrs, defStyleAttr); mLauncher = (Launcher) context; + setOverScrollMode(View.OVER_SCROLL_NEVER); } /** @@ -71,9 +84,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView /** * Sets the number of apps per row in this recycler view. */ - public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) { + public void setNumAppsPerRow(int numAppsPerRow) { mNumAppsPerRow = numAppsPerRow; - mNumPredictedAppsPerRow = numPredictedAppsPerRow; DeviceProfile grid = mLauncher.getDeviceProfile(); RecyclerView.RecycledViewPool pool = getRecycledViewPool(); @@ -103,11 +115,12 @@ public class AllAppsRecyclerView extends BaseRecyclerView */ public int getScrollPosition() { List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); - getCurScrollState(scrollPosState, items); - if (scrollPosState.rowIndex != -1) { + getCurScrollState(mScrollPosState, items); + if (mScrollPosState.rowIndex != -1) { int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight; - return getPaddingTop() + (scrollPosState.rowIndex * scrollPosState.rowHeight) + - predictionBarHeight - scrollPosState.rowTopOffset; + return getPaddingTop() + predictionBarHeight + + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - + mScrollPosState.rowTopOffset; } return 0; } @@ -132,143 +145,159 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } - @Override - protected void onFastScrollingEnd() { - mLastFastscrollPosition = -1; - } - /** * Maps the touch (from 0..1) to the adapter position that should be visible. */ @Override public String scrollToPositionAtProgress(float touchFraction) { - // Ensure that we have any sections - List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = - mApps.getFastScrollerSections(); - if (fastScrollSections.isEmpty()) { + int rowCount = mApps.getNumAppRows(); + if (rowCount == 0) { return ""; } // Stop the scroller if it is scrolling - LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); stopScroll(); - // If there is a prediction bar, then capture the appropriate area for the prediction bar - float predictionBarFraction = 0f; - if (!mApps.getPredictedApps().isEmpty()) { - predictionBarFraction = (float) mNumPredictedAppsPerRow / mApps.getSize(); - if (touchFraction <= predictionBarFraction) { - // Scroll to the top of the view, where the prediction bar is - layoutManager.scrollToPositionWithOffset(0, 0); - return ""; + // Find the fastscroll section that maps to this touch fraction + List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = + mApps.getFastScrollerSections(); + AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); + if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { + for (int i = 1; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); + if (info.touchFraction > touchFraction) { + break; + } + lastInfo = info; } + } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ + lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1))); + } else { + throw new RuntimeException("Unexpected scroll bar mode"); } - // Since the app ranges are from 0..1, we need to map the touch fraction back to 0..1 from - // predictionBarFraction..1 - touchFraction = (touchFraction - predictionBarFraction) * - (1f / (1f - predictionBarFraction)); - AlphabeticalAppsList.FastScrollSectionInfo lastScrollSection = fastScrollSections.get(0); - for (int i = 1; i < fastScrollSections.size(); i++) { - AlphabeticalAppsList.FastScrollSectionInfo scrollSection = fastScrollSections.get(i); - if (lastScrollSection.appRangeFraction <= touchFraction && - touchFraction < scrollSection.appRangeFraction) { - break; - } - lastScrollSection = scrollSection; + // Map the touch position back to the scroll of the recycler view + getCurScrollState(mScrollPosState, mApps.getAdapterItems()); + int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight; + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, + predictionBarHeight); + LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); + if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); } - // Scroll to the view at the position, anchored at the top of the screen. We call the scroll - // method on the LayoutManager directly since it is not exposed by RecyclerView. - if (mLastFastscrollPosition != lastScrollSection.appItem.position) { - mLastFastscrollPosition = lastScrollSection.appItem.position; - layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0); - } + if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { + mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; - return lastScrollSection.sectionName; - } + // Reset the last focused view + if (mLastFastScrollFocusedView != null) { + mLastFastScrollFocusedView.setFastScrollFocused(false, true); + mLastFastScrollFocusedView = null; + } - /** - * Returns the row index for a app index in the list. - */ - private int findRowForAppIndex(int index) { - List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); - int appIndex = 0; - int rowCount = 0; - for (AlphabeticalAppsList.SectionInfo info : sections) { - int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow); - if (appIndex + info.numApps > index) { - return rowCount + ((index - appIndex) / mNumAppsPerRow); + if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { + smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); + } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } + } else { + throw new RuntimeException("Unexpected fast scroll mode"); } - appIndex += info.numApps; - rowCount += numRowsInSection; } - return appIndex; + return lastInfo.sectionName; } - /** - * Returns the total number of rows in the list. - */ - private int getNumRows() { - List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); - int rowCount = 0; - for (AlphabeticalAppsList.SectionInfo info : sections) { - int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow); - rowCount += numRowsInSection; + @Override + public void onFastScrollCompleted() { + super.onFastScrollCompleted(); + // Reset and clean up the last focused view + if (mLastFastScrollFocusedView != null) { + mLastFastScrollFocusedView.setFastScrollFocused(false, true); + mLastFastScrollFocusedView = null; } - return rowCount; + mPrevFastScrollFocusedPosition = -1; } - /** * Updates the bounds for the scrollbar. */ @Override - public void updateVerticalScrollbarBounds() { + public void onUpdateScrollbar() { List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); // Skip early if there are no items or we haven't been measured if (items.isEmpty() || mNumAppsPerRow == 0) { - verticalScrollbarBounds.setEmpty(); + mScrollbar.setScrollbarThumbOffset(-1, -1); return; } // Find the index and height of the first visible row (all rows have the same height) - int x, y; + int rowCount = mApps.getNumAppRows(); + getCurScrollState(mScrollPosState, items); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight; - int rowCount = getNumRows(); - getCurScrollState(scrollPosState, items); - if (scrollPosState.rowIndex != -1) { - int height = getHeight() - getPaddingTop() - getPaddingBottom(); - int totalScrollHeight = rowCount * scrollPosState.rowHeight + predictionBarHeight; - if (totalScrollHeight > height) { - int scrollbarHeight = (int) (height / ((float) totalScrollHeight / height)); - - // Calculate the position and size of the scroll bar - if (Utilities.isRtl(getResources())) { - x = mBackgroundPadding.left; - } else { - x = getWidth() - mBackgroundPadding.right - getScrollbarWidth(); - } + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, predictionBarHeight); + } - // To calculate the offset, we compute the percentage of the total scrollable height - // that the user has already scrolled and then map that to the scroll bar bounds - int availableY = totalScrollHeight - height; - int availableScrollY = height - scrollbarHeight; - y = (scrollPosState.rowIndex * scrollPosState.rowHeight) + predictionBarHeight - - scrollPosState.rowTopOffset; - y = getPaddingTop() + - (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY); - - verticalScrollbarBounds.set(x, y, x + getScrollbarWidth(), y + scrollbarHeight); - return; + /** + * This runnable runs a single frame of the smooth scroll animation and posts the next frame + * if necessary. + */ + private Runnable mSmoothSnapNextFrameRunnable = new Runnable() { + @Override + public void run() { + if (mFastScrollFrameIndex < mFastScrollFrames.length) { + scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); + mFastScrollFrameIndex++; + postOnAnimation(mSmoothSnapNextFrameRunnable); + } else { + // Animation completed, set the fast scroll state on the target view + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && + mLastFastScrollFocusedView != vh.itemView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } } } - verticalScrollbarBounds.setEmpty(); + }; + + /** + * Smoothly snaps to a given position. We do this manually by calculating the keyframes + * ourselves and animating the scroll on the recycler view. + */ + private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) { + removeCallbacks(mSmoothSnapNextFrameRunnable); + + // Calculate the full animation from the current scroll position to the final scroll + // position, and then run the animation for the duration. + int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight; + int curScrollY = getPaddingTop() + predictionBarHeight + + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); + int numFrames = mFastScrollFrames.length; + for (int i = 0; i < numFrames; i++) { + // TODO(winsonc): We can interpolate this as well. + mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames; + } + mFastScrollFrameIndex = 0; + postOnAnimation(mSmoothSnapNextFrameRunnable); } /** - * Returns the current scroll state. + * Returns the current scroll state of the apps rows, not including the prediction + * bar. */ private void getCurScrollState(ScrollPositionState stateOut, List<AlphabeticalAppsList.AdapterItem> items) { @@ -288,7 +317,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView if (position != NO_POSITION) { AlphabeticalAppsList.AdapterItem item = items.get(position); if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { - stateOut.rowIndex = findRowForAppIndex(item.appIndex); + stateOut.rowIndex = item.rowIndex; stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); stateOut.rowHeight = child.getHeight(); break; @@ -296,4 +325,17 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } } + + /** + * Returns the scrollY for the given position in the adapter. + */ + private int getScrollAtPosition(int position, int rowHeight) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { + int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight; + return getPaddingTop() + predictionBarHeight + item.rowIndex * rowHeight; + } else { + return 0; + } + } } |