| /* |
| * 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.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.RecyclerView; |
| import android.util.AttributeSet; |
| import android.view.View; |
| |
| import com.android.launcher3.BaseRecyclerView; |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Stats; |
| import com.android.launcher3.Utilities; |
| |
| 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 AlphabeticalAppsList mApps; |
| private AllAppsFastScrollHelper mFastScrollHelper; |
| private BaseRecyclerView.ScrollPositionState mScrollPosState = |
| new BaseRecyclerView.ScrollPositionState(); |
| private int mNumAppsPerRow; |
| |
| // The specific icon heights that we use to calculate scroll |
| private int mPredictionIconHeight; |
| private int mIconHeight; |
| |
| // The empty-search result background |
| private AllAppsBackgroundDrawable mEmptySearchBackground; |
| private int mEmptySearchBackgroundTopOffset; |
| |
| private HeaderElevationController mElevationController; |
| |
| 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(); |
| addOnItemTouchListener(this); |
| 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; |
| mFastScrollHelper = new AllAppsFastScrollHelper(this, apps); |
| } |
| |
| public void setElevationController(HeaderElevationController elevationController) { |
| mElevationController = elevationController; |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * Sets the heights of the icons in this view (for scroll calculations). |
| */ |
| public void setPremeasuredIconHeights(int predictionIconHeight, int iconHeight) { |
| mPredictionIconHeight = predictionIconHeight; |
| mIconHeight = iconHeight; |
| } |
| |
| /** |
| * 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); |
| if (mElevationController != null) { |
| mElevationController.reset(); |
| } |
| } |
| |
| /** |
| * 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) { |
| // Clip to ensure that we don't draw the overscroll effect beyond the background bounds |
| 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 |
| public void fillInLaunchSourceData(View v, 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 { |
| if (v instanceof BubbleTextView) { |
| BubbleTextView icon = (BubbleTextView) v; |
| int position = getChildPosition(icon); |
| if (position != NO_POSITION) { |
| List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); |
| AlphabeticalAppsList.AdapterItem item = items.get(position); |
| if (item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { |
| sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, |
| Stats.SUB_CONTAINER_ALL_APPS_PREDICTION); |
| return; |
| } |
| } |
| } |
| 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); |
| for (int i = 1; i < fastScrollSections.size(); i++) { |
| AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); |
| if (info.touchFraction > touchFraction) { |
| break; |
| } |
| lastInfo = info; |
| } |
| |
| // Update the fast scroll |
| int scrollY = getScrollTop(mScrollPosState); |
| int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows()); |
| mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo); |
| return lastInfo.sectionName; |
| } |
| |
| @Override |
| public void onFastScrollCompleted() { |
| super.onFastScrollCompleted(); |
| mFastScrollHelper.onFastScrollCompleted(); |
| } |
| |
| @Override |
| public void setAdapter(Adapter adapter) { |
| super.setAdapter(adapter); |
| mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter); |
| } |
| |
| /** |
| * 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, -1); |
| 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()); |
| 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 = getScrollTop(mScrollPosState); |
| 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); |
| } |
| } |
| |
| /** |
| * Returns the current scroll state of the apps rows. |
| */ |
| protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) { |
| stateOut.rowIndex = -1; |
| stateOut.rowTopOffset = -1; |
| stateOut.itemPos = -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 & viewTypeMask) != 0) { |
| stateOut.rowIndex = item.rowIndex; |
| stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); |
| stateOut.itemPos = position; |
| return; |
| } |
| } |
| } |
| return; |
| } |
| |
| @Override |
| protected boolean supportsFastScrolling() { |
| // Only allow fast scrolling when the user is not searching, since the results are not |
| // grouped in a meaningful order |
| return !mApps.hasFilter(); |
| } |
| |
| protected int getTop(int rowIndex) { |
| if (getChildCount() == 0 || rowIndex <= 0) { |
| return 0; |
| } |
| |
| // The prediction bar icons have more padding, so account for that in the row offset |
| return mPredictionIconHeight + (rowIndex - 1) * mIconHeight; |
| } |
| |
| /** |
| * 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()); |
| } |
| } |