| /* |
| * 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.Intent; |
| import android.content.res.Resources; |
| import android.support.animation.DynamicAnimation; |
| import android.support.animation.SpringAnimation; |
| import android.support.v4.view.accessibility.AccessibilityEventCompat; |
| import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; |
| import android.support.v4.view.accessibility.AccessibilityRecordCompat; |
| import android.support.v7.widget.GridLayoutManager; |
| import android.support.v7.widget.RecyclerView; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnFocusChangeListener; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.TextView; |
| |
| import com.android.launcher3.AppInfo; |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem; |
| import com.android.launcher3.anim.SpringAnimationHandler; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.discovery.AppDiscoveryAppInfo; |
| import com.android.launcher3.discovery.AppDiscoveryItemView; |
| import com.android.launcher3.util.PackageManagerHelper; |
| |
| import java.util.List; |
| |
| /** |
| * The grid view adapter of all the apps. |
| */ |
| public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { |
| |
| public static final String TAG = "AppsGridAdapter"; |
| |
| // A normal icon |
| public static final int VIEW_TYPE_ICON = 1 << 1; |
| // A prediction icon |
| public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2; |
| // The message shown when there are no filtered results |
| public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3; |
| // The message to continue to a market search when there are no filtered results |
| public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4; |
| |
| // We use various dividers for various purposes. They share enough attributes to reuse layouts, |
| // but differ in enough attributes to require different view types |
| |
| // A divider that separates the apps list and the search market button |
| public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5; |
| // The divider that separates prediction icons from the app list |
| public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 6; |
| public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 7; |
| public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 8; |
| |
| // Common view type masks |
| public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_MARKET_DIVIDER |
| | VIEW_TYPE_PREDICTION_DIVIDER; |
| public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON |
| | VIEW_TYPE_PREDICTION_ICON; |
| public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON |
| | VIEW_TYPE_DISCOVERY_ITEM; |
| public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON |
| | VIEW_TYPE_PREDICTION_DIVIDER; |
| |
| |
| public interface BindViewCallback { |
| void onBindView(ViewHolder holder); |
| } |
| |
| /** |
| * ViewHolder for each icon. |
| */ |
| public static class ViewHolder extends RecyclerView.ViewHolder { |
| |
| public ViewHolder(View v) { |
| super(v); |
| } |
| } |
| |
| /** |
| * A subclass of GridLayoutManager that overrides accessibility values during app search. |
| */ |
| public class AppsGridLayoutManager extends GridLayoutManager { |
| |
| public AppsGridLayoutManager(Context context) { |
| super(context, 1, GridLayoutManager.VERTICAL, false); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityEvent(AccessibilityEvent event) { |
| super.onInitializeAccessibilityEvent(event); |
| |
| // Ensure that we only report the number apps for accessibility not including other |
| // adapter views |
| final AccessibilityRecordCompat record = AccessibilityEventCompat |
| .asRecord(event); |
| record.setItemCount(mApps.getNumFilteredApps()); |
| record.setFromIndex(Math.max(0, |
| record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex()))); |
| record.setToIndex(Math.max(0, |
| record.getToIndex() - getRowsNotForAccessibility(record.getToIndex()))); |
| } |
| |
| @Override |
| public int getRowCountForAccessibility(RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| return super.getRowCountForAccessibility(recycler, state) - |
| getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, |
| RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { |
| super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info); |
| |
| ViewGroup.LayoutParams lp = host.getLayoutParams(); |
| AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo(); |
| if (!(lp instanceof LayoutParams) || (cic == null)) { |
| return; |
| } |
| LayoutParams glp = (LayoutParams) lp; |
| info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( |
| cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()), |
| cic.getRowSpan(), |
| cic.getColumnIndex(), |
| cic.getColumnSpan(), |
| cic.isHeading(), |
| cic.isSelected())); |
| } |
| |
| /** |
| * Returns the number of rows before {@param adapterPosition}, including this position |
| * which should not be counted towards the collection info. |
| */ |
| private int getRowsNotForAccessibility(int adapterPosition) { |
| List<AdapterItem> items = mApps.getAdapterItems(); |
| adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); |
| int extraRows = 0; |
| for (int i = 0; i <= adapterPosition; i++) { |
| if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) { |
| extraRows++; |
| } |
| } |
| return extraRows; |
| } |
| } |
| |
| /** |
| * Helper class to size the grid items. |
| */ |
| public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { |
| |
| public GridSpanSizer() { |
| super(); |
| setSpanIndexCacheEnabled(true); |
| } |
| |
| @Override |
| public int getSpanSize(int position) { |
| if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) { |
| return 1; |
| } else { |
| // Section breaks span the full width |
| return mAppsPerRow; |
| } |
| } |
| } |
| |
| private final Launcher mLauncher; |
| private final LayoutInflater mLayoutInflater; |
| private final AlphabeticalAppsList mApps; |
| private final GridLayoutManager mGridLayoutMgr; |
| private final GridSpanSizer mGridSizer; |
| private final View.OnClickListener mIconClickListener; |
| private final View.OnLongClickListener mIconLongClickListener; |
| |
| private int mAppsPerRow; |
| |
| private BindViewCallback mBindViewCallback; |
| private OnFocusChangeListener mIconFocusListener; |
| |
| // The text to show when there are no search results and no market search handler. |
| private String mEmptySearchMessage; |
| // The intent to send off to the market app, updated each time the search query changes. |
| private Intent mMarketSearchIntent; |
| |
| private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler; |
| |
| public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener |
| iconClickListener, View.OnLongClickListener iconLongClickListener) { |
| Resources res = launcher.getResources(); |
| mLauncher = launcher; |
| mApps = apps; |
| mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); |
| mGridSizer = new GridSpanSizer(); |
| mGridLayoutMgr = new AppsGridLayoutManager(launcher); |
| mGridLayoutMgr.setSpanSizeLookup(mGridSizer); |
| mLayoutInflater = LayoutInflater.from(launcher); |
| mIconClickListener = iconClickListener; |
| mIconLongClickListener = iconLongClickListener; |
| if (FeatureFlags.LAUNCHER3_PHYSICS) { |
| mSpringAnimationHandler = new SpringAnimationHandler<>( |
| SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory()); |
| } |
| } |
| |
| public SpringAnimationHandler getSpringAnimationHandler() { |
| return mSpringAnimationHandler; |
| } |
| |
| public static boolean isDividerViewType(int viewType) { |
| return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); |
| } |
| |
| public static boolean isIconViewType(int viewType) { |
| return isViewType(viewType, VIEW_TYPE_MASK_ICON); |
| } |
| |
| public static boolean isViewType(int viewType, int viewTypeMask) { |
| return (viewType & viewTypeMask) != 0; |
| } |
| |
| /** |
| * Sets the number of apps per row. |
| */ |
| public void setNumAppsPerRow(int appsPerRow) { |
| mAppsPerRow = appsPerRow; |
| mGridLayoutMgr.setSpanCount(appsPerRow); |
| } |
| |
| public int getNumAppsPerRow() { |
| return mAppsPerRow; |
| } |
| |
| public void setIconFocusListener(OnFocusChangeListener focusListener) { |
| mIconFocusListener = focusListener; |
| } |
| |
| /** |
| * Sets the last search query that was made, used to show when there are no results and to also |
| * seed the intent for searching the market. |
| */ |
| public void setLastSearchQuery(String query) { |
| Resources res = mLauncher.getResources(); |
| mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); |
| mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query); |
| } |
| |
| /** |
| * Sets the callback for when views are bound. |
| */ |
| public void setBindViewCallback(BindViewCallback cb) { |
| mBindViewCallback = cb; |
| } |
| |
| /** |
| * Returns the grid layout manager. |
| */ |
| public GridLayoutManager getLayoutManager() { |
| return mGridLayoutMgr; |
| } |
| |
| @Override |
| public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| switch (viewType) { |
| case VIEW_TYPE_ICON: |
| case VIEW_TYPE_PREDICTION_ICON: |
| BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( |
| R.layout.all_apps_icon, parent, false); |
| icon.setOnClickListener(mIconClickListener); |
| icon.setOnLongClickListener(mIconLongClickListener); |
| icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout()); |
| icon.setOnFocusChangeListener(mIconFocusListener); |
| |
| // Ensure the all apps icon height matches the workspace icons in portrait mode. |
| icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; |
| return new ViewHolder(icon); |
| case VIEW_TYPE_DISCOVERY_ITEM: |
| AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater |
| .inflate(R.layout.all_apps_discovery_item, parent, false); |
| appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(), |
| mIconLongClickListener); |
| return new ViewHolder(appDiscoveryItemView); |
| case VIEW_TYPE_EMPTY_SEARCH: |
| return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, |
| parent, false)); |
| case VIEW_TYPE_SEARCH_MARKET: |
| View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, |
| parent, false); |
| searchMarketView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| mLauncher.startActivitySafely(v, mMarketSearchIntent, null); |
| } |
| }); |
| return new ViewHolder(searchMarketView); |
| case VIEW_TYPE_APPS_LOADING_DIVIDER: |
| View loadingDividerView = mLayoutInflater.inflate( |
| R.layout.all_apps_discovery_loading_divider, parent, false); |
| return new ViewHolder(loadingDividerView); |
| case VIEW_TYPE_PREDICTION_DIVIDER: |
| case VIEW_TYPE_SEARCH_MARKET_DIVIDER: |
| return new ViewHolder(mLayoutInflater.inflate( |
| R.layout.all_apps_divider, parent, false)); |
| default: |
| throw new RuntimeException("Unexpected view type"); |
| } |
| } |
| |
| @Override |
| public void onBindViewHolder(ViewHolder holder, int position) { |
| switch (holder.getItemViewType()) { |
| case VIEW_TYPE_ICON: |
| case VIEW_TYPE_PREDICTION_ICON: |
| AppInfo info = mApps.getAdapterItems().get(position).appInfo; |
| BubbleTextView icon = (BubbleTextView) holder.itemView; |
| icon.applyFromApplicationInfo(info); |
| break; |
| case VIEW_TYPE_DISCOVERY_ITEM: |
| AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo) |
| mApps.getAdapterItems().get(position).appInfo; |
| AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView; |
| view.apply(appDiscoveryAppInfo); |
| break; |
| case VIEW_TYPE_EMPTY_SEARCH: |
| TextView emptyViewText = (TextView) holder.itemView; |
| emptyViewText.setText(mEmptySearchMessage); |
| emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : |
| Gravity.START | Gravity.CENTER_VERTICAL); |
| break; |
| case VIEW_TYPE_SEARCH_MARKET: |
| TextView searchView = (TextView) holder.itemView; |
| if (mMarketSearchIntent != null) { |
| searchView.setVisibility(View.VISIBLE); |
| } else { |
| searchView.setVisibility(View.GONE); |
| } |
| break; |
| case VIEW_TYPE_APPS_LOADING_DIVIDER: |
| int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; |
| int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; |
| holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading); |
| holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded); |
| break; |
| case VIEW_TYPE_SEARCH_MARKET_DIVIDER: |
| // nothing to do |
| break; |
| } |
| if (mBindViewCallback != null) { |
| mBindViewCallback.onBindView(holder); |
| } |
| } |
| |
| @Override |
| public void onViewAttachedToWindow(ViewHolder holder) { |
| int type = holder.getItemViewType(); |
| if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) { |
| mSpringAnimationHandler.add(holder.itemView, holder); |
| } |
| } |
| |
| @Override |
| public void onViewDetachedFromWindow(ViewHolder holder) { |
| int type = holder.getItemViewType(); |
| if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) { |
| mSpringAnimationHandler.remove(holder.itemView); |
| } |
| } |
| |
| @Override |
| public boolean onFailedToRecycleView(ViewHolder holder) { |
| // Always recycle and we will reset the view when it is bound |
| return true; |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mApps.getAdapterItems().size(); |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); |
| return item.viewType; |
| } |
| |
| /** |
| * Helper class to set the SpringAnimation values for an item in the adapter. |
| */ |
| private class AllAppsSpringAnimationFactory |
| implements SpringAnimationHandler.AnimationFactory<ViewHolder> { |
| private static final float DEFAULT_MAX_VALUE_PX = 100; |
| private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX; |
| |
| // Damping ratio range is [0, 1] |
| private static final float SPRING_DAMPING_RATIO = 0.55f; |
| |
| // Stiffness is a non-negative number. |
| private static final float MIN_SPRING_STIFFNESS = 580f; |
| private static final float MAX_SPRING_STIFFNESS = 900f; |
| |
| // The amount by which each adjacent rows' stiffness will differ. |
| private static final float ROW_STIFFNESS_COEFFICIENT = 50f; |
| |
| @Override |
| public SpringAnimation initialize(ViewHolder vh) { |
| return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0); |
| } |
| |
| /** |
| * @param spring A new or recycled SpringAnimation. |
| * @param vh The ViewHolder that {@param spring} is related to. |
| */ |
| @Override |
| public void update(SpringAnimation spring, ViewHolder vh) { |
| int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size()); |
| int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps, |
| mAppsPerRow); |
| |
| int col = appPosition % mAppsPerRow; |
| int row = appPosition / mAppsPerRow; |
| |
| int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count |
| if (row > (numTotalRows / 2)) { |
| // Mirror the rows so that the top row acts the same as the bottom row. |
| row = Math.abs(numTotalRows - row); |
| } |
| |
| calculateSpringValues(spring, row, col); |
| } |
| |
| @Override |
| public void setDefaultValues(SpringAnimation spring) { |
| calculateSpringValues(spring, 0, mAppsPerRow / 2); |
| } |
| |
| /** |
| * We manipulate the stiffness, min, and max values based on the items distance to the |
| * first row and the items distance to the center column to create the ^-shaped motion |
| * effect. |
| */ |
| private void calculateSpringValues(SpringAnimation spring, int row, int col) { |
| float rowFactor = (1 + row) * 0.5f; |
| float colFactor = getColumnFactor(col, mAppsPerRow); |
| |
| float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor); |
| float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor); |
| |
| float stiffness = Utilities.boundToRange( |
| MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT), |
| MIN_SPRING_STIFFNESS, |
| MAX_SPRING_STIFFNESS); |
| |
| spring.setMinValue(minValue) |
| .setMaxValue(maxValue) |
| .getSpring() |
| .setStiffness(stiffness) |
| .setDampingRatio(SPRING_DAMPING_RATIO); |
| } |
| |
| /** |
| * @return The app position is the position of the app in the Adapter if we ignored all |
| * other view types. |
| * |
| * The first app is at position 0, and the first app each following row is at a |
| * position that is a multiple of {@param appsPerRow}. |
| * |
| * ie. If there are 5 apps per row, and there are two rows of apps: |
| * 0 1 2 3 4 |
| * 5 6 7 8 9 |
| */ |
| private int getAppPosition(int position, int numPredictedApps, int appsPerRow) { |
| if (position < numPredictedApps) { |
| // Predicted apps are first in the adapter. |
| return position; |
| } |
| |
| // There is at most 1 divider view between the predicted apps and the alphabetical apps. |
| int numDividerViews = numPredictedApps == 0 ? 0 : 1; |
| |
| // This offset takes into consideration an incomplete row of predicted apps. |
| int numPredictedAppsOffset = appsPerRow - numPredictedApps; |
| return position + numPredictedAppsOffset - numDividerViews; |
| } |
| |
| /** |
| * Increase the column factor as the distance increases between the column and the center |
| * column(s). |
| */ |
| private float getColumnFactor(int col, int numCols) { |
| float centerColumn = numCols / 2; |
| int distanceToCenter = (int) Math.abs(col - centerColumn); |
| |
| boolean evenNumberOfColumns = numCols % 2 == 0; |
| if (evenNumberOfColumns && col < centerColumn) { |
| distanceToCenter -= 1; |
| } |
| |
| float factor = 0; |
| while (distanceToCenter > 0) { |
| if (distanceToCenter == 1) { |
| factor += 0.2f; |
| } else { |
| factor += 0.1f; |
| } |
| --distanceToCenter; |
| } |
| |
| return factor; |
| } |
| } |
| } |