Home | History | Annotate | Download | only in allapps
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.launcher3.allapps;
     17 
     18 import android.content.Context;
     19 import android.content.Intent;
     20 import android.content.res.Resources;
     21 import android.support.animation.DynamicAnimation;
     22 import android.support.animation.SpringAnimation;
     23 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     25 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     26 import android.support.v7.widget.GridLayoutManager;
     27 import android.support.v7.widget.RecyclerView;
     28 import android.view.Gravity;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.View.OnFocusChangeListener;
     32 import android.view.ViewConfiguration;
     33 import android.view.ViewGroup;
     34 import android.view.accessibility.AccessibilityEvent;
     35 import android.widget.TextView;
     36 
     37 import com.android.launcher3.AppInfo;
     38 import com.android.launcher3.BubbleTextView;
     39 import com.android.launcher3.Launcher;
     40 import com.android.launcher3.R;
     41 import com.android.launcher3.Utilities;
     42 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
     43 import com.android.launcher3.anim.SpringAnimationHandler;
     44 import com.android.launcher3.config.FeatureFlags;
     45 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
     46 import com.android.launcher3.discovery.AppDiscoveryItemView;
     47 import com.android.launcher3.util.PackageManagerHelper;
     48 
     49 import java.util.List;
     50 
     51 /**
     52  * The grid view adapter of all the apps.
     53  */
     54 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
     55 
     56     public static final String TAG = "AppsGridAdapter";
     57 
     58     // A normal icon
     59     public static final int VIEW_TYPE_ICON = 1 << 1;
     60     // A prediction icon
     61     public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2;
     62     // The message shown when there are no filtered results
     63     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3;
     64     // The message to continue to a market search when there are no filtered results
     65     public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4;
     66 
     67     // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
     68     // but differ in enough attributes to require different view types
     69 
     70     // A divider that separates the apps list and the search market button
     71     public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5;
     72     // The divider that separates prediction icons from the app list
     73     public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 6;
     74     public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 7;
     75     public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 8;
     76 
     77     // Common view type masks
     78     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_MARKET_DIVIDER
     79             | VIEW_TYPE_PREDICTION_DIVIDER;
     80     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
     81             | VIEW_TYPE_PREDICTION_ICON;
     82     public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
     83             | VIEW_TYPE_DISCOVERY_ITEM;
     84     public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON
     85             | VIEW_TYPE_PREDICTION_DIVIDER;
     86 
     87 
     88     public interface BindViewCallback {
     89         void onBindView(ViewHolder holder);
     90     }
     91 
     92     /**
     93      * ViewHolder for each icon.
     94      */
     95     public static class ViewHolder extends RecyclerView.ViewHolder {
     96 
     97         public ViewHolder(View v) {
     98             super(v);
     99         }
    100     }
    101 
    102     /**
    103      * A subclass of GridLayoutManager that overrides accessibility values during app search.
    104      */
    105     public class AppsGridLayoutManager extends GridLayoutManager {
    106 
    107         public AppsGridLayoutManager(Context context) {
    108             super(context, 1, GridLayoutManager.VERTICAL, false);
    109         }
    110 
    111         @Override
    112         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    113             super.onInitializeAccessibilityEvent(event);
    114 
    115             // Ensure that we only report the number apps for accessibility not including other
    116             // adapter views
    117             final AccessibilityRecordCompat record = AccessibilityEventCompat
    118                     .asRecord(event);
    119             record.setItemCount(mApps.getNumFilteredApps());
    120             record.setFromIndex(Math.max(0,
    121                     record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
    122             record.setToIndex(Math.max(0,
    123                     record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
    124         }
    125 
    126         @Override
    127         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
    128                 RecyclerView.State state) {
    129             return super.getRowCountForAccessibility(recycler, state) -
    130                     getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
    131         }
    132 
    133         @Override
    134         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
    135                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
    136             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
    137 
    138             ViewGroup.LayoutParams lp = host.getLayoutParams();
    139             AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
    140             if (!(lp instanceof LayoutParams) || (cic == null)) {
    141                 return;
    142             }
    143             LayoutParams glp = (LayoutParams) lp;
    144             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
    145                     cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
    146                     cic.getRowSpan(),
    147                     cic.getColumnIndex(),
    148                     cic.getColumnSpan(),
    149                     cic.isHeading(),
    150                     cic.isSelected()));
    151         }
    152 
    153         /**
    154          * Returns the number of rows before {@param adapterPosition}, including this position
    155          * which should not be counted towards the collection info.
    156          */
    157         private int getRowsNotForAccessibility(int adapterPosition) {
    158             List<AdapterItem> items = mApps.getAdapterItems();
    159             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
    160             int extraRows = 0;
    161             for (int i = 0; i <= adapterPosition; i++) {
    162                 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
    163                     extraRows++;
    164                 }
    165             }
    166             return extraRows;
    167         }
    168     }
    169 
    170     /**
    171      * Helper class to size the grid items.
    172      */
    173     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
    174 
    175         public GridSpanSizer() {
    176             super();
    177             setSpanIndexCacheEnabled(true);
    178         }
    179 
    180         @Override
    181         public int getSpanSize(int position) {
    182             if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
    183                 return 1;
    184             } else {
    185                     // Section breaks span the full width
    186                     return mAppsPerRow;
    187             }
    188         }
    189     }
    190 
    191     private final Launcher mLauncher;
    192     private final LayoutInflater mLayoutInflater;
    193     private final AlphabeticalAppsList mApps;
    194     private final GridLayoutManager mGridLayoutMgr;
    195     private final GridSpanSizer mGridSizer;
    196     private final View.OnClickListener mIconClickListener;
    197     private final View.OnLongClickListener mIconLongClickListener;
    198 
    199     private int mAppsPerRow;
    200 
    201     private BindViewCallback mBindViewCallback;
    202     private OnFocusChangeListener mIconFocusListener;
    203 
    204     // The text to show when there are no search results and no market search handler.
    205     private String mEmptySearchMessage;
    206     // The intent to send off to the market app, updated each time the search query changes.
    207     private Intent mMarketSearchIntent;
    208 
    209     private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
    210 
    211     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
    212             iconClickListener, View.OnLongClickListener iconLongClickListener) {
    213         Resources res = launcher.getResources();
    214         mLauncher = launcher;
    215         mApps = apps;
    216         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
    217         mGridSizer = new GridSpanSizer();
    218         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
    219         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
    220         mLayoutInflater = LayoutInflater.from(launcher);
    221         mIconClickListener = iconClickListener;
    222         mIconLongClickListener = iconLongClickListener;
    223         if (FeatureFlags.LAUNCHER3_PHYSICS) {
    224             mSpringAnimationHandler = new SpringAnimationHandler<>(
    225                     SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory());
    226         }
    227     }
    228 
    229     public SpringAnimationHandler getSpringAnimationHandler() {
    230         return mSpringAnimationHandler;
    231     }
    232 
    233     public static boolean isDividerViewType(int viewType) {
    234         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
    235     }
    236 
    237     public static boolean isIconViewType(int viewType) {
    238         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
    239     }
    240 
    241     public static boolean isViewType(int viewType, int viewTypeMask) {
    242         return (viewType & viewTypeMask) != 0;
    243     }
    244 
    245     /**
    246      * Sets the number of apps per row.
    247      */
    248     public void setNumAppsPerRow(int appsPerRow) {
    249         mAppsPerRow = appsPerRow;
    250         mGridLayoutMgr.setSpanCount(appsPerRow);
    251     }
    252 
    253     public int getNumAppsPerRow() {
    254         return mAppsPerRow;
    255     }
    256 
    257     public void setIconFocusListener(OnFocusChangeListener focusListener) {
    258         mIconFocusListener = focusListener;
    259     }
    260 
    261     /**
    262      * Sets the last search query that was made, used to show when there are no results and to also
    263      * seed the intent for searching the market.
    264      */
    265     public void setLastSearchQuery(String query) {
    266         Resources res = mLauncher.getResources();
    267         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
    268         mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
    269     }
    270 
    271     /**
    272      * Sets the callback for when views are bound.
    273      */
    274     public void setBindViewCallback(BindViewCallback cb) {
    275         mBindViewCallback = cb;
    276     }
    277 
    278     /**
    279      * Returns the grid layout manager.
    280      */
    281     public GridLayoutManager getLayoutManager() {
    282         return mGridLayoutMgr;
    283     }
    284 
    285     @Override
    286     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    287         switch (viewType) {
    288             case VIEW_TYPE_ICON:
    289             case VIEW_TYPE_PREDICTION_ICON:
    290                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
    291                         R.layout.all_apps_icon, parent, false);
    292                 icon.setOnClickListener(mIconClickListener);
    293                 icon.setOnLongClickListener(mIconLongClickListener);
    294                 icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
    295                 icon.setOnFocusChangeListener(mIconFocusListener);
    296 
    297                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
    298                 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
    299                 return new ViewHolder(icon);
    300             case VIEW_TYPE_DISCOVERY_ITEM:
    301                 AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
    302                         .inflate(R.layout.all_apps_discovery_item, parent, false);
    303                 appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
    304                         mIconLongClickListener);
    305                 return new ViewHolder(appDiscoveryItemView);
    306             case VIEW_TYPE_EMPTY_SEARCH:
    307                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
    308                         parent, false));
    309             case VIEW_TYPE_SEARCH_MARKET:
    310                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
    311                         parent, false);
    312                 searchMarketView.setOnClickListener(new View.OnClickListener() {
    313                     @Override
    314                     public void onClick(View v) {
    315                         mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
    316                     }
    317                 });
    318                 return new ViewHolder(searchMarketView);
    319             case VIEW_TYPE_APPS_LOADING_DIVIDER:
    320                 View loadingDividerView = mLayoutInflater.inflate(
    321                         R.layout.all_apps_discovery_loading_divider, parent, false);
    322                 return new ViewHolder(loadingDividerView);
    323             case VIEW_TYPE_PREDICTION_DIVIDER:
    324             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
    325                 return new ViewHolder(mLayoutInflater.inflate(
    326                         R.layout.all_apps_divider, parent, false));
    327             default:
    328                 throw new RuntimeException("Unexpected view type");
    329         }
    330     }
    331 
    332     @Override
    333     public void onBindViewHolder(ViewHolder holder, int position) {
    334         switch (holder.getItemViewType()) {
    335             case VIEW_TYPE_ICON:
    336             case VIEW_TYPE_PREDICTION_ICON:
    337                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
    338                 BubbleTextView icon = (BubbleTextView) holder.itemView;
    339                 icon.applyFromApplicationInfo(info);
    340                 break;
    341             case VIEW_TYPE_DISCOVERY_ITEM:
    342                 AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
    343                         mApps.getAdapterItems().get(position).appInfo;
    344                 AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView;
    345                 view.apply(appDiscoveryAppInfo);
    346                 break;
    347             case VIEW_TYPE_EMPTY_SEARCH:
    348                 TextView emptyViewText = (TextView) holder.itemView;
    349                 emptyViewText.setText(mEmptySearchMessage);
    350                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
    351                         Gravity.START | Gravity.CENTER_VERTICAL);
    352                 break;
    353             case VIEW_TYPE_SEARCH_MARKET:
    354                 TextView searchView = (TextView) holder.itemView;
    355                 if (mMarketSearchIntent != null) {
    356                     searchView.setVisibility(View.VISIBLE);
    357                 } else {
    358                     searchView.setVisibility(View.GONE);
    359                 }
    360                 break;
    361             case VIEW_TYPE_APPS_LOADING_DIVIDER:
    362                 int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
    363                 int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
    364                 holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading);
    365                 holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded);
    366                 break;
    367             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
    368                 // nothing to do
    369                 break;
    370         }
    371         if (mBindViewCallback != null) {
    372             mBindViewCallback.onBindView(holder);
    373         }
    374     }
    375 
    376     @Override
    377     public void onViewAttachedToWindow(ViewHolder holder) {
    378         int type = holder.getItemViewType();
    379         if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
    380             mSpringAnimationHandler.add(holder.itemView, holder);
    381         }
    382     }
    383 
    384     @Override
    385     public void onViewDetachedFromWindow(ViewHolder holder) {
    386         int type = holder.getItemViewType();
    387         if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
    388             mSpringAnimationHandler.remove(holder.itemView);
    389         }
    390     }
    391 
    392     @Override
    393     public boolean onFailedToRecycleView(ViewHolder holder) {
    394         // Always recycle and we will reset the view when it is bound
    395         return true;
    396     }
    397 
    398     @Override
    399     public int getItemCount() {
    400         return mApps.getAdapterItems().size();
    401     }
    402 
    403     @Override
    404     public int getItemViewType(int position) {
    405         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
    406         return item.viewType;
    407     }
    408 
    409     /**
    410      * Helper class to set the SpringAnimation values for an item in the adapter.
    411      */
    412     private class AllAppsSpringAnimationFactory
    413             implements SpringAnimationHandler.AnimationFactory<ViewHolder> {
    414         private static final float DEFAULT_MAX_VALUE_PX = 100;
    415         private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX;
    416 
    417         // Damping ratio range is [0, 1]
    418         private static final float SPRING_DAMPING_RATIO = 0.55f;
    419 
    420         // Stiffness is a non-negative number.
    421         private static final float MIN_SPRING_STIFFNESS = 580f;
    422         private static final float MAX_SPRING_STIFFNESS = 900f;
    423 
    424         // The amount by which each adjacent rows' stiffness will differ.
    425         private static final float ROW_STIFFNESS_COEFFICIENT = 50f;
    426 
    427         @Override
    428         public SpringAnimation initialize(ViewHolder vh) {
    429             return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0);
    430         }
    431 
    432         /**
    433          * @param spring A new or recycled SpringAnimation.
    434          * @param vh The ViewHolder that {@param spring} is related to.
    435          */
    436         @Override
    437         public void update(SpringAnimation spring, ViewHolder vh) {
    438             int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size());
    439             int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps,
    440                     mAppsPerRow);
    441 
    442             int col = appPosition % mAppsPerRow;
    443             int row = appPosition / mAppsPerRow;
    444 
    445             int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count
    446             if (row > (numTotalRows / 2)) {
    447                 // Mirror the rows so that the top row acts the same as the bottom row.
    448                 row = Math.abs(numTotalRows - row);
    449             }
    450 
    451             calculateSpringValues(spring, row, col);
    452         }
    453 
    454         @Override
    455         public void setDefaultValues(SpringAnimation spring) {
    456             calculateSpringValues(spring, 0, mAppsPerRow / 2);
    457         }
    458 
    459         /**
    460          * We manipulate the stiffness, min, and max values based on the items distance to the
    461          * first row and the items distance to the center column to create the ^-shaped motion
    462          * effect.
    463          */
    464         private void calculateSpringValues(SpringAnimation spring, int row, int col) {
    465             float rowFactor = (1 + row) * 0.5f;
    466             float colFactor = getColumnFactor(col, mAppsPerRow);
    467 
    468             float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor);
    469             float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor);
    470 
    471             float stiffness = Utilities.boundToRange(
    472                     MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT),
    473                     MIN_SPRING_STIFFNESS,
    474                     MAX_SPRING_STIFFNESS);
    475 
    476             spring.setMinValue(minValue)
    477                     .setMaxValue(maxValue)
    478                     .getSpring()
    479                     .setStiffness(stiffness)
    480                     .setDampingRatio(SPRING_DAMPING_RATIO);
    481         }
    482 
    483         /**
    484          * @return The app position is the position of the app in the Adapter if we ignored all
    485          * other view types.
    486          *
    487          * The first app is at position 0, and the first app each following row is at a
    488          * position that is a multiple of {@param appsPerRow}.
    489          *
    490          * ie. If there are 5 apps per row, and there are two rows of apps:
    491          *     0 1 2 3 4
    492          *     5 6 7 8 9
    493          */
    494         private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
    495             if (position < numPredictedApps) {
    496                 // Predicted apps are first in the adapter.
    497                 return position;
    498             }
    499 
    500             // There is at most 1 divider view between the predicted apps and the alphabetical apps.
    501             int numDividerViews = numPredictedApps == 0 ? 0 : 1;
    502 
    503             // This offset takes into consideration an incomplete row of predicted apps.
    504             int numPredictedAppsOffset = appsPerRow - numPredictedApps;
    505             return position + numPredictedAppsOffset - numDividerViews;
    506         }
    507 
    508         /**
    509          * Increase the column factor as the distance increases between the column and the center
    510          * column(s).
    511          */
    512         private float getColumnFactor(int col, int numCols) {
    513             float centerColumn = numCols / 2;
    514             int distanceToCenter = (int) Math.abs(col - centerColumn);
    515 
    516             boolean evenNumberOfColumns = numCols % 2 == 0;
    517             if (evenNumberOfColumns && col < centerColumn) {
    518                 distanceToCenter -= 1;
    519             }
    520 
    521             float factor = 0;
    522             while (distanceToCenter > 0) {
    523                 if (distanceToCenter == 1) {
    524                     factor += 0.2f;
    525                 } else {
    526                     factor += 0.1f;
    527                 }
    528                 --distanceToCenter;
    529             }
    530 
    531             return factor;
    532         }
    533     }
    534 }
    535