summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/allapps/AllAppsRecyclerView.java')
-rw-r--r--src/com/android/launcher3/allapps/AllAppsRecyclerView.java463
1 files changed, 463 insertions, 0 deletions
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
new file mode 100644
index 000000000..2f66e2cad
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -0,0 +1,463 @@
+/*
+ * 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.allapps;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+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.R;
+import com.android.launcher3.Stats;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Thunk;
+
+import java.util.List;
+
+/**
+ * A RecyclerView with custom fast scroll support for the all apps view.
+ */
+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;
+
+ @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
+ @Thunk int mPrevFastScrollFocusedPosition;
+ @Thunk int mFastScrollFrameIndex;
+ @Thunk final 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 ScrollPositionState mScrollPosState = new ScrollPositionState();
+
+ private AllAppsBackgroundDrawable mEmptySearchBackground;
+ private int mEmptySearchBackgroundTopOffset;
+
+ public AllAppsRecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public AllAppsRecyclerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr);
+
+ Resources res = getResources();
+ mScrollbar.setDetachThumbOnFastScroll();
+ mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
+ R.dimen.all_apps_empty_search_bg_top_offset);
+ }
+
+ /**
+ * Sets the list of apps in this view, used to determine the fastscroll position.
+ */
+ public void setApps(AlphabeticalAppsList apps) {
+ mApps = apps;
+ }
+
+ /**
+ * Sets the number of apps per row in this recycler view.
+ */
+ public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
+ mNumAppsPerRow = numAppsPerRow;
+
+ RecyclerView.RecycledViewPool pool = getRecycledViewPool();
+ int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
+ }
+
+ /**
+ * Scrolls this recycler view to the top.
+ */
+ public void scrollToTop() {
+ // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
+ if (mScrollbar.isThumbDetached()) {
+ mScrollbar.reattachThumbToScroll();
+ }
+ scrollToPosition(0);
+ }
+
+ /**
+ * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
+ * background bounds.
+ */
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
+ getWidth() - mBackgroundPadding.right,
+ getHeight() - mBackgroundPadding.bottom);
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ // Draw the background
+ if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
+ c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
+ getWidth() - mBackgroundPadding.right,
+ getHeight() - mBackgroundPadding.bottom);
+
+ mEmptySearchBackground.draw(c);
+ }
+
+ super.onDraw(c);
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mEmptySearchBackground || super.verifyDrawable(who);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ updateEmptySearchBackgroundBounds();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ // Bind event handlers
+ addOnItemTouchListener(this);
+ }
+
+ @Override
+ public void fillInLaunchSourceData(Bundle sourceData) {
+ sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
+ if (mApps.hasFilter()) {
+ sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+ Stats.SUB_CONTAINER_ALL_APPS_SEARCH);
+ } else {
+ sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+ Stats.SUB_CONTAINER_ALL_APPS_A_Z);
+ }
+ }
+
+ public void onSearchResultsChanged() {
+ // Always scroll the view to the top so the user can see the changed results
+ scrollToTop();
+
+ if (mApps.hasNoFilteredResults()) {
+ if (mEmptySearchBackground == null) {
+ mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext());
+ mEmptySearchBackground.setAlpha(0);
+ mEmptySearchBackground.setCallback(this);
+ updateEmptySearchBackgroundBounds();
+ }
+ mEmptySearchBackground.animateBgAlpha(1f, 150);
+ } else if (mEmptySearchBackground != null) {
+ // For the time being, we just immediately hide the background to ensure that it does
+ // not overlap with the results
+ mEmptySearchBackground.setBgAlpha(0f);
+ }
+ }
+
+ /**
+ * Maps the touch (from 0..1) to the adapter position that should be visible.
+ */
+ @Override
+ public String scrollToPositionAtProgress(float touchFraction) {
+ int rowCount = mApps.getNumAppRows();
+ if (rowCount == 0) {
+ return "";
+ }
+
+ // Stop the scroller if it is scrolling
+ stopScroll();
+
+ // 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");
+ }
+
+ // Map the touch position back to the scroll of the recycler view
+ getCurScrollState(mScrollPosState);
+ int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
+ LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+ if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
+ layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
+ }
+
+ if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
+ mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
+
+ // Reset the last focused view
+ if (mLastFastScrollFocusedView != null) {
+ mLastFastScrollFocusedView.setFastScrollFocused(false, true);
+ mLastFastScrollFocusedView = null;
+ }
+
+ 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");
+ }
+ }
+ return lastInfo.sectionName;
+ }
+
+ @Override
+ public void onFastScrollCompleted() {
+ super.onFastScrollCompleted();
+ // Reset and clean up the last focused view
+ if (mLastFastScrollFocusedView != null) {
+ mLastFastScrollFocusedView.setFastScrollFocused(false, true);
+ mLastFastScrollFocusedView = null;
+ }
+ mPrevFastScrollFocusedPosition = -1;
+ }
+
+ /**
+ * Updates the bounds for the scrollbar.
+ */
+ @Override
+ public void onUpdateScrollbar(int dy) {
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+
+ // Skip early if there are no items or we haven't been measured
+ if (items.isEmpty() || mNumAppsPerRow == 0) {
+ mScrollbar.setThumbOffset(-1, -1);
+ return;
+ }
+
+ // Find the index and height of the first visible row (all rows have the same height)
+ int rowCount = mApps.getNumAppRows();
+ getCurScrollState(mScrollPosState);
+ if (mScrollPosState.rowIndex < 0) {
+ mScrollbar.setThumbOffset(-1, -1);
+ return;
+ }
+
+ // Only show the scrollbar if there is height to be scrolled
+ int availableScrollBarHeight = getAvailableScrollBarHeight();
+ int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
+ if (availableScrollHeight <= 0) {
+ mScrollbar.setThumbOffset(-1, -1);
+ return;
+ }
+
+ // Calculate the current scroll position, the scrollY of the recycler view accounts for the
+ // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
+ // padding)
+ int scrollY = getPaddingTop() +
+ (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
+ int scrollBarY = mBackgroundPadding.top +
+ (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
+
+ if (mScrollbar.isThumbDetached()) {
+ int scrollBarX;
+ if (Utilities.isRtl(getResources())) {
+ scrollBarX = mBackgroundPadding.left;
+ } else {
+ scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
+ }
+
+ if (mScrollbar.isDraggingThumb()) {
+ // If the thumb is detached, then just update the thumb to the current
+ // touch position
+ mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY());
+ } else {
+ int thumbScrollY = mScrollbar.getThumbOffset().y;
+ int diffScrollY = scrollBarY - thumbScrollY;
+ if (diffScrollY * dy > 0f) {
+ // User is scrolling in the same direction the thumb needs to catch up to the
+ // current scroll position. We do this by mapping the difference in movement
+ // from the original scroll bar position to the difference in movement necessary
+ // in the detached thumb position to ensure that both speed towards the same
+ // position at either end of the list.
+ if (dy < 0) {
+ int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY);
+ thumbScrollY += Math.max(offset, diffScrollY);
+ } else {
+ int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) /
+ (float) (availableScrollBarHeight - scrollBarY));
+ thumbScrollY += Math.min(offset, diffScrollY);
+ }
+ thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
+ mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
+ if (scrollBarY == thumbScrollY) {
+ mScrollbar.reattachThumbToScroll();
+ }
+ } else {
+ // User is scrolling in an opposite direction to the direction that the thumb
+ // needs to catch up to the scroll position. Do nothing except for updating
+ // the scroll bar x to match the thumb width.
+ mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
+ }
+ }
+ } else {
+ synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
+ }
+ }
+
+ /**
+ * This runnable runs a single frame of the smooth scroll animation and posts the next frame
+ * if necessary.
+ */
+ @Thunk 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);
+ }
+ }
+ }
+ };
+
+ /**
+ * 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 curScrollY = getPaddingTop() +
+ (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 of the apps rows.
+ */
+ protected void getCurScrollState(ScrollPositionState stateOut) {
+ stateOut.rowIndex = -1;
+ stateOut.rowTopOffset = -1;
+ stateOut.rowHeight = -1;
+
+ // Return early if there are no items or we haven't been measured
+ List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ if (items.isEmpty() || mNumAppsPerRow == 0) {
+ return;
+ }
+
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ int position = getChildPosition(child);
+ if (position != NO_POSITION) {
+ AlphabeticalAppsList.AdapterItem item = items.get(position);
+ if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
+ item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ stateOut.rowIndex = item.rowIndex;
+ stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
+ stateOut.rowHeight = child.getHeight();
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 ||
+ item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
+ return offset + item.rowIndex * rowHeight;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Updates the bounds of the empty search background.
+ */
+ private void updateEmptySearchBackgroundBounds() {
+ if (mEmptySearchBackground == null) {
+ return;
+ }
+
+ // Center the empty search background on this new view bounds
+ int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2;
+ int y = mEmptySearchBackgroundTopOffset;
+ mEmptySearchBackground.setBounds(x, y,
+ x + mEmptySearchBackground.getIntrinsicWidth(),
+ y + mEmptySearchBackground.getIntrinsicHeight());
+ }
+}